Skip to content

Instantly share code, notes, and snippets.

@noaione
Last active August 16, 2025 15:33
Show Gist options
  • Save noaione/bf239ce7c66e9a50495193e011ddc6fa to your computer and use it in GitHub Desktop.
Save noaione/bf239ce7c66e9a50495193e011ddc6fa to your computer and use it in GitHub Desktop.
quick script to generate playlist for Navidrome for my umamusu discography | sorted by the "type" of the album then release date
# /// script
# dependencies = [
# "mutagen",
# ]
# ///
# run with `uv run uma-pl-gen.py` on the same root folder as all the albums folders
from pathlib import Path
from mutagen.flac import FLAC
from mutagen.mp4 import MP4
CURRENT_DIR = Path(__file__).resolve().parent
SEP = " • "
M3U8_TEMPLATE = """#EXTM3U
#PLAYLIST:Umamusume 🐴
"""
album_candidates: list[tuple[str, str, Path]] = []
for folder in CURRENT_DIR.iterdir():
if not folder.is_dir():
continue
# Folder format: [YYYY.MM.DD] Album Name
if not folder.name.startswith("[") or "]" not in folder.name:
continue
year_stuff, album_name = folder.name.split("]", 1)
year_stuff = year_stuff.strip("[]")
year_stuff = year_stuff.replace(".", "-")
album_name = album_name.strip()
album_candidates.append((year_stuff, album_name, folder))
# We want to sort albums by "type" and then by date
# Sorting priority (asc):
# - Umamusume Pretty Derby STARTING GATE
# - ANIMATION DERBY (without Season - a.k.a Season 1)
# - ANIMATION DERBY Season 2
# - ANIMATION DERBY Season 3
# - Umamusume Pretty Derby WINNING LIVE
# - Umamusume Pretty Derby ROAD TO THE TOP
# - Umamusume Pretty Derby BEGINNING OF THE NEW ERA
# When nothing matches, put at the end then sort by date
# first sort by folder name
# it's already "sorted" in this format [YYYY.MM.DD] Album Name
# but Linux would resort them again
album_candidates.sort(key=lambda x: x[2].name)
def get_album_type_priority(album_name: str) -> int:
"""Scoring function for album types"""
if album_name.startswith("Umamusume Pretty Derby STARTING GATE"):
return 0
elif album_name.startswith("ANIMATION DERBY") and "Season" not in album_name:
return 1
elif album_name.startswith("ANIMATION DERBY Season 2"):
return 2
elif album_name.startswith("ANIMATION DERBY Season 3"):
return 3
elif album_name.startswith("Umamusume Pretty Derby WINNING LIVE"):
return 4
elif album_name.startswith("Umamusume Pretty Derby ROAD TO THE TOP"):
return 5
elif album_name.startswith("Umamusume Pretty Derby BEGINNING OF THE NEW ERA"):
return 6
else:
return 999 # Put unmatched albums at the end
def sort_albums(album_tuple):
"""Sort function for albums: first by type priority, then by date."""
date_str, album_name, _ = album_tuple
type_priority = get_album_type_priority(album_name)
return (type_priority, date_str)
def load_file(song_file: Path) -> MP4 | FLAC | None:
if song_file.suffix.lower() == ".m4a":
return MP4(song_file)
elif song_file.suffix.lower() == ".flac":
return FLAC(song_file)
return None
def determine_file_length(song_file: MP4 | FLAC | None) -> float | None:
if isinstance(song_file, MP4):
return song_file.info.length if song_file.info else None
elif isinstance(song_file, FLAC):
return song_file.info.length if song_file.info else None
return None
def make_extinf_title(song_file: MP4 | FLAC | None) -> str | None:
if isinstance(song_file, MP4):
titles: list[str] = song_file["\xa9nam"] # type: ignore
artists: list[str] = song_file["\xa9ART"] # type: ignore
artist_merged = SEP.join(artists)
titles_merged = SEP.join(titles)
return f"{artist_merged} - {titles_merged}"
elif isinstance(song_file, FLAC):
titles: list[str] = song_file["title"]
artists: list[str] = song_file["artist"]
artist_merged = SEP.join(artists)
titles_merged = SEP.join(titles)
return f"{artist_merged} - {titles_merged}"
return None
# sort score by type
sorted_albums = sorted(album_candidates, key=sort_albums)
print(f"Processing {len(album_candidates)} albums...")
m3u8_file = CURRENT_DIR / "Umamusume 🐴.m3u"
# delete the file if it exists
if m3u8_file.exists():
m3u8_file.unlink()
with m3u8_file.open("a", encoding="utf-8") as m3u8_f:
m3u8_file.write_text(M3U8_TEMPLATE, encoding="utf-8")
for date_str, album_name, folder in sorted_albums:
print(f"Processing: [{date_str}] {album_name}")
song_files = list(folder.rglob("*.m4a")) + list(folder.rglob("*.flac"))
# sort by filename (without suffix)
song_files.sort(key=lambda x: x.stem)
for song_file in song_files:
file_loader = load_file(song_file)
assert file_loader is not None, f"Could not load file: {song_file}"
# check how long the media file is for EXTINF
file_length = determine_file_length(file_loader)
assert file_length is not None, f"File length for {song_file} could not be determined."
extinf_title = make_extinf_title(file_loader)
assert extinf_title is not None, f"Could not create EXTINF title for {song_file}"
# round to int
file_length = int(round(file_length, 0))
song_file_cast = song_file.relative_to(CURRENT_DIR).as_posix()
real_path = f"/mnt/tachyon/ongaku/Japanese/Umamusume/{song_file_cast}"
m3u8_f.write(f"#EXTINF:{file_length},{extinf_title}\n")
m3u8_f.write(f"{real_path}\n")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment