aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md13
-rw-r--r--main.py91
-rw-r--r--pyproject.toml1
-rw-r--r--uv.lock25
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 <base_dir> [--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,6 +116,7 @@ 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" },
@@ -110,6 +124,15 @@ requires-dist = [
]
[[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"
source = { registry = "https://pypi.org/simple" }
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage