Source code for sublime_music.adapters.subsonic.api_objects

"""
These are the API objects that are returned by Subsonic.
"""

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Union

import dataclasses_json
from dataclasses_json import DataClassJsonMixin, LetterCase, config, dataclass_json
from dateutil import parser

from .. import api_objects as SublimeAPI

# Translation map for encoding/decoding API results. For instance some servers
# may return a string where an integer is required.
decoder_functions = {
    datetime: (lambda s: parser.parse(s) if s else None),
    timedelta: (lambda s: timedelta(seconds=float(s)) if s else None),
    int: (lambda s: int(s) if s else None),
}
encoder_functions = {
    datetime: (lambda d: datetime.strftime(d, "%Y-%m-%dT%H:%M:%S.%f%z") if d else None),
    timedelta: (lambda t: t.total_seconds() if t else None),
}

for type_, translation_function in decoder_functions.items():
    dataclasses_json.cfg.global_config.decoders[type_] = translation_function
    dataclasses_json.cfg.global_config.decoders[
        Optional[type_]  # type: ignore
    ] = translation_function

for type_, translation_function in encoder_functions.items():
    dataclasses_json.cfg.global_config.encoders[type_] = translation_function
    dataclasses_json.cfg.global_config.encoders[
        Optional[type_]  # type: ignore
    ] = translation_function


