from __future__ import annotations import dataclasses from abc import ABC, abstractmethod from typing import Any, Sequence __all__ = ["PlaylistInfo", "TrackInfo", "YoutubeVideoInfo", "YoutubeVideoInPlaylistInfo", "UserInfo", "TokenInfo"] @dataclasses.dataclass(frozen=True) class _NormalizeInfoMixin(ABC): @abstractmethod def _get_normalized_fields(self) -> Sequence[str]: pass @staticmethod def _normalize(value: str) -> str: return value.lower().strip() def __post_init__(self): for field in dataclasses.fields(self.__class__): if field.name not in self._get_normalized_fields(): continue value = getattr(self, field.name) if isinstance(value, str): value = self._normalize(value) elif isinstance(value, list) and all(isinstance(v, str) for v in value): value = [self._normalize(v) for v in value] else: continue object.__setattr__(self, field.name, value) @dataclasses.dataclass(frozen=True) class _DictableMixin(ABC): def to_dict(self) -> dict[str, Any]: return dataclasses.asdict(self) @dataclasses.dataclass(frozen=True) class PlaylistInfo(_DictableMixin): title: str image_url: str total_tracks: int user_id: str user_name: str | None description: str | None = None @dataclasses.dataclass(frozen=True) class TrackInfo(_DictableMixin, _NormalizeInfoMixin): id: str title: str artists: list[str] isrc: str | None def __hash__(self) -> int: return hash((self.title, tuple(self.artists))) def __eq__(self, other: object) -> bool: if not isinstance(other, TrackInfo): return NotImplemented return self.title == other.title and self.artists == other.artists def _get_normalized_fields(self) -> Sequence[str]: return "title", "artists" @dataclasses.dataclass(frozen=True) class YoutubeVideoInfo(_DictableMixin, _NormalizeInfoMixin): id: str title: str channel_title: str channel_id: str def _get_normalized_fields(self) -> Sequence[str]: return "title", "channel_title" @dataclasses.dataclass(frozen=True) class YoutubeVideoInPlaylistInfo(_DictableMixin): id: str playlist_item_id: str @dataclasses.dataclass(frozen=True) class UserInfo(_DictableMixin): user_identifier: str display_name: str image_url: str | None = None @dataclasses.dataclass(frozen=True) class TokenInfo(_DictableMixin): expires_in: int access_token: str refresh_token: str | None = None