91 lines
2.5 KiB
Python
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)
|
|
)
|