Add tagger.py

This commit is contained in:
aadit 2025-10-22 01:26:54 +05:30
parent 1160d360b4
commit e917b8de56

219
tagger.py Normal file
View file

@ -0,0 +1,219 @@
#!/usr/bin/env python3
import re
from pathlib import Path
from typing import List
def prompt(message: str, default=None, input_type=str):
"""Prompt user with optional default value."""
if default is not None:
msg = f"{message} [{default}]: "
else:
msg = f"{message}: "
while True:
try:
value = input(msg).strip()
if not value:
if default is not None:
return input_type(default)
print("This field is required.")
continue
return input_type(value)
except ValueError:
print(f"Invalid input. Please enter a valid {input_type.__name__}.")
def prompt_bool(message: str, default=True):
"""Prompt user for yes/no."""
default_str = "Y/n" if default else "y/N"
while True:
value = input(f"{message} [{default_str}]: ").strip().lower()
if not value:
return default
if value in ("y", "yes"):
return True
if value in ("n", "no"):
return False
print("Please enter 'y' or 'n'.")
def get_config():
"""Interactively gather configuration."""
print("\n" + "=" * 60)
print("Audio File Tagger")
print("=" * 60 + "\n")
# Required: folder path
while True:
outdir = Path(prompt("Enter folder path containing audio files"))
if outdir.is_dir():
break
print(f"Folder not found: {outdir}")
print("\nOptional settings (press Enter for defaults):\n")
album = prompt("Album name", default="The Bongo Cat Collection")
artist = prompt("Artist name", default="Bongo Cat")
album_artist = prompt("Album artist", default=artist)
disc = prompt("Disc number", default="2")
start_track = prompt(
"Starting track number", default="15", input_type=int
)
cover_filename = prompt(
"Cover filename (relative to folder)",
default="cover.png",
)
return {
"outdir": outdir,
"album": album,
"artist": artist,
"album_artist": album_artist,
"disc": disc,
"start_track": start_track,
"cover_filename": cover_filename,
}
def list_audio_files(folder: Path) -> List[Path]:
files = list(folder.glob("*.opus")) + list(folder.glob("*.ogg"))
def sort_key(p: Path):
m = re.match(r"^\s*(\d+)", p.stem)
return (int(m.group(1)) if m else 10**9, p.name.lower())
return sorted(files, key=sort_key)
def infer_clean_title(p: Path) -> str:
name = p.stem
name = re.sub(r"^\s*\d+\s*-\s*", "", name, count=1)
return name.strip()
def tag_one(
path: Path,
title: str,
track_number: int,
cover_path: Path,
artist: str,
album: str,
album_artist: str,
disc: str,
):
from mutagen.oggopus import OggOpus
from mutagen.oggvorbis import OggVorbis
from mutagen.flac import Picture
import base64
audio = None
try:
audio = OggOpus(path)
except Exception:
audio = OggVorbis(path)
if audio.tags is None:
audio.add_tags()
audio["TITLE"] = [title]
audio["ARTIST"] = [artist]
audio["ALBUM"] = [album]
audio["ALBUMARTIST"] = [album_artist]
audio["DISCNUMBER"] = [disc]
audio["TRACKNUMBER"] = [str(track_number)]
if cover_path.is_file():
data = cover_path.read_bytes()
pic = Picture()
pic.type = 3
pic.mime = "image/png"
pic.desc = "cover"
pic.width = 0
pic.height = 0
pic.depth = 0
pic.colors = 0
pic.data = data
b64 = base64.b64encode(pic.write()).decode("ascii")
audio["METADATA_BLOCK_PICTURE"] = [b64]
if "COVERART" in audio:
del audio["COVERART"]
if "COVERARTMIME" in audio:
del audio["COVERARTMIME"]
audio.save()
def main():
config = get_config()
cover_path = config["outdir"] / config["cover_filename"]
if not cover_path.exists():
print(
f"\n⚠ Warning: cover not found at {cover_path}; "
"will proceed without cover art.\n"
)
files = list_audio_files(config["outdir"])
if not files:
print("No .opus or .ogg files found.")
return
# Show summary
print("\n" + "=" * 60)
print("Configuration Summary:")
print("=" * 60)
print(f"Folder: {config['outdir']}")
print(f"Files to tag: {len(files)}")
print(f"Album: {config['album']}")
print(f"Artist: {config['artist']}")
print(f"Album Artist: {config['album_artist']}")
print(f"Disc Number: {config['disc']}")
print(f"Starting Track: {config['start_track']}")
print(f"Cover file: {cover_path.name} "
f"({'found' if cover_path.exists() else 'not found'})")
print("=" * 60 + "\n")
# Show first few files
print("Sample files to be tagged:")
for f in files[:5]:
title = infer_clean_title(f)
print(f" - {f.name}{title}")
if len(files) > 5:
print(f" ... and {len(files) - 5} more")
print()
proceed = prompt_bool("Proceed with tagging?", default=True)
if not proceed:
print("Cancelled.")
exit(0)
print(f"\nTagging {len(files)} files...\n")
for i, f in enumerate(files, start=1):
title = infer_clean_title(f)
track_no = config["start_track"] + (i - 1)
try:
tag_one(
f,
title,
track_no,
cover_path,
config["artist"],
config["album"],
config["album_artist"],
config["disc"],
)
print(f"[{i}/{len(files)}] ✓ {f.name} (track {track_no})")
except Exception as e:
print(f"[{i}/{len(files)}] ✗ {f.name}{e}")
print("\n✓ Done.")
if __name__ == "__main__":
main()