auto-git:
[add] .DS_Store [add] README.md [add] auth.json [add] beautify-audio.py [add] onetagger-autotagger.json [add] requirements.txt
This commit is contained in:
64
README.md
Normal file
64
README.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Beautify Audio
|
||||||
|
|
||||||
|
One-file CLI to make tracks library-ready: convert to 320 kbps MP3, wipe old tags, retag via OneTagger Auto Tag, and rename to `Artist - Title.mp3` with collision safety.
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- Converts any ffmpeg-readable source to 44.1 kHz stereo 320 kbps CBR MP3 (optional loudness normalization, default -11.5 LUFS).
|
||||||
|
- Strips existing metadata before retagging.
|
||||||
|
- Runs OneTagger Auto Tag with `onetagger-autotagger.json` (Discogs/Spotify creds can be injected).
|
||||||
|
- Renames the result using tagged artist/title; appends `-1`, `-2`, etc. to avoid overwrites.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
1) Install Python deps: `pip install -r requirements.txt`
|
||||||
|
2) Install `ffmpeg` and ensure it is on PATH.
|
||||||
|
3) Install OneTagger CLI (≥1.7). On macOS, build from source and place the resulting `onetagger-cli` on PATH or next to this script. https://github.com/OneTagger/OneTagger
|
||||||
|
4) Run: `python beautify-audio.py /path/to/track.wav`
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- Python 3.9+.
|
||||||
|
- `ffmpeg` on PATH.
|
||||||
|
- Python deps from `requirements.txt` (currently `mutagen`).
|
||||||
|
- OneTagger CLI on PATH or placed next to the script as `onetagger-cli` (not bundled here because it is GPL-licensed). The Auto Tag profile is provided as `onetagger-autotagger.json`.
|
||||||
|
- The helper module `onetagger_integration.py` must remain in the repo root (the script prepends the parent dir to `PYTHONPATH`).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```bash
|
||||||
|
python beautify-audio.py /path/to/track.wav
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful flags:
|
||||||
|
- `--output-dir DIR` – where to place the processed MP3 (default: source folder).
|
||||||
|
- `--lufs -9.5` – change loudness target; use `--no-loudness` to disable normalization.
|
||||||
|
- `--artist/--title/--album` – seed metadata hints for tagging/renaming.
|
||||||
|
- `--onetagger-cli /path/to/onetagger-cli` – override CLI path (otherwise tries script folder or PATH).
|
||||||
|
- `--skip-onetagger` – only convert/strip/rename, skip tagging.
|
||||||
|
- Auth: `--discogs-token`, `--spotify-id`, `--spotify-secret`, or `--auth-file auth.json`.
|
||||||
|
|
||||||
|
### Authentication resolution
|
||||||
|
Order of precedence:
|
||||||
|
1) CLI flags
|
||||||
|
2) `--auth-file` JSON with `discogs_token`, `spotify_id`, `spotify_secret`
|
||||||
|
3) Environment variables `DISCOGS_TOKEN`, `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`
|
||||||
|
4) `auth.json` next to the script (if present)
|
||||||
|
|
||||||
|
Missing Spotify/Discogs creds cause those platforms to be dropped from the config copy for that run.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1) Pick output path with collision-safe suffixing.
|
||||||
|
2) Convert source to MP3 (optional loudness normalization).
|
||||||
|
3) Strip all metadata (skipped if `mutagen` is missing).
|
||||||
|
4) Optionally rename for better OneTagger filename hints.
|
||||||
|
5) Run OneTagger Auto Tag.
|
||||||
|
6) Rename final file to a sanitized `Artist - Title.mp3`.
|
||||||
|
|
||||||
|
## Example (with credentials file)
|
||||||
|
```bash
|
||||||
|
python beautify-audio.py ~/Downloads/Track.wav \
|
||||||
|
--output-dir ~/Music/Prep \
|
||||||
|
--lufs -12 \
|
||||||
|
--auth-file ~/secrets/onetagger-auth.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Automator-friendly: one input file in, one clean MP3 out.
|
||||||
|
- If OneTagger is missing or fails, conversion and cleanup still run; renaming uses best-effort metadata.
|
||||||
5
auth.json
Normal file
5
auth.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"discogs_token": "",
|
||||||
|
"spotify_id": "",
|
||||||
|
"spotify_secret": ""
|
||||||
|
}
|
||||||
481
beautify-audio.py
Normal file
481
beautify-audio.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Beautify Audio CLI.
|
||||||
|
|
||||||
|
Pipeline:
|
||||||
|
- Convert any supported audio file to a 320 kbps CBR MP3 (optional LUFS loudness correction).
|
||||||
|
- Strip existing metadata.
|
||||||
|
- Tag the file with OneTagger using the bundled AutoTagger profile (tokens/creds can be passed in).
|
||||||
|
- Rename the output using tagged artist/title (with fallbacks).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Keep this script self-contained except for the OneTagger integration helper.
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
if str(REPO_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import mutagen # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
mutagen = None
|
||||||
|
|
||||||
|
from onetagger_integration import ( # type: ignore
|
||||||
|
OneTaggerCliNotFoundError,
|
||||||
|
tag_audio_with_onetagger,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Beautify Audio: convert, clean, tag, and rename a single audio file."
|
||||||
|
)
|
||||||
|
parser.add_argument("input_file", type=Path, help="Path to the source audio file")
|
||||||
|
parser.add_argument(
|
||||||
|
"--lufs",
|
||||||
|
type=float,
|
||||||
|
default=-11.5,
|
||||||
|
help="Integrated loudness target (LUFS). Valid range: -16 to 0. Default: -11.5",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-loudness",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable loudness correction (conversion still runs at 320 kbps CBR).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-dir",
|
||||||
|
type=Path,
|
||||||
|
help="Output directory (defaults to the input file's directory).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--onetagger-cli",
|
||||||
|
type=Path,
|
||||||
|
help="Path to onetagger-cli. Defaults to PATH lookup or the script folder copy.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--artist", help="Optional artist name to help with renaming.")
|
||||||
|
parser.add_argument("--title", help="Optional title to help with renaming.")
|
||||||
|
parser.add_argument("--album", help="Optional album name (passed to OneTagger config copy).")
|
||||||
|
parser.add_argument(
|
||||||
|
"--discogs-token",
|
||||||
|
default=None,
|
||||||
|
help="Discogs token for OneTagger (CLI overrides auth file/env).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--spotify-id",
|
||||||
|
default=None,
|
||||||
|
help="Spotify client ID for OneTagger (CLI overrides auth file/env).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--spotify-secret",
|
||||||
|
default=None,
|
||||||
|
help="Spotify client secret for OneTagger (CLI overrides auth file/env).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--auth-file",
|
||||||
|
type=Path,
|
||||||
|
help="Optional JSON file with discogs_token/spotify_id/spotify_secret. Defaults to auth.json next to the script if present.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--skip-onetagger",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip the OneTagger tagging step (conversion + cleanup still happen).",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_inputs(args: argparse.Namespace) -> tuple[Path, Path]:
|
||||||
|
src = args.input_file.expanduser().resolve()
|
||||||
|
if not src.exists() or not src.is_file():
|
||||||
|
raise FileNotFoundError(f"Input file not found: {src}")
|
||||||
|
if not (-16.0 <= args.lufs <= 0.0):
|
||||||
|
raise ValueError("LUFS must be between -16.0 and 0.0")
|
||||||
|
out_dir = args.output_dir.expanduser().resolve() if args.output_dir else src.parent
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return src, out_dir
|
||||||
|
|
||||||
|
|
||||||
|
def next_available_path(path: Path) -> Path:
|
||||||
|
"""Return a unique path by appending '-<n>' before the suffix if needed."""
|
||||||
|
if not path.exists():
|
||||||
|
return path
|
||||||
|
stem = path.stem
|
||||||
|
suffix = path.suffix
|
||||||
|
counter = 1
|
||||||
|
while True:
|
||||||
|
candidate = path.with_name(f"{stem}-{counter}{suffix}")
|
||||||
|
if not candidate.exists():
|
||||||
|
return candidate
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
|
||||||
|
def find_onetagger_cli(user_supplied: Path | None) -> Path | None:
|
||||||
|
"""Resolve onetagger-cli from user input, script folder, or PATH."""
|
||||||
|
candidates: list[Path] = []
|
||||||
|
if user_supplied:
|
||||||
|
candidates.append(user_supplied)
|
||||||
|
script_dir = Path(__file__).resolve().parent
|
||||||
|
candidates.append(script_dir / "onetagger-cli")
|
||||||
|
found_in_path = shutil.which("onetagger-cli")
|
||||||
|
if found_in_path:
|
||||||
|
candidates.append(Path(found_in_path))
|
||||||
|
|
||||||
|
for cand in candidates:
|
||||||
|
if cand and cand.exists():
|
||||||
|
if os.access(cand, os.X_OK):
|
||||||
|
return cand.resolve()
|
||||||
|
raise OneTaggerCliNotFoundError(f"onetagger-cli is not executable: {cand}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_mp3(
|
||||||
|
src: Path,
|
||||||
|
dest: Path,
|
||||||
|
loudness_enabled: bool,
|
||||||
|
target_lufs: float,
|
||||||
|
) -> Path:
|
||||||
|
"""Convert to 320 kbps CBR MP3 with optional loudness correction."""
|
||||||
|
if not shutil.which("ffmpeg"):
|
||||||
|
raise RuntimeError("ffmpeg not found in PATH. Please install ffmpeg.")
|
||||||
|
cmd = ["ffmpeg", "-y", "-i", str(src), "-vn"]
|
||||||
|
if loudness_enabled:
|
||||||
|
cmd += ["-af", f"loudnorm=I={target_lufs}:TP=-1.5:LRA=11"]
|
||||||
|
cmd += ["-c:a", "libmp3lame", "-b:a", "320k", "-ar", "44100", "-ac", "2", str(dest)]
|
||||||
|
print(f"🔊 Converting → {dest}")
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"ffmpeg failed (exit {result.returncode}).\nSTDERR:\n{result.stderr or result.stdout}"
|
||||||
|
)
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def read_simple_tags(path: Path) -> tuple[str | None, str | None, str | None]:
|
||||||
|
"""Read (title, artist, album) tags using mutagen if available."""
|
||||||
|
if mutagen is None:
|
||||||
|
return None, None, None
|
||||||
|
try:
|
||||||
|
audio = mutagen.File(str(path), easy=True)
|
||||||
|
if not audio:
|
||||||
|
return None, None, None
|
||||||
|
title = audio.get("title", [None])[0]
|
||||||
|
artist = audio.get("artist", [None])[0]
|
||||||
|
album = audio.get("album", [None])[0]
|
||||||
|
return title, artist, album
|
||||||
|
except Exception:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_filename_artist_title(path: Path) -> tuple[str | None, str | None]:
|
||||||
|
"""If filename looks like 'Artist - Title', split and return parts."""
|
||||||
|
stem = path.stem
|
||||||
|
if stem.count("-") != 1:
|
||||||
|
return None, None
|
||||||
|
artist, title = [part.strip() for part in stem.split("-", 1)]
|
||||||
|
return (artist or None, title or None)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_seed_metadata(
|
||||||
|
path: Path, cli_artist: str | None, cli_title: str | None, cli_album: str | None
|
||||||
|
) -> dict[str, str | None]:
|
||||||
|
"""Choose the best available artist/title/album for tagging and renaming."""
|
||||||
|
tag_title, tag_artist, tag_album = read_simple_tags(path)
|
||||||
|
fname_artist, fname_title = parse_filename_artist_title(path)
|
||||||
|
|
||||||
|
artist = cli_artist or tag_artist or fname_artist
|
||||||
|
title = cli_title or tag_title or fname_title
|
||||||
|
album = cli_album or tag_album
|
||||||
|
|
||||||
|
return {
|
||||||
|
"artist": artist,
|
||||||
|
"title": title,
|
||||||
|
"album": album,
|
||||||
|
"fname_artist": fname_artist,
|
||||||
|
"fname_title": fname_title,
|
||||||
|
"tag_artist": tag_artist,
|
||||||
|
"tag_title": tag_title,
|
||||||
|
"tag_album": tag_album,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_auth_credentials(auth_file: Path | None) -> dict[str, str]:
|
||||||
|
"""Load auth data from JSON if present; keys: discogs_token, spotify_id, spotify_secret."""
|
||||||
|
data = {"discogs_token": "", "spotify_id": "", "spotify_secret": ""}
|
||||||
|
candidate = auth_file
|
||||||
|
if candidate and candidate.exists():
|
||||||
|
try:
|
||||||
|
with candidate.open("r", encoding="utf-8") as f:
|
||||||
|
parsed = json.load(f)
|
||||||
|
data["discogs_token"] = parsed.get("discogs_token", "") or ""
|
||||||
|
data["spotify_id"] = parsed.get("spotify_id", "") or ""
|
||||||
|
data["spotify_secret"] = parsed.get("spotify_secret", "") or ""
|
||||||
|
print(f"🔑 Loaded auth from {candidate}")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"⚠️ Failed to read auth file {candidate}: {exc}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def strip_all_metadata(path: Path) -> None:
|
||||||
|
"""Remove all tags from the file in-place."""
|
||||||
|
if mutagen is None:
|
||||||
|
print("⚠️ mutagen not installed; skipping metadata strip.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
audio = mutagen.File(str(path))
|
||||||
|
if audio is None:
|
||||||
|
return
|
||||||
|
if hasattr(audio, "delete"):
|
||||||
|
audio.delete()
|
||||||
|
elif audio.tags is not None:
|
||||||
|
audio.tags.clear()
|
||||||
|
audio.save()
|
||||||
|
print("🧹 Stripped existing metadata.")
|
||||||
|
except Exception as exc: # pragma: no cover - defensive
|
||||||
|
print(f"⚠️ Failed to strip metadata: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_onetagger_config(
|
||||||
|
base_config: Path,
|
||||||
|
discogs_token: str,
|
||||||
|
spotify_id: str,
|
||||||
|
spotify_secret: str,
|
||||||
|
album: str | None = None,
|
||||||
|
seed_artist: str | None = None,
|
||||||
|
seed_title: str | None = None,
|
||||||
|
) -> Path:
|
||||||
|
"""Copy the base config and inject credentials when provided."""
|
||||||
|
with base_config.open("r", encoding="utf-8") as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
|
||||||
|
cfg.setdefault("custom", {})
|
||||||
|
cfg.setdefault("platforms", cfg.get("platforms") or [])
|
||||||
|
|
||||||
|
if discogs_token:
|
||||||
|
cfg["custom"].setdefault("discogs", {})["token"] = discogs_token
|
||||||
|
if "discogs" not in cfg["platforms"]:
|
||||||
|
cfg["platforms"].append("discogs")
|
||||||
|
if spotify_id and spotify_secret:
|
||||||
|
cfg["spotify"] = {"clientId": spotify_id, "clientSecret": spotify_secret}
|
||||||
|
if "spotify" not in cfg["platforms"]:
|
||||||
|
cfg["platforms"].append("spotify")
|
||||||
|
|
||||||
|
defaults = cfg.setdefault("custom", {}).setdefault("defaults", {})
|
||||||
|
if album:
|
||||||
|
defaults["album"] = album
|
||||||
|
if seed_artist:
|
||||||
|
defaults["artist"] = seed_artist
|
||||||
|
if seed_title:
|
||||||
|
defaults["title"] = seed_title
|
||||||
|
|
||||||
|
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".json", prefix="onetagger_cfg_")
|
||||||
|
with open(tmp.name, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(cfg, f, indent=2)
|
||||||
|
return Path(tmp.name)
|
||||||
|
|
||||||
|
|
||||||
|
def run_onetagger(
|
||||||
|
audio_path: Path,
|
||||||
|
cli_path: Path,
|
||||||
|
base_config: Path,
|
||||||
|
discogs_token: str,
|
||||||
|
spotify_id: str,
|
||||||
|
spotify_secret: str,
|
||||||
|
album: str | None,
|
||||||
|
seed_artist: str | None,
|
||||||
|
seed_title: str | None,
|
||||||
|
) -> bool:
|
||||||
|
cfg_copy = prepare_onetagger_config(
|
||||||
|
base_config,
|
||||||
|
discogs_token,
|
||||||
|
spotify_id,
|
||||||
|
spotify_secret,
|
||||||
|
album,
|
||||||
|
seed_artist,
|
||||||
|
seed_title,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
res = tag_audio_with_onetagger(audio_file=audio_path, onetagger_cli_path=cli_path, config_path=cfg_copy)
|
||||||
|
if res.success:
|
||||||
|
print("🪪 OneTagger tagging succeeded.")
|
||||||
|
return True
|
||||||
|
print(f"⚠️ OneTagger tagging failed: {res.stderr or 'unknown error'}")
|
||||||
|
return False
|
||||||
|
except OneTaggerCliNotFoundError as exc:
|
||||||
|
print(f"⚠️ {exc}")
|
||||||
|
return False
|
||||||
|
except Exception as exc: # pragma: no cover - defensive
|
||||||
|
print(f"⚠️ OneTagger error: {exc}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
cfg_copy.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def build_safe_basename(path: Path, fallback_artist: str | None, fallback_title: str | None) -> str:
|
||||||
|
"""Derive a sanitized base filename using tags when available."""
|
||||||
|
tag_title, tag_artist, _ = read_simple_tags(path)
|
||||||
|
artist = tag_artist or fallback_artist
|
||||||
|
title = tag_title or fallback_title
|
||||||
|
if artist and title:
|
||||||
|
base = f"{artist} - {title}"
|
||||||
|
elif artist:
|
||||||
|
base = f"{artist} - {path.stem}"
|
||||||
|
elif title:
|
||||||
|
base = f"{path.stem} - {title}"
|
||||||
|
else:
|
||||||
|
base = path.stem
|
||||||
|
return re.sub(r"[\\/:*?\"<>|]", "_", base).strip("_ ").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def rename_output(
|
||||||
|
path: Path,
|
||||||
|
output_dir: Path,
|
||||||
|
fallback_artist: str | None,
|
||||||
|
fallback_title: str | None,
|
||||||
|
) -> Path:
|
||||||
|
base = build_safe_basename(path, fallback_artist, fallback_title)
|
||||||
|
desired = output_dir / f"{base}{path.suffix}"
|
||||||
|
if desired.resolve() == path.resolve():
|
||||||
|
return path
|
||||||
|
dest = next_available_path(desired)
|
||||||
|
if dest.resolve() == path.resolve():
|
||||||
|
return path
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.rename(dest)
|
||||||
|
print(f"📝 Renamed to {dest.name}")
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def rename_for_tagging(path: Path, output_dir: Path, artist: str | None, title: str | None) -> Path:
|
||||||
|
"""Rename the working file so OneTagger sees a helpful filename."""
|
||||||
|
if not artist or not title:
|
||||||
|
return path
|
||||||
|
desired = output_dir / f"{artist} - {title}{path.suffix}"
|
||||||
|
if desired.resolve() == path.resolve():
|
||||||
|
return path
|
||||||
|
target = next_available_path(desired)
|
||||||
|
if target.resolve() == path.resolve():
|
||||||
|
return path
|
||||||
|
try:
|
||||||
|
path.rename(target)
|
||||||
|
print(f"🏷️ Using filename hint for tagging: {target.name}")
|
||||||
|
return target
|
||||||
|
except Exception:
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
src, out_dir = validate_inputs(args)
|
||||||
|
seed_info = collect_seed_metadata(src, args.artist, args.title, args.album)
|
||||||
|
album = args.album or seed_info.get("tag_album")
|
||||||
|
|
||||||
|
primary_artist = args.artist or seed_info.get("tag_artist") or seed_info.get("fname_artist")
|
||||||
|
primary_title = args.title or seed_info.get("tag_title") or seed_info.get("fname_title")
|
||||||
|
|
||||||
|
secondary_artist = None
|
||||||
|
secondary_title = None
|
||||||
|
if seed_info.get("fname_artist") and seed_info.get("fname_title"):
|
||||||
|
if (seed_info.get("fname_artist") != primary_artist) or (seed_info.get("fname_title") != primary_title):
|
||||||
|
secondary_artist = seed_info.get("fname_artist")
|
||||||
|
secondary_title = seed_info.get("fname_title")
|
||||||
|
|
||||||
|
# Derive the initial output path with collision avoidance
|
||||||
|
base_target = out_dir / f"{src.stem}.mp3"
|
||||||
|
target_mp3 = next_available_path(base_target)
|
||||||
|
|
||||||
|
loudness_on = not args.no_loudness
|
||||||
|
print(
|
||||||
|
f"🚀 Beautifying '{src.name}' → '{target_mp3.name}' "
|
||||||
|
f"(loudness {'on' if loudness_on else 'off'}, target {args.lufs} LUFS)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Auth resolution precedence:
|
||||||
|
# 1) CLI flags
|
||||||
|
# 2) Explicit auth file (--auth-file)
|
||||||
|
# 3) Environment variables
|
||||||
|
# 4) Default auth.json next to the script (only if nothing else provided)
|
||||||
|
explicit_auth = load_auth_credentials(args.auth_file)
|
||||||
|
discogs_token = args.discogs_token or explicit_auth["discogs_token"] or os.environ.get("DISCOGS_TOKEN", "")
|
||||||
|
spotify_id = args.spotify_id or explicit_auth["spotify_id"] or os.environ.get("SPOTIFY_CLIENT_ID", "")
|
||||||
|
spotify_secret = args.spotify_secret or explicit_auth["spotify_secret"] or os.environ.get("SPOTIFY_CLIENT_SECRET", "")
|
||||||
|
|
||||||
|
if (
|
||||||
|
not args.auth_file
|
||||||
|
and not discogs_token
|
||||||
|
and not spotify_id
|
||||||
|
and not spotify_secret
|
||||||
|
):
|
||||||
|
default_auth_path = Path(__file__).resolve().parent / "auth.json"
|
||||||
|
default_auth = load_auth_credentials(default_auth_path)
|
||||||
|
discogs_token = default_auth["discogs_token"]
|
||||||
|
spotify_id = default_auth["spotify_id"]
|
||||||
|
spotify_secret = default_auth["spotify_secret"]
|
||||||
|
|
||||||
|
converted = convert_to_mp3(src, target_mp3, loudness_on, args.lufs)
|
||||||
|
strip_all_metadata(converted)
|
||||||
|
|
||||||
|
working_file = rename_for_tagging(converted, out_dir, primary_artist, primary_title)
|
||||||
|
tagged = working_file
|
||||||
|
tag_success = False
|
||||||
|
cli_path = None
|
||||||
|
if not args.skip_onetagger:
|
||||||
|
cli_path = find_onetagger_cli(args.onetagger_cli)
|
||||||
|
cfg_path = Path(__file__).resolve().parent / "onetagger-autotagger.json"
|
||||||
|
if cli_path and cfg_path.exists():
|
||||||
|
tag_success = run_onetagger(
|
||||||
|
audio_path=working_file,
|
||||||
|
cli_path=cli_path,
|
||||||
|
base_config=cfg_path,
|
||||||
|
discogs_token=discogs_token,
|
||||||
|
spotify_id=spotify_id,
|
||||||
|
spotify_secret=spotify_secret,
|
||||||
|
album=album,
|
||||||
|
seed_artist=primary_artist,
|
||||||
|
seed_title=primary_title,
|
||||||
|
)
|
||||||
|
if not tag_success and secondary_artist and secondary_title:
|
||||||
|
working_file = rename_for_tagging(working_file, out_dir, secondary_artist, secondary_title)
|
||||||
|
tagged = working_file
|
||||||
|
tag_success = run_onetagger(
|
||||||
|
audio_path=working_file,
|
||||||
|
cli_path=cli_path,
|
||||||
|
base_config=cfg_path,
|
||||||
|
discogs_token=discogs_token,
|
||||||
|
spotify_id=spotify_id,
|
||||||
|
spotify_secret=spotify_secret,
|
||||||
|
album=album,
|
||||||
|
seed_artist=secondary_artist,
|
||||||
|
seed_title=secondary_title,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("⚠️ OneTagger skipped (binary or config not found).")
|
||||||
|
else:
|
||||||
|
print("⏭️ Skipping OneTagger tagging step as requested.")
|
||||||
|
|
||||||
|
fallback_artist = primary_artist or secondary_artist
|
||||||
|
fallback_title = primary_title or secondary_title
|
||||||
|
final_path = rename_output(tagged, out_dir, fallback_artist, fallback_title)
|
||||||
|
print(f"✅ Done: {final_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nAborted by user.")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"❌ Error: {exc}")
|
||||||
|
sys.exit(1)
|
||||||
101
onetagger-autotagger.json
Normal file
101
onetagger-autotagger.json
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"platforms": [
|
||||||
|
"beatport",
|
||||||
|
"spotify",
|
||||||
|
"discogs",
|
||||||
|
"bandcamp"
|
||||||
|
],
|
||||||
|
"path": null,
|
||||||
|
"tags": [
|
||||||
|
"genre",
|
||||||
|
"style",
|
||||||
|
"releaseDate",
|
||||||
|
"label",
|
||||||
|
"albumArt",
|
||||||
|
"artist",
|
||||||
|
"album",
|
||||||
|
"title"
|
||||||
|
],
|
||||||
|
"separators": {
|
||||||
|
"id3": ", ",
|
||||||
|
"vorbis": ", ",
|
||||||
|
"mp4": ", "
|
||||||
|
},
|
||||||
|
"id3v24": true,
|
||||||
|
"overwrite": true,
|
||||||
|
"overwriteTags": [],
|
||||||
|
"threads": 16,
|
||||||
|
"strictness": 0.8,
|
||||||
|
"mergeGenres": false,
|
||||||
|
"albumArtFile": true,
|
||||||
|
"camelot": false,
|
||||||
|
"parseFilename": true,
|
||||||
|
"filenameTemplate": "%artists% - %title%",
|
||||||
|
"shortTitle": true,
|
||||||
|
"matchDuration": false,
|
||||||
|
"maxDurationDifference": 30,
|
||||||
|
"matchById": false,
|
||||||
|
"multipleMatches": "Default",
|
||||||
|
"postCommand": null,
|
||||||
|
"stylesOptions": "default",
|
||||||
|
"stylesCustomTag": {
|
||||||
|
"id3": "STYLE",
|
||||||
|
"vorbis": "STYLE",
|
||||||
|
"mp4": "STYLE"
|
||||||
|
},
|
||||||
|
"trackNumberLeadingZeroes": 0,
|
||||||
|
"enableShazam": true,
|
||||||
|
"forceShazam": false,
|
||||||
|
"skipTagged": false,
|
||||||
|
"includeSubfolders": false,
|
||||||
|
"onlyYear": false,
|
||||||
|
"titleRegex": null,
|
||||||
|
"moveSuccess": false,
|
||||||
|
"moveSuccessPath": null,
|
||||||
|
"moveFailed": false,
|
||||||
|
"moveFailedPath": null,
|
||||||
|
"writeLrc": false,
|
||||||
|
"enhancedLrc": false,
|
||||||
|
"capitalizeGenres": false,
|
||||||
|
"id3CommLang": null,
|
||||||
|
"removeAllCovers": false,
|
||||||
|
"multiplatform": false,
|
||||||
|
"fetchAllResults": false,
|
||||||
|
"albumTagging": false,
|
||||||
|
"albumTaggingRatio": 0.5,
|
||||||
|
"coverFilename": null,
|
||||||
|
"custom": {
|
||||||
|
"bpmsupreme": {
|
||||||
|
"email": "",
|
||||||
|
"library": "Supreme",
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
|
"itunes": {
|
||||||
|
"art_resolution": 1000
|
||||||
|
},
|
||||||
|
"discogs": {
|
||||||
|
"max_albums": 4,
|
||||||
|
"token": "",
|
||||||
|
"track_number_int": true
|
||||||
|
},
|
||||||
|
"beatsource": {
|
||||||
|
"art_resolution": 500
|
||||||
|
},
|
||||||
|
"beatport": {
|
||||||
|
"art_resolution": 500,
|
||||||
|
"ignore_version": false,
|
||||||
|
"max_pages": 1
|
||||||
|
},
|
||||||
|
"deezer": {
|
||||||
|
"art_resolution": 1200,
|
||||||
|
"content_language": "en-US"
|
||||||
|
},
|
||||||
|
"bandcamp": {
|
||||||
|
"match_artist": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spotify": {
|
||||||
|
"clientId": "",
|
||||||
|
"clientSecret": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
mutagen>=1.47,<2.0
|
||||||
Reference in New Issue
Block a user