[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Genre(SublimeAPI.Genre): name: str = field(metadata=config(field_name="value")) song_count: Optional[int] = None album_count: Optional[int] = None
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Album(SublimeAPI.Album): name: str id: Optional[str] cover_art: Optional[str] = None song_count: Optional[int] = None year: Optional[int] = None duration: Optional[timedelta] = None created: Optional[datetime] = None songs: List["Song"] = field(default_factory=list, metadata=config(field_name="song")) play_count: Optional[int] = None starred: Optional[datetime] = None # Artist artist: Optional["ArtistAndArtistInfo"] = field(init=False) _artist: Optional[str] = field(default=None, metadata=config(field_name="artist")) artist_id: Optional[str] = None # Genre genre: Optional[Genre] = field(init=False) _genre: Optional[str] = field(default=None, metadata=config(field_name="genre")) def __post_init__(self): # Initialize the cross-references self.artist = ( None if not self.artist_id or not self._artist else ArtistAndArtistInfo(id=self.artist_id, name=self._artist) ) self.genre = None if not self._genre else Genre(self._genre)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class ArtistAndArtistInfo(SublimeAPI.Artist): name: str id: Optional[str] albums: List[Album] = field(default_factory=list, metadata=config(field_name="album")) album_count: Optional[int] = None cover_art: Optional[str] = None artist_image_url: Optional[str] = None starred: Optional[datetime] = None # Artist Info similar_artists: List["ArtistAndArtistInfo"] = field(default_factory=list) biography: Optional[str] = None music_brainz_id: Optional[str] = None last_fm_url: Optional[str] = None def __post_init__(self): if not self.album_count and len(self.albums) > 0: self.album_count = len(self.albums) if not self.artist_image_url: self.artist_image_url = self.cover_art
[docs] def augment_with_artist_info(self, artist_info: Optional["ArtistInfo"]): if artist_info: self.similar_artists = artist_info.similar_artists self.biography = artist_info.biography self.last_fm_url = artist_info.last_fm_url self.artist_image_url = artist_info.artist_image_url or self.artist_image_url self.music_brainz_id = artist_info.music_brainz_id
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class ArtistInfo: similar_artists: List[ArtistAndArtistInfo] = field( default_factory=list, metadata=config(field_name="similarArtist") ) biography: Optional[str] = None last_fm_url: Optional[str] = None artist_image_url: Optional[str] = field( default=None, metadata=config(field_name="largeImageUrl") ) music_brainz_id: Optional[str] = None def __post_init__(self): if self.artist_image_url: placeholder_image_names = ( "2a96cbd8b46e442fc41c2b86b821562f.png", "-No_image_available.svg.png", ) for n in placeholder_image_names: if self.artist_image_url.endswith(n): self.artist_image_url = "" return
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Directory(SublimeAPI.Directory, DataClassJsonMixin): id: str name: Optional[str] = None title: Optional[str] = None parent_id: Optional[str] = field(default=None, metadata=config(field_name="parent")) children: List[Union["Directory", "Song"]] = field(init=False) _children: List[Dict[str, Any]] = field( default_factory=list, metadata=config(field_name="child") ) def __post_init__(self): if not isinstance(self.id, str): self.id = str(self.id) self.parent_id = (self.parent_id or "root") if self.id != "root" else None self.name = self.name or self.title self.children = [ Directory.from_dict(c) if c.get("isDir") else Song.from_dict(c) for c in self._children ]
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Song(SublimeAPI.Song, DataClassJsonMixin): id: str title: str = field(metadata=config(field_name="name")) path: Optional[str] = None parent_id: Optional[str] = field(default=None, metadata=config(field_name="parent")) duration: Optional[timedelta] = None # Artist artist: Optional[ArtistAndArtistInfo] = field(init=False) _artist: Optional[str] = field(default=None, metadata=config(field_name="artist")) artist_id: Optional[str] = None # Album album: Optional[Album] = field(init=False) _album: Optional[str] = field(default=None, metadata=config(field_name="album")) album_id: Optional[str] = None # Genre genre: Optional[Genre] = field(init=False) _genre: Optional[str] = field(default=None, metadata=config(field_name="genre")) track: Optional[int] = None disc_number: Optional[int] = None year: Optional[int] = None size: Optional[int] = None cover_art: Optional[str] = None user_rating: Optional[int] = None starred: Optional[datetime] = None def __post_init__(self): if not isinstance(self.id, str): self.id = str(self.id) self.parent_id = (self.parent_id or "root") if self.id != "root" else None self.artist = ( None if not self._artist else ArtistAndArtistInfo(id=self.artist_id, name=self._artist) ) self.album = None if not self._album else Album(id=self.album_id, name=self._album) self.genre = None if not self._genre else Genre(self._genre)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Playlist(SublimeAPI.Playlist): id: str name: str songs: List[Song] = field(default_factory=list, metadata=config(field_name="entry")) song_count: Optional[int] = field(default=None) duration: Optional[timedelta] = field(default=None) created: Optional[datetime] = None changed: Optional[datetime] = None comment: Optional[str] = None owner: Optional[str] = None public: Optional[bool] = None cover_art: Optional[str] = None def __post_init__(self): if not isinstance(self.id, str): self.id = str(self.id) if self.songs is None: return if self.song_count is None: self.song_count = len(self.songs) if self.duration is None: self.duration = timedelta( seconds=sum(s.duration.total_seconds() if s.duration else 0 for s in self.songs) )
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class PlayQueue(SublimeAPI.PlayQueue): songs: List[Song] = field(default_factory=list, metadata=config(field_name="entry")) position: timedelta = timedelta(0) username: Optional[str] = None changed: Optional[datetime] = None changed_by: Optional[str] = None value: Optional[str] = None current: Optional[str] = None current_index: Optional[int] = None def __post_init__(self): if pos := self.position: # The position for this endpoint is in milliseconds instead of seconds # because the Subsonic API is sometime stupid. self.position = pos / 1000 if cur := self.current: self.current_index = [s.id for s in self.songs].index(str(cur))
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Index: name: str artist: List[Dict[str, Any]] = field(default_factory=list)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class IndexID3: name: str artist: List[ArtistAndArtistInfo] = field(default_factory=list)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class ArtistsID3: ignored_articles: Optional[str] = None index: List[IndexID3] = field(default_factory=list)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class AlbumList2: album: List[Album] = field(default_factory=list)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Genres: genre: List[Genre] = field(default_factory=list)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Indexes: ignored_articles: Optional[str] = None index: List[Index] = field(default_factory=list)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Playlists: playlist: List[Playlist] = field(default_factory=list)
[docs]@dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class SearchResult3: artist: List[ArtistAndArtistInfo] = field(default_factory=list) album: List[Album] = field(default_factory=list) song: List[Song] = field(default_factory=list)
[docs]@dataclass class Response(DataClassJsonMixin): """The base Subsonic response object.""" artists: Optional[ArtistsID3] = None artist: Optional[ArtistAndArtistInfo] = None artist_info: Optional[ArtistInfo] = field( default=None, metadata=config(field_name="artistInfo2") ) albums: Optional[AlbumList2] = field(default=None, metadata=config(field_name="albumList2")) album: Optional[Album] = None directory: Optional[Directory] = None genres: Optional[Genres] = None indexes: Optional[Indexes] = None playlist: Optional[Playlist] = None playlists: Optional[Playlists] = None play_queue: Optional[PlayQueue] = field(default=None, metadata=config(field_name="playQueue")) song: Optional[Song] = None search_result: Optional[SearchResult3] = field( default=None, metadata=config(field_name="searchResult3") )