from datetime import datetime from typing import Literal from pydantic import BaseModel, HttpUrl, field_validator class Error(Exception): status_code: int error: str def __init__(self, status_code: int, error: str) -> None: self.status_code = status_code self.error = error super().__init__(f'{status_code}: {error}') class ClientError(Error): pass class InternalError(Error): pass class Token(BaseModel): token_type: Literal['Bearer'] access_token: str expires_in: int scope: str class UserAccessToken(Token): refresh_token: str class Device(BaseModel): id: str | None is_active: bool is_private_session: bool is_restricted: bool name: str type: str volume_percent: int | None supports_volume: bool class Restriction(BaseModel): reason: Literal['market', 'product', 'explicit'] class Image(BaseModel): url: HttpUrl height: int width: int @field_validator('height', 'width', mode='before') @classmethod def convert_null_to_zero(cls, v: int | None) -> int: if v is None: return 0 return v class ExternalUrls(BaseModel): spotify: HttpUrl class ExternalIDs(BaseModel): isrc: str | None = None ean: str | None = None upc: str | None = None class Artist(BaseModel): external_urls: ExternalUrls href: HttpUrl id: str name: str type: Literal['artist'] uri: str class Cursor(BaseModel): after: int before: int class Context(BaseModel): type: Literal['artist', 'playlist', 'album', 'show', 'collection'] href: HttpUrl external_urls: ExternalUrls uri: str class Album(BaseModel): album_type: Literal['album', 'single', 'compilation'] total_tracks: int external_urls: ExternalUrls href: HttpUrl id: str images: list[Image] name: str release_date: datetime release_date_precision: Literal['year', 'month', 'day'] restrictions: Restriction | None = None type: Literal['album'] uri: str artists: list[Artist] class PlaylistOwner(BaseModel): external_urls: ExternalUrls href: HttpUrl id: str type: Literal['user'] uri: str display_name: str | None class PlaylistTracks(BaseModel): href: HttpUrl total: int class Playlist(BaseModel): collaborative: bool description: str external_urls: ExternalUrls href: HttpUrl id: str images: list[Image] name: str owner: PlaylistOwner public: bool snapshot_id: str items: PlaylistTracks type: Literal['playlist'] uri: str class Track(BaseModel): album: Album artists: list[Artist] disc_number: int duration_ms: int explicit: bool external_ids: ExternalIDs external_urls: ExternalUrls href: HttpUrl id: str is_playable: bool | None = None restrictions: Restriction | None = None name: str track_number: int type: Literal['track'] uri: str is_local: bool class EpisodeResumePoint(BaseModel): fully_played: bool resume_position_ms: int class Copyright(BaseModel): text: str type: Literal['C', 'P'] class Show(BaseModel): copyrights: list[Copyright] description: str html_description: str explicit: bool external_ids: ExternalIDs | None = None href: HttpUrl id: str images: list[Image] is_externally_hosted: bool languages: list[str] media_type: str name: str type: Literal['show'] uri: str total_episodes: int class Episode(BaseModel): description: str html_description: str duration_ms: int explicit: bool external_urls: ExternalUrls href: HttpUrl id: str images: list[Image] is_externally_hosted: bool is_playable: bool languages: list[str] name: str release_date: datetime release_date_precision: Literal['year', 'month', 'day'] resume_point: EpisodeResumePoint type: Literal['episode'] uri: str restrictions: Restriction | None = None show: Show | None = None class AudioBookAuthor(BaseModel): name: str class AudioBookNarator(BaseModel): name: str class AudioBook(BaseModel): authors: list[AudioBookAuthor] copyrights: list[Copyright] description: str html_description: str edition: str explicit: bool external_urls: ExternalUrls href: str id: str images: list[Image] languages: list[str] media_type: str name: str narrators: list[AudioBookNarator] type: Literal['audiobook'] uri: str total_chapters: int class PlaybackStateActions(BaseModel): interrupting_playback: bool | None = None pausing: bool | None = None resuming: bool | None = None seeking: bool | None = None skipping_next: bool | None = None skipping_prev: bool | None = None toggling_repeat_context: bool | None = None toggling_shuffle: bool | None = None toggling_repeat_track: bool | None = None transferring_playback: bool | None = None class PlaybackState(BaseModel): device: Device repeat_state: Literal['off', 'track', 'context'] shuffle_state: bool context: Context | None timestamp: int progress_ms: int is_playing: bool item: Track | Episode | None currently_playing_type: Literal['track', 'episode', 'ad', 'unknown'] # TODO: allows has 'disallows' inside? not sure about that # actions: PlaybackStateActions class PlayHistoryObject(BaseModel): track: Track played_at: datetime context: Context | None = None class RecentlyPlayed(BaseModel): href: HttpUrl limit: int next: HttpUrl | None cursors: Cursor total: int | None = None items: list[PlayHistoryObject] class SearchData(BaseModel): href: HttpUrl limit: int next: HttpUrl | None offset: int previous: int | None total: int class SearchAlbumData(SearchData): items: list[Album] class SearchAlbum(BaseModel): albums: SearchAlbumData class SearchArtistData(SearchData): items: list[Artist] class SearchArtist(BaseModel): artists: SearchArtistData class SearchPlaylistData(SearchData): items: list[Playlist | None] class SearchPlaylist(BaseModel): playlists: SearchPlaylistData class SearchTrackData(SearchData): items: list[Track] class SearchTrack(BaseModel): tracks: SearchTrackData class SearchShowData(SearchData): items: list[Show] class SearchShow(BaseModel): shows: SearchShowData class SearchEpisodeData(SearchData): items: list[Episode] class SearchEpisode(BaseModel): episodes: SearchEpisodeData class SearchAudiobookData(SearchData): items: list[AudioBook] class SearchAudiobook(BaseModel): audiobooks: SearchAudiobookData class UserProfile(BaseModel): id: str display_name: str external_urls: ExternalUrls href: HttpUrl images: list[Image] type: Literal['user'] uri: str