491 lines
8.8 KiB
Python
491 lines
8.8 KiB
Python
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
|