Первый релиз
All checks were successful
Build And Publish Package / publish (push) Successful in 33s

This commit is contained in:
2026-03-08 06:41:33 +03:00
commit 727af5899a
11 changed files with 1233 additions and 0 deletions

353
src/oxidespotify/schema.py Normal file
View File

@ -0,0 +1,353 @@
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