from datetime import date, datetime from typing import Annotated, Literal from pydantic import ( BaseModel, BeforeValidator, HttpUrl, RootModel, 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 def normalize_spotify_date(v: str) -> str: parts = v.split('-') if len(parts) == 1: return f'{v}-01-01' if len(parts) == 2: return f'{v}-01' return v PrecisionedReleaseDate = Annotated[ date, BeforeValidator(normalize_spotify_date) ] 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 Cursors(BaseModel): after: int before: int class Paginated(BaseModel): href: HttpUrl limit: int next: HttpUrl | None offset: int = 0 previous: HttpUrl | None = None total: int = 0 class ExternalUrls(BaseModel): spotify: HttpUrl class ExternalIDs(BaseModel): isrc: str | None = None ean: str | None = None upc: str | None = None class Copyright(BaseModel): text: str type: Literal['C', 'P'] class ResumePoint(BaseModel): fully_played: bool resume_position_ms: int class Artist(BaseModel): external_urls: ExternalUrls href: HttpUrl id: str name: str type: Literal['artist'] uri: str class BaseTrack(BaseModel): artists: list[Artist] disc_number: int duration_ms: int explicit: bool 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 AlbumTracks(Paginated): items: list[BaseTrack] class BaseAlbum(BaseModel): album_type: Literal['album', 'single', 'compilation'] total_tracks: int external_urls: ExternalUrls href: HttpUrl id: str images: list[Image] name: str release_date: PrecisionedReleaseDate release_date_precision: Literal['year', 'month', 'day'] restrictions: Restriction | None = None type: Literal['album'] uri: str artists: list[Artist] class Album(BaseAlbum): external_ids: ExternalIDs copyrights: list[Copyright] tracks: AlbumTracks class Track(BaseTrack): album: BaseAlbum class UserSavedAlbum(BaseModel): added_at: datetime album: Album class UserSavedAlbums(Paginated): items: list[UserSavedAlbum] class ArtistsAlbums(Paginated): items: list[Album] 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 AudioBookChapter(BaseModel): chapter_number: int description: str html_description: str duration_ms: int explicit: bool external_urls: ExternalUrls href: HttpUrl id: str images: list[Image] is_playable: bool languages: list[str] name: str release_date: PrecisionedReleaseDate release_date_precision: Literal['year', 'month', 'day'] resume_point: ResumePoint type: Literal['episode'] uri: str restrictions: Restriction class AudiobookChapters(BaseModel): items: list[AudioBookChapter] class UserSavedAudiobooks(Paginated): items: list[AudioBook] class Chapter(AudioBookChapter): audiobook: AudioBook 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(AudioBookChapter): show: Show class UsedSavedEpisode(BaseModel): added_at: datetime episode: Episode class UserSavedEpisodes(Paginated): items: list[UsedSavedEpisode] class CheckUserSavedItems(RootModel[list[bool]]): pass class Context(BaseModel): type: Literal['artist', 'playlist', 'album', 'show', 'collection'] href: HttpUrl external_urls: ExternalUrls uri: str 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'] class Devices(BaseModel): devices: list[Device] class RecentlyPlayedTrack(BaseModel): track: Track played_at: datetime context: Context | None = None class RecentlyPlayedTracks(Paginated): cursors: Cursors items: list[RecentlyPlayedTrack] class UsersQueue(BaseModel): currently_playing: Track | Episode | None queue: list[Track | Episode] class PlaylistOwner(BaseModel): external_urls: ExternalUrls href: HttpUrl id: str type: Literal['user'] uri: str display_name: str | None class PlaylistTrack(BaseModel): added_at: datetime added_by: PlaylistOwner is_local: bool item: Track | Episode class PlaylistTracks(Paginated): href: HttpUrl item: PlaylistTrack class SimplifiedPlaylistTracks(Paginated): href: HttpUrl total: int = 0 class Playlist(BaseModel): type: Literal['playlist'] uri: str 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 | None = None class SimplifiedPlaylist(BaseModel): type: Literal['playlist'] uri: str collaborative: bool external_urls: ExternalUrls href: HttpUrl id: str images: list[Image] name: str owner: PlaylistOwner public: bool snapshot_id: str items: SimplifiedPlaylistTracks class UserPlaylists(Paginated): items: list[Playlist] class PlaylistImages(RootModel[list[Image]]): pass class SearchAlbumData(Paginated): items: list[Album] class SearchAlbum(BaseModel): albums: SearchAlbumData class SearchArtistData(Paginated): items: list[Artist] class SearchArtist(BaseModel): artists: SearchArtistData class SearchPlaylistData(Paginated): items: list[Playlist | None] class SearchPlaylist(BaseModel): playlists: SearchPlaylistData class SearchTrackData(Paginated): items: list[Track] class SearchTrack(BaseModel): tracks: SearchTrackData class SearchShowData(Paginated): items: list[Show] class SearchShow(BaseModel): shows: SearchShowData class SearchEpisodeData(Paginated): items: list[Episode] class SearchEpisode(BaseModel): episodes: SearchEpisodeData class SearchAudiobookData(Paginated): items: list[AudioBook] class SearchAudiobook(BaseModel): audiobooks: SearchAudiobookData class ShowEpisodes(Paginated): items: list[AudioBookChapter] class SavedShow(BaseModel): added_at: datetime show: Show class SavedShows(Paginated): items: list[SavedShow] class SavedTrack(BaseModel): added_at: datetime track: Track class SavedTracks(Paginated): items: list[SavedTrack] class UserProfile(BaseModel): id: str display_name: str external_urls: ExternalUrls href: HttpUrl images: list[Image] type: Literal['user'] uri: str class UsersTopItemsTracks(Paginated): items: list[Track] class UsersTopItemsArtists(Paginated): items: list[Artist] class FollowedArtistsData(Paginated): items: list[Artist] class FollowedArtists(BaseModel): artists: FollowedArtistsData