Compare commits

..

2 Commits

Author SHA1 Message Date
fa89e49f29 Merge pull request 'Полный релиз 1.0.0' (#5) from dev into latest
All checks were successful
Build And Publish Package / publish (push) Successful in 28s
Reviewed-on: #5
2026-04-02 07:59:52 +03:00
5135c40555 Полный релиз 1.0.0
All checks were successful
Verify Dev Build / publish (push) Successful in 49s
2026-04-02 07:57:41 +03:00
3 changed files with 1063 additions and 249 deletions

View File

@ -1,11 +1,11 @@
[project]
name = "oxidespotify"
version = "0.2.2"
version = "1.0.0"
description = "Client for Spotify API"
readme = "README.md"
authors = [{ name = "Miwory", email = "miwory.uwu@gmail.com" }]
requires-python = ">=3.14"
dependencies = ["oxidehttp>=1.0.3,<=2.0.0", "pydantic>=2.12,<=2.13"]
dependencies = ["oxidehttp>=1.4.0,<=2.0.0", "pydantic>=2.12,<=2.13"]
[build-system]
requires = ["uv_build>=0.9.2,<0.11.0"]

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, HttpUrl, field_validator
from pydantic import BaseModel, HttpUrl, RootModel, field_validator
class Error(Exception):
@ -62,6 +62,20 @@ class Image(BaseModel):
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
total: int
class ExternalUrls(BaseModel):
spotify: HttpUrl
@ -72,6 +86,16 @@ class ExternalIDs(BaseModel):
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
@ -81,19 +105,28 @@ class Artist(BaseModel):
uri: str
class Cursor(BaseModel):
after: int
before: int
class Context(BaseModel):
type: Literal['artist', 'playlist', 'album', 'show', 'collection']
href: HttpUrl
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 Album(BaseModel):
class AlbumTracks(Paginated):
items: list[BaseTrack]
class BaseAlbum(BaseModel):
album_type: Literal['album', 'single', 'compilation']
total_tracks: int
external_urls: ExternalUrls
@ -109,103 +142,27 @@ class Album(BaseModel):
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
class Album(BaseAlbum):
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
tracks: AlbumTracks
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 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):
@ -236,17 +193,79 @@ class AudioBook(BaseModel):
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 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: datetime
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):
@ -259,35 +278,94 @@ class PlaybackState(BaseModel):
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):
class Devices(BaseModel):
devices: list[Device]
class RecentlyPlayedTrack(BaseModel):
track: Track
played_at: datetime
context: Context | None = None
context: Context
class RecentlyPlayed(BaseModel):
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
limit: int
next: HttpUrl | None
cursors: Cursor
total: int | None = None
items: list[PlayHistoryObject]
id: str
type: Literal['user']
uri: str
display_name: str | None
class SearchData(BaseModel):
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
limit: int
next: HttpUrl | None
offset: int
previous: int | None
total: int
class SearchAlbumData(SearchData):
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]
@ -295,7 +373,7 @@ class SearchAlbum(BaseModel):
albums: SearchAlbumData
class SearchArtistData(SearchData):
class SearchArtistData(Paginated):
items: list[Artist]
@ -303,7 +381,7 @@ class SearchArtist(BaseModel):
artists: SearchArtistData
class SearchPlaylistData(SearchData):
class SearchPlaylistData(Paginated):
items: list[Playlist | None]
@ -311,7 +389,7 @@ class SearchPlaylist(BaseModel):
playlists: SearchPlaylistData
class SearchTrackData(SearchData):
class SearchTrackData(Paginated):
items: list[Track]
@ -319,7 +397,7 @@ class SearchTrack(BaseModel):
tracks: SearchTrackData
class SearchShowData(SearchData):
class SearchShowData(Paginated):
items: list[Show]
@ -327,7 +405,7 @@ class SearchShow(BaseModel):
shows: SearchShowData
class SearchEpisodeData(SearchData):
class SearchEpisodeData(Paginated):
items: list[Episode]
@ -335,7 +413,7 @@ class SearchEpisode(BaseModel):
episodes: SearchEpisodeData
class SearchAudiobookData(SearchData):
class SearchAudiobookData(Paginated):
items: list[AudioBook]
@ -343,6 +421,28 @@ 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
@ -351,3 +451,19 @@ class UserProfile(BaseModel):
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