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