91 lines
2.5 KiB
Python

from __future__ import annotations
import filecmp
from dataclasses import dataclass
from pathlib import Path
from typing import Self
type Title = str
type Artist = str
@dataclass(frozen=True)
class Song:
"""
A song, represented by a txt file and accompanying files (most importantly, an audio file).
See https://usdx.eu/format/ for the specification.
"""
title: Title
artist: Artist
audio: Path | None
video: Path | None
cover: Path | None
song_txt: Path
@property
def dir(self) -> Path:
return self.song_txt.parent
def has_identic_files(self, other: Song) -> bool:
"""
Return if the directory for this song and the directory for the other song contain identic files.
"""
comparison = filecmp.dircmp(self.dir, other.dir, shallow=False)
# Two directories are identic if they don't have any differing files, and if there were also no
# errors during comparison.
#
# See also:
# - https://docs.python.org/3/library/filecmp.html#filecmp.dircmp.diff_files
return not comparison.diff_files and not comparison.funny_files
@classmethod
def from_song_txt(cls, song_txt: Path) -> Self | None:
with song_txt.open(encoding="utf-8", errors="ignore") as f:
tags = dict(_parse_tag_line(line) for line in f if line.startswith("#"))
title = tags.get("TITLE")
artist = tags.get("ARTIST")
audio_name = tags.get("AUDIO", tags.get("MP3"))
video_name = tags.get("VIDEO")
cover_name = tags.get("COVER")
if not title or not artist:
# Both are mandatory according to the specification
return None
return cls(
title=title,
artist=artist,
audio=song_txt / audio_name if audio_name else None,
video=song_txt / video_name if video_name else None,
cover=song_txt / cover_name if cover_name else None,
song_txt=song_txt,
)
def _parse_tag_line(tag_line: str) -> tuple[str, str | None]:
"""
Parse a tag line of the format:
#KEY:Value
or
#KEY:
Returns a tuple of (key, value), where the value might be None.
"""
key_and_potentially_value = tuple(
tag_line.strip().removeprefix("#").split(":", maxsplit=1)
)
return (
key_and_potentially_value
if len(key_and_potentially_value) == 2
else (key_and_potentially_value[0], None)
)