From 14ead1b03b4209eae97921d785dbfb0da5dc2fd4 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Sat, 25 Apr 2026 00:56:39 -0700 Subject: handle fixing sample rate, bit depth, and block size encoding --- README.md | 13 ++++++++- main.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- pyproject.toml | 1 + uv.lock | 25 +++++++++++++++- 4 files changed, 123 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a42cb1f..9f95f28 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,23 @@ # FIIO Snowsky Echo FLAC Media Tool -This is a small script to help owners who already maintain a FLAC library make their music show up nicer on the Snowsky Echo/Echo Mini +This is a small script to help owners who already maintain a FLAC library make their music work/show up nicer on the Snowsky Echo/Echo Mini The script does the following: - Recursively searches through the provided directory +- Re-samples audio higher than 192Khz 24bit via ffmpeg +- Re-encodes files with block size higher than 4096 via `flac` CLI - Rename FLAC file to `TRACK_NAME.flac` - Resize album art to 500x500px - Download LRC file +# External Dependencies +You need the FLAC command line tool to be accessible globally, meaning it must be able to run anywhere on your machine. Using the official tool was the most consistent way of fixing the block-size issue cross-platform. + +https://xiph.org/flac/download.html + +- Windows: `winget install -e --id Xiph.FLAC` +- Linux: `sudo pacman -S flac` (follow your package manager) +- macOS: `brew install flac` (idk tho i don't own a mac) + ```bash uv sync uv run main.py [--nolrc] diff --git a/main.py b/main.py index be9a742..dffe4b3 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index 4b6dca1..d6449eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Media tool for retagging FLACs for Snowsky Echo/Mini" readme = "README.md" requires-python = ">=3.13" dependencies = [ + "ffmpeg-python>=0.2.0", "mutagen>=1.47.0", "pillow>=12.2.0", "syncedlyrics>=1.0.1", diff --git a/uv.lock b/uv.lock index 06980a5..eec6aed 100644 --- a/uv.lock +++ b/uv.lock @@ -91,10 +91,23 @@ wheels = [ ] [[package]] -name = "fiio-snowsky-flac-tagger" +name = "ffmpeg-python" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "future" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, +] + +[[package]] +name = "fiio-snowsky-flac-media" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "ffmpeg-python" }, { name = "mutagen" }, { name = "pillow" }, { name = "syncedlyrics" }, @@ -103,12 +116,22 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "mutagen", specifier = ">=1.47.0" }, { name = "pillow", specifier = ">=12.2.0" }, { name = "syncedlyrics", specifier = ">=1.0.1" }, { name = "tqdm", specifier = ">=4.67.3" }, ] +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, +] + [[package]] name = "idna" version = "3.13" -- cgit v1.2.3