diff options
Diffstat (limited to 'main.py')
| -rw-r--r-- | main.py | 91 |
1 files changed, 86 insertions, 5 deletions
@@ -1,9 +1,12 @@ import argparse +import subprocess from pathlib import Path from typing import Iterator from tqdm import tqdm import syncedlyrics from mutagen.flac import FLAC +import ffmpeg + def iter_files(base: Path) -> Iterator[Path]: iterator = base.rglob("*") @@ -11,17 +14,73 @@ def iter_files(base: Path) -> Iterator[Path]: if p.is_file(): yield p.resolve() + def rename_file(filepath: Path, new_name: str) -> None: target = filepath.with_name(new_name) filepath.rename(target) +def get_audio_issues(path: Path) -> dict: + audio = FLAC(str(path)) + info = audio.info + sample_rate = getattr(info, "sample_rate", 0) + bits_per_sample = getattr(info, "bits_per_sample", 24) + max_blocksize = getattr(info, "max_blocksize", 4096) + + return { + "needs_sample_rate_fix": sample_rate > 192000, + "needs_bitdepth_fix": bits_per_sample > 24, + "needs_blocksize_fix": max_blocksize != 4096, + "sample_rate": sample_rate, + "bits_per_sample": bits_per_sample, + "max_blocksize": max_blocksize, + } + + +def fix_with_ffmpeg(path: Path, fix_sample_rate: bool, fix_bitdepth: bool) -> Path: + output_kwargs = { + "acodec": "flac", + "map": "0:a", + } + if fix_bitdepth: + output_kwargs["sample_fmt"] = "s24" + if fix_sample_rate: + output_kwargs["ar"] = "192000" + + temp_path = path.with_suffix(".tmp.flac") + ( + ffmpeg + .input(str(path)) + .output(str(temp_path), **output_kwargs) + .overwrite_output() + .run(quiet=True) + ) + path.unlink() + temp_path.rename(path) + return path + + +def fix_blocksize(path: Path, blocksize: int = 4096) -> Path: + temp_path = path.with_suffix(".tmp.flac") + command = ["flac", "--force", f"--blocksize={blocksize}", str(path), "-o", str(temp_path)] + try: + subprocess.run(command, capture_output=True, text=True, check=True) + path.unlink() + temp_path.rename(path) + except subprocess.CalledProcessError as e: + temp_path.unlink(missing_ok=True) + print(f" Error fixing blocksize for {path.name}:") + print(e.stderr) + return path + + def get_track_info(path: Path) -> tuple: audio = FLAC(str(path)) title = audio.get("TITLE", [""]) artist = audio.get("ARTIST", [""]) return (title[0], artist[0]) + def resize_album_art(path: Path) -> None: audio = FLAC(str(path)) if not audio.pictures: @@ -37,24 +96,45 @@ def resize_album_art(path: Path) -> None: pic.data = out.getvalue() audio.save() + def main(): parser = argparse.ArgumentParser() parser.add_argument("base_dir", type=Path) - parser.add_argument("--nolrc", "-n", action="store_true", dest="flag") + parser.add_argument("--nolrc", "-n", action="store_true", dest="nolrc") args = parser.parse_args() base = args.base_dir files = [p for p in iter_files(base) if p.suffix == ".flac"] for fp in tqdm(files, desc="Processing FLAC files", unit="file"): - print("Fetching track info and renaming file") + print(f"\nProcessing: {fp.name}") title, artist = get_track_info(fp) new_file_name = title + ".flac" rename_file(fp, new_file_name) fp = fp.with_name(new_file_name) - print("Resizing album art to 500x500") + issues = get_audio_issues(fp) + print(f" Stats: {issues['sample_rate']}Hz, {issues['bits_per_sample']}-bit, blocksize={issues['max_blocksize']}") + + if issues["needs_sample_rate_fix"] or issues["needs_bitdepth_fix"]: + reasons = [] + if issues["needs_sample_rate_fix"]: + reasons.append(f"sample rate {issues['sample_rate']}Hz -> 192000Hz") + if issues["needs_bitdepth_fix"]: + reasons.append(f"bit depth {issues['bits_per_sample']}-bit -> 24-bit") + print(f" Fixing via ffmpeg: {', '.join(reasons)}") + fp = fix_with_ffmpeg(fp, issues["needs_sample_rate_fix"], issues["needs_bitdepth_fix"]) + + post_info = FLAC(str(fp)).info + if getattr(post_info, "max_blocksize", 4096) != 4096: + issues["needs_blocksize_fix"] = True + + if issues["needs_blocksize_fix"]: + print(f" Fixing blocksize -> 4096 via flac CLI") + fp = fix_blocksize(fp) + + print(" Resizing album art to 500x500") resize_album_art(fp) if args.nolrc: @@ -62,14 +142,15 @@ def main(): lrc_path = fp.with_suffix(".lrc") if lrc_path.exists(): - print("Skipping", lrc_path, "as LRC already exists") + print(f" Skipping LRC for {fp.name} (already exists)") continue - print(f"Fetching LRC file for {title} {artist}") + print(f" Fetching LRC for: {title} - {artist}") lrc = syncedlyrics.search(f"{title} {artist}", providers=["Lrclib", "Megalobiz", "NetEase"]) with open(lrc_path, "w", encoding="utf-8") as f: f.write(lrc if lrc else "") + if __name__ == "__main__": main() |
