Verified Commit 62247139 authored by Jakob Moser's avatar Jakob Moser
Browse files

Add Song

parent 8d33ef6b
Loading
Loading
Loading
Loading

karaokatalog/Song.py

0 → 100644
+88 −0
Changes for karaokatalog/Song.py: 88 added lines, 0 removed lines.
Original line number Diff line number Diff line
from __future__ import annotations

from pathlib import Path
from typing import Self
import filecmp

from dataclasses import dataclass


@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: str
    artist: str
    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)
    )