From aeb88cd295c667a2eed1b49bbdc2be862da30b35 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:29:38 +0100 Subject: [PATCH 01/16] rewrote some schema classes trying to make it more consistent with api docs. still wip --- .../providers/audiobookshelf/abs_schema.py | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) diff --git a/music_assistant/providers/audiobookshelf/abs_schema.py b/music_assistant/providers/audiobookshelf/abs_schema.py index 687ed05ed..78281f2e4 100644 --- a/music_assistant/providers/audiobookshelf/abs_schema.py +++ b/music_assistant/providers/audiobookshelf/abs_schema.py @@ -434,3 +434,244 @@ class ABSSessionsResponse(BaseModel): num_pages: Annotated[int, Alias("numPages")] items_per_page: Annotated[int, Alias("itemsPerPage")] sessions: list[ABSPlaybackSession] + + +## REWORK +@dataclass +class ABSPodcastMetaDataNormal(BaseModel): + """ABSPodcastMetaDataNormal.""" + + title: str | None + author: str | None + description: str | None + release_date: Annotated[str | None, Alias("releaseDate")] + genres: list[str] | None + feed_url: Annotated[str | None, Alias("feedUrl")] + image_url: Annotated[str | None, Alias("imageUrl")] + itunes_page_url: Annotated[str | None, Alias("itunesPageUrl")] + itunes_id: Annotated[int | None, Alias("itunesId")] + itunes_artist_id: Annotated[int | None, Alias("itunesArtistId")] + explicit: bool + language: str | None + type_: Annotated[str | None, Alias("type")] + + +@dataclass +class ABSPodcastMetaDataMinified(ABSPodcastMetaDataNormal): + """ABSPodcastMetaDataMinified.""" + + title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")] + + +ABSPodcastMetaDataExpanded = ABSPodcastMetaDataMinified + + +@dataclass +class ABSPodcastEpisodeNormal(BaseModel): + """ABSPodcastEpisodeNormal.""" + + library_item_id: Annotated[str, Alias("libraryItemId")] + id_: Annotated[str, Alias("id")] + index: int | None + # audio_file: # not needed for mass application + published_at: Annotated[int | None, Alias("publishedAt")] # ms posix epoch + added_at: Annotated[int | None, Alias("addedAt")] # ms posix epoch + updated_at: Annotated[int | None, Alias("updatedAt")] # ms posix epoch + season: str = "" + episode: str = "" + episode_type: Annotated[str, Alias("episodeType")] = "" + title: str = "" + subtitle: str = "" + description: str = "" + enclosure: str = "" + pub_date: Annotated[str, Alias("pubDate")] = "" + guid: str = "" + # chapters + + +@dataclass +class ABSPodcastBase(BaseModel): + """ABSPodcastNormal.""" + + cover_path: Annotated[str, Alias("coverPath")] + + +@dataclass +class ABSPodcastNormal(BaseModel): + """ABSPodcastNormal.""" + + metadata: ABSPodcastMetaDataNormal + library_item_id: Annotated[str, Alias("libraryItemId")] + tags: list[str] + episodes: list[ABSPodcastEpisodeNormal] + + +@dataclass +class ABSPodcastMinified(ABSPodcastBase): + """ABSPodcastMinified.""" + + metadata: ABSPodcastMetaDataMinified + size: int # bytes + num_episodes: Annotated[int, Alias("numEpisodes")] = 0 + + +@dataclass +class ABSPodcastExpanded(ABSPodcastBase): + """ABSPodcastEpisodeExpanded.""" + + size: int # bytes + metadata: ABSPodcastMetaDataExpanded + episodes: list[ABSPodcastEpisodeExpanded] + + +@dataclass +class ABSAudioBookMetaDataBase(BaseModel): + """ABSAudioBookMetaDataMinified.""" + + title: str + subtitle: str + genres: list[str] | None + published_year: Annotated[str | None, Alias("publishedYear")] + published_date: Annotated[str | None, Alias("publishedDate")] + publisher: str | None + description: str | None + isbn: str | None + asin: str | None + language: str | None + explicit: bool + + +@dataclass +class ABSAudioBookMetaDataNormal(ABSAudioBookMetaDataBase): + """ABSAudioBookMetaDataNormal.""" + + authors: list[ABSAuthorMinified] + narrators: list[str] + series: list[ABSSeriesSequence] + + +@dataclass +class ABSAudioBookMetaDataMinified(ABSAudioBookMetaDataBase): + """ABSAudioBookMetaDataMinified.""" + + # these are normally there + title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")] + author_name: Annotated[str, Alias("authorName")] + author_name_lf: Annotated[str, Alias("authorNameLF")] + narrator_name: Annotated[str, Alias("narratorName")] + series_name: Annotated[str, Alias("seriesName")] + + +@dataclass +class ABSAudioBookMetaDataExpanded(ABSAudioBookMetaDataNormal, ABSAudioBookMetaDataMinified): + """ABSAudioBookMetaDataExpanded.""" + + +@dataclass +class ABSAudioBookBase(BaseModel): + """ABSAudioBookBase.""" + + tags: list[str] + cover_path: Annotated[str | None, Alias("coverPath")] + + +@dataclass +class ABSAudioBookNormal(ABSAudioBookBase): + """ABSAudioBookNormal.""" + + library_item_id: Annotated[str, Alias("libraryItemId")] + metadata: ABSAudioBookMetaDataNormal + # audioFiles + chapters: list[ABSAudioBookChapter] + # ebookFile + + +@dataclass +class ABSAudioBookMinified(ABSAudioBookBase): + """ABSAudioBookBase.""" + + metadata: ABSAudioBookMetaDataMinified + num_tracks: Annotated[int, Alias("numTracks")] + num_audiofiles: Annotated[int, Alias("numAudioFiles")] + num_chapters: Annotated[int, Alias("numChapters")] + duration: float # in s + size: int # in bytes + # ebookFormat + + +@dataclass +class ABSAudioBookExpanded(ABSAudioBookBase): + """ABSAudioBookExpanded.""" + + library_item_id: Annotated[str, Alias("libraryItemId")] + metadata: ABSAudioBookMetaDataExpanded + chapters: list[ABSAudioBookChapter] + duration: float + size: int # bytes + tracks: list[ABSAudioTrack] + + +@dataclass +class ABSLibraryItemBase(BaseModel): + """ABSLibraryItemBase.""" + + id_: Annotated[str, Alias("id")] + ino: str + library_id: Annotated[str, Alias("libraryId")] + folder_id: Annotated[str, Alias("folderId")] + path: str + relative_path: Annotated[str, Alias("relPath")] + is_file: Annotated[bool, Alias("isFile")] + last_modified_ms: Annotated[int, Alias("mtimeMs")] # epoch + last_changed_ms: Annotated[int, Alias("ctimeMs")] # epoch + birthtime_ms: Annotated[int, Alias("birthtimeMs")] # epoch + added_at: Annotated[int, Alias("addedAt")] # ms epoch + updated_at: Annotated[int, Alias("updatedAt")] # ms epoch + is_missing: Annotated[bool, Alias("isMissing")] + is_invalid: Annotated[bool, Alias("isInvalid")] + media_type: Annotated[str, Alias("mediaType")] + + +@dataclass +class ABSLibraryItemNormal(ABSLibraryItemBase): + """ABSLibraryItemNormal.""" + + last_scan: Annotated[int | None, Alias("lastScan")] # ms epoch + scan_version: Annotated[str | None, Alias("scanVersion")] + # libraryFiles + + +@dataclass +class ABSLibraryItemNormalBook(ABSLibraryItemNormal): + """ABSLibraryItemNormalBook.""" + + media: ABSAudioBookNormal + + +@dataclass +class ABSLibraryItemMinified(ABSLibraryItemBase): + """ABSLibraryItemMinified.""" + + num_files: Annotated[int, Alias("numFiles")] + size: int # bytes + + +@dataclass +class ABSLibraryItemMinifiedBook(ABSLibraryItemMinified): + """ABSLibraryItemMinifiedBook.""" + + media: ABSAudioBookMinified + + +@dataclass +class ABSLibraryItemExpanded(ABSLibraryItemBase): + """ABSLibraryItemExpanded.""" + + size: int # bytes + + +@dataclass +class ABSLibraryItemExpandedBook(ABSLibraryItemExpanded): + """ABSLibraryItemExpanded.""" + + media: ABSAudioBookExpanded From 604b608584fadc40da0e04c26968bf50e3d9c015 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:34:42 +0100 Subject: [PATCH 02/16] works with new names --- .../providers/audiobookshelf/__init__.py | 18 +- .../providers/audiobookshelf/abs_client.py | 42 ++-- .../providers/audiobookshelf/abs_schema.py | 209 ++++++------------ 3 files changed, 110 insertions(+), 159 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index eae17c609..353a8fe6c 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -36,11 +36,11 @@ from music_assistant.models.music_provider import MusicProvider from music_assistant.providers.audiobookshelf.abs_client import ABSClient from music_assistant.providers.audiobookshelf.abs_schema import ( - ABSAudioBook, ABSDeviceInfo, ABSLibrary, + ABSLibraryItemExpandedBook, + ABSLibraryItemExpandedPodcast, ABSPlaybackSessionExpanded, - ABSPodcast, ABSPodcastEpisodeExpanded, ) @@ -174,7 +174,7 @@ async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: await self._client.sync() await super().sync_library(media_types=media_types) - def _parse_podcast(self, abs_podcast: ABSPodcast) -> Podcast: + def _parse_podcast(self, abs_podcast: ABSLibraryItemExpandedPodcast) -> Podcast: """Translate ABSPodcast to MassPodcast.""" title = abs_podcast.media.metadata.title # Per API doc title may be None. @@ -185,7 +185,7 @@ def _parse_podcast(self, abs_podcast: ABSPodcast) -> Podcast: name=title, publisher=abs_podcast.media.metadata.author, provider=self.lookup_key, - total_episodes=abs_podcast.media.num_episodes, + total_episodes=len(abs_podcast.media.episodes), provider_mappings={ ProviderMapping( item_id=abs_podcast.id_, @@ -309,7 +309,7 @@ async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode: episode_cnt += 1 raise MediaNotFoundError("Episode not found") - async def _parse_audiobook(self, abs_audiobook: ABSAudioBook) -> Audiobook: + async def _parse_audiobook(self, abs_audiobook: ABSLibraryItemExpandedBook) -> Audiobook: mass_audiobook = Audiobook( item_id=abs_audiobook.id_, provider=self.lookup_key, @@ -431,7 +431,9 @@ async def get_stream_details( return await self._get_stream_details_audiobook(abs_audiobook) raise MediaNotFoundError("Stream unknown") - async def _get_stream_details_audiobook(self, abs_audiobook: ABSAudioBook) -> StreamDetails: + async def _get_stream_details_audiobook( + self, abs_audiobook: ABSLibraryItemExpandedBook + ) -> StreamDetails: """Only single audio file in audiobook.""" self.logger.debug( f"Using direct playback for audiobook {abs_audiobook.media.metadata.title}" @@ -554,7 +556,9 @@ async def _browse_lib( if library is None: raise MediaNotFoundError("Lib missing.") - def get_item_mapping(item: ABSAudioBook | ABSPodcast) -> ItemMapping: + def get_item_mapping( + item: ABSLibraryItemExpandedBook | ABSLibraryItemExpandedPodcast, + ) -> ItemMapping: title = item.media.metadata.title if title is None: title = "UNKNOWN" diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index b4fa79ca5..6d0dd56d9 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -12,18 +12,20 @@ from music_assistant_models.media_items import UniqueList from music_assistant.providers.audiobookshelf.abs_schema import ( - ABSAudioBook, ABSDeviceInfo, - ABSLibrariesItemsResponse, + ABSLibrariesItemsMinifiedBookResponse, + ABSLibrariesItemsMinifiedPodcastResponse, ABSLibrariesResponse, ABSLibrary, - ABSLibraryItem, + ABSLibraryItemExpandedBook, + ABSLibraryItemExpandedPodcast, + ABSLibraryItemMinifiedBook, + ABSLibraryItemMinifiedPodcast, ABSLoginResponse, ABSMediaProgress, ABSPlaybackSession, ABSPlaybackSessionExpanded, ABSPlayRequest, - ABSPodcast, ABSSessionsResponse, ABSSessionUpdate, ABSUser, @@ -163,7 +165,7 @@ async def sync(self) -> None: self.podcast_libraries.append(library) self.user = await self.get_authenticated_user() - async def get_all_podcasts(self) -> AsyncGenerator[ABSPodcast]: + async def get_all_podcasts(self) -> AsyncGenerator[ABSLibraryItemExpandedPodcast]: """Get all available podcasts.""" for library in self.podcast_libraries: async for podcast in self.get_all_podcasts_by_library(library): @@ -180,14 +182,18 @@ async def _get_lib_items(self, lib: ABSLibrary) -> AsyncGenerator[bytes]: page_cnt += 1 yield data - async def get_all_podcasts_by_library(self, lib: ABSLibrary) -> AsyncGenerator[ABSPodcast]: + async def get_all_podcasts_by_library( + self, lib: ABSLibrary + ) -> AsyncGenerator[ABSLibraryItemExpandedPodcast]: """Get all podcasts in a library.""" async for podcast_data in self._get_lib_items(lib): - podcast_list = ABSLibrariesItemsResponse.from_json(podcast_data).results + podcast_list = ABSLibrariesItemsMinifiedPodcastResponse.from_json(podcast_data).results if not podcast_list: # [] if page exceeds return - async def _get_id(plist: list[ABSLibraryItem] = podcast_list) -> AsyncGenerator[str]: + async def _get_id( + plist: list[ABSLibraryItemMinifiedPodcast] = podcast_list, + ) -> AsyncGenerator[str]: for entry in plist: yield entry.id_ @@ -195,11 +201,11 @@ async def _get_id(plist: list[ABSLibraryItem] = podcast_list) -> AsyncGenerator[ podcast = await self.get_podcast(id_) yield podcast - async def get_podcast(self, id_: str) -> ABSPodcast: + async def get_podcast(self, id_: str) -> ABSLibraryItemExpandedPodcast: """Get a single Podcast by ID.""" # this endpoint gives more podcast extra data data = await self._get(f"items/{id_}?expanded=1") - return ABSPodcast.from_json(data) + return ABSLibraryItemExpandedPodcast.from_json(data) async def _get_progress_ms( self, @@ -288,20 +294,24 @@ async def update_audiobook_progress( endpoint = f"me/progress/{audiobook_id}" await self._update_progress(endpoint, progress_s, duration_s, is_finished) - async def get_all_audiobooks(self) -> AsyncGenerator[ABSAudioBook]: + async def get_all_audiobooks(self) -> AsyncGenerator[ABSLibraryItemExpandedBook]: """Get all audiobooks.""" for library in self.audiobook_libraries: async for book in self.get_all_audiobooks_by_library(library): yield book - async def get_all_audiobooks_by_library(self, lib: ABSLibrary) -> AsyncGenerator[ABSAudioBook]: + async def get_all_audiobooks_by_library( + self, lib: ABSLibrary + ) -> AsyncGenerator[ABSLibraryItemExpandedBook]: """Get all Audiobooks in a library.""" async for audiobook_data in self._get_lib_items(lib): - audiobook_list = ABSLibrariesItemsResponse.from_json(audiobook_data).results + audiobook_list = ABSLibrariesItemsMinifiedBookResponse.from_json(audiobook_data).results if not audiobook_list: # [] if page exceeds return - async def _get_id(alist: list[ABSLibraryItem] = audiobook_list) -> AsyncGenerator[str]: + async def _get_id( + alist: list[ABSLibraryItemMinifiedBook] = audiobook_list, + ) -> AsyncGenerator[str]: for entry in alist: yield entry.id_ @@ -309,11 +319,11 @@ async def _get_id(alist: list[ABSLibraryItem] = audiobook_list) -> AsyncGenerato audiobook = await self.get_audiobook(id_) yield audiobook - async def get_audiobook(self, id_: str) -> ABSAudioBook: + async def get_audiobook(self, id_: str) -> ABSLibraryItemExpandedBook: """Get a single Audiobook by ID.""" # this endpoint gives more audiobook extra data audiobook = await self._get(f"items/{id_}?expanded=1") - return ABSAudioBook.from_json(audiobook) + return ABSLibraryItemExpandedBook.from_json(audiobook) async def get_playback_session_podcast( self, device_info: ABSDeviceInfo, podcast_id: str, episode_id: str diff --git a/music_assistant/providers/audiobookshelf/abs_schema.py b/music_assistant/providers/audiobookshelf/abs_schema.py index 78281f2e4..5945dccb8 100644 --- a/music_assistant/providers/audiobookshelf/abs_schema.py +++ b/music_assistant/providers/audiobookshelf/abs_schema.py @@ -42,76 +42,6 @@ class ABSAudioTrack(BaseModel): # metadata: # not needed for mass application -@dataclass -class ABSPodcastEpisodeExpanded(BaseModel): - """ABSPodcastEpisode. - - https://api.audiobookshelf.org/#podcast-episode - """ - - library_item_id: Annotated[str, Alias("libraryItemId")] - id_: Annotated[str, Alias("id")] - index: int | None - # audio_file: # not needed for mass application - published_at: Annotated[int | None, Alias("publishedAt")] # ms posix epoch - added_at: Annotated[int | None, Alias("addedAt")] # ms posix epoch - updated_at: Annotated[int | None, Alias("updatedAt")] # ms posix epoch - audio_track: Annotated[ABSAudioTrack, Alias("audioTrack")] - size: int # in bytes - season: str = "" - episode: str = "" - episode_type: Annotated[str, Alias("episodeType")] = "" - title: str = "" - subtitle: str = "" - description: str = "" - enclosure: str = "" - pub_date: Annotated[str, Alias("pubDate")] = "" - guid: str = "" - # chapters - duration: float = 0.0 - - -@dataclass -class ABSPodcastMetaData(BaseModel): - """PodcastMetaData https://api.audiobookshelf.org/?shell#podcasts.""" - - title: str | None - author: str | None - description: str | None - release_date: Annotated[str | None, Alias("releaseDate")] - genres: list[str] | None - feed_url: Annotated[str | None, Alias("feedUrl")] - image_url: Annotated[str | None, Alias("imageUrl")] - itunes_page_url: Annotated[str | None, Alias("itunesPageUrl")] - itunes_id: Annotated[int | None, Alias("itunesId")] - itunes_artist_id: Annotated[int | None, Alias("itunesArtistId")] - explicit: bool - language: str | None - type_: Annotated[str | None, Alias("type")] - - -@dataclass -class ABSPodcastMedia(BaseModel): - """ABSPodcastMedia.""" - - metadata: ABSPodcastMetaData - cover_path: Annotated[str, Alias("coverPath")] - episodes: list[ABSPodcastEpisodeExpanded] - num_episodes: Annotated[int, Alias("numEpisodes")] = 0 - - -@dataclass -class ABSPodcast(BaseModel): - """ABSPodcast. - - Depending on endpoint we get different results. This class does not - fully reflect https://api.audiobookshelf.org/#podcast. - """ - - id_: Annotated[str, Alias("id")] - media: ABSPodcastMedia - - @dataclass class ABSAuthorMinified(BaseModel): """ABSAuthor. @@ -135,29 +65,6 @@ class ABSSeriesSequence(BaseModel): sequence: str | None -@dataclass -class ABSAudioBookMetaData(BaseModel): - """ABSAudioBookMetaData. - - https://api.audiobookshelf.org/#book-metadata - """ - - title: str - subtitle: str - authors: list[ABSAuthorMinified] - narrators: list[str] - series: list[ABSSeriesSequence] - genres: list[str] | None - published_year: Annotated[str | None, Alias("publishedYear")] - published_date: Annotated[str | None, Alias("publishedDate")] - publisher: str | None - description: str | None - isbn: str | None - asin: str | None - language: str | None - explicit: bool - - @dataclass class ABSAudioBookChapter(BaseModel): """ @@ -172,32 +79,6 @@ class ABSAudioBookChapter(BaseModel): title: str -@dataclass -class ABSAudioBookMedia(BaseModel): - """ABSAudioBookMedia. - - Helper class due to API endpoint used. - """ - - metadata: ABSAudioBookMetaData - cover_path: Annotated[str, Alias("coverPath")] - chapters: list[ABSAudioBookChapter] - duration: float - tracks: list[ABSAudioTrack] - - -@dataclass -class ABSAudioBook(BaseModel): - """ABSAudioBook. - - Depending on endpoint we get different results. This class does not - full reflect https://api.audiobookshelf.org/#book. - """ - - id_: Annotated[str, Alias("id")] - media: ABSAudioBookMedia - - @dataclass class ABSMediaProgress(BaseModel): """ABSMediaProgress. @@ -305,23 +186,6 @@ class ABSLibrariesResponse(BaseModel): libraries: list[ABSLibrary] -@dataclass -class ABSLibraryItem(BaseModel): - """ABSLibraryItem.""" - - id_: Annotated[str, Alias("id")] - - -@dataclass -class ABSLibrariesItemsResponse(BaseModel): - """ABSLibrariesItemsResponse. - - https://api.audiobookshelf.org/#get-a-library-39-s-items - """ - - results: list[ABSLibraryItem] - - # Schema to enable sessions: @dataclass class ABSDeviceInfo(BaseModel): @@ -489,6 +353,35 @@ class ABSPodcastEpisodeNormal(BaseModel): # chapters +@dataclass +class ABSPodcastEpisodeExpanded(BaseModel): + """ABSPodcastEpisode. + + https://api.audiobookshelf.org/#podcast-episode + """ + + library_item_id: Annotated[str, Alias("libraryItemId")] + id_: Annotated[str, Alias("id")] + index: int | None + # audio_file: # not needed for mass application + published_at: Annotated[int | None, Alias("publishedAt")] # ms posix epoch + added_at: Annotated[int | None, Alias("addedAt")] # ms posix epoch + updated_at: Annotated[int | None, Alias("updatedAt")] # ms posix epoch + audio_track: Annotated[ABSAudioTrack, Alias("audioTrack")] + size: int # in bytes + season: str = "" + episode: str = "" + episode_type: Annotated[str, Alias("episodeType")] = "" + title: str = "" + subtitle: str = "" + description: str = "" + enclosure: str = "" + pub_date: Annotated[str, Alias("pubDate")] = "" + guid: str = "" + # chapters + duration: float = 0.0 + + @dataclass class ABSPodcastBase(BaseModel): """ABSPodcastNormal.""" @@ -648,6 +541,13 @@ class ABSLibraryItemNormalBook(ABSLibraryItemNormal): media: ABSAudioBookNormal +@dataclass +class ABSLibraryItemNormalPodcast(ABSLibraryItemNormal): + """ABSLibraryItemNormalBook.""" + + media: ABSPodcastNormal + + @dataclass class ABSLibraryItemMinified(ABSLibraryItemBase): """ABSLibraryItemMinified.""" @@ -663,6 +563,13 @@ class ABSLibraryItemMinifiedBook(ABSLibraryItemMinified): media: ABSAudioBookMinified +@dataclass +class ABSLibraryItemMinifiedPodcast(ABSLibraryItemMinified): + """ABSLibraryItemMinifiedBook.""" + + media: ABSPodcastMinified + + @dataclass class ABSLibraryItemExpanded(ABSLibraryItemBase): """ABSLibraryItemExpanded.""" @@ -675,3 +582,33 @@ class ABSLibraryItemExpandedBook(ABSLibraryItemExpanded): """ABSLibraryItemExpanded.""" media: ABSAudioBookExpanded + + +@dataclass +class ABSLibraryItemExpandedPodcast(ABSLibraryItemExpanded): + """ABSLibraryItemExpanded.""" + + media: ABSPodcastExpanded + + +@dataclass +class ABSLibrariesItemsMinifiedBookResponse(BaseModel): + """ABSLibrariesItemsResponse. + + https://api.audiobookshelf.org/#get-a-library-39-s-items + No matter what options I append to the request, I always end up with + minified items. Maybe a bug in abs. If that would be fixed, there is + potential for reduced in API calls. + """ + + results: list[ABSLibraryItemMinifiedBook] + + +@dataclass +class ABSLibrariesItemsMinifiedPodcastResponse(BaseModel): + """ABSLibrariesItemsResponse. + + see above. + """ + + results: list[ABSLibraryItemMinifiedPodcast] From e7a1abd4f0210a3ef0e90f0ed86278a45a09833f Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:08:18 +0100 Subject: [PATCH 03/16] reorganize abs schema The naming of the classes is now the same as in the Audiobookshelf API doc. --- .../providers/audiobookshelf/abs_schema.py | 517 +++++++++++------- 1 file changed, 326 insertions(+), 191 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/abs_schema.py b/music_assistant/providers/audiobookshelf/abs_schema.py index 5945dccb8..91c71ede4 100644 --- a/music_assistant/providers/audiobookshelf/abs_schema.py +++ b/music_assistant/providers/audiobookshelf/abs_schema.py @@ -1,6 +1,15 @@ -"""Schema definition of Audiobookshelf. +"""Schema definition of Audiobookshelf (ABS). https://api.audiobookshelf.org/ + +Some schema definitions have variants. Take book as example: +https://api.audiobookshelf.org/#book +Naming Scheme in this file: + - the standard definition has nothing added + - minified/ expanded: here, 2 additional variants + +Sometimes these variants remove or change attributes in such a way, that +it makes sense to define a base class for inheritance. """ from dataclasses import dataclass, field @@ -28,7 +37,7 @@ class Config(BaseConfig): @dataclass class ABSAudioTrack(BaseModel): - """ABS audioTrack. + """ABS audioTrack. No variants. https://api.audiobookshelf.org/#audio-track """ @@ -43,32 +52,9 @@ class ABSAudioTrack(BaseModel): @dataclass -class ABSAuthorMinified(BaseModel): - """ABSAuthor. - - https://api.audiobookshelf.org/#author +class ABSBookChapter(BaseModel): """ - - id_: Annotated[str, Alias("id")] - name: str - - -@dataclass -class ABSSeriesSequence(BaseModel): - """Series Sequence. - - https://api.audiobookshelf.org/#series - """ - - id_: Annotated[str, Alias("id")] - name: str - sequence: str | None - - -@dataclass -class ABSAudioBookChapter(BaseModel): - """ - ABSAudioBookChapter. + ABSBookChapter. No variants. https://api.audiobookshelf.org/#book-chapter """ @@ -80,29 +66,12 @@ class ABSAudioBookChapter(BaseModel): @dataclass -class ABSMediaProgress(BaseModel): - """ABSMediaProgress. +class ABSAudioBookmark(BaseModel): + """ABSAudioBookmark. No variants. - https://api.audiobookshelf.org/#media-progress + https://api.audiobookshelf.org/#audio-bookmark """ - id_: Annotated[str, Alias("id")] - library_item_id: Annotated[str, Alias("libraryItemId")] - episode_id: Annotated[str, Alias("episodeId")] - duration: float # seconds - progress: float # percent 0->1 - current_time: Annotated[float, Alias("currentTime")] # seconds - is_finished: Annotated[bool, Alias("isFinished")] - hide_from_continue_listening: Annotated[bool, Alias("hideFromContinueListening")] - last_update: Annotated[int, Alias("lastUpdate")] # ms epoch - started_at: Annotated[int, Alias("startedAt")] # ms epoch - finished_at: Annotated[int | None, Alias("finishedAt")] # ms epoch - - -@dataclass -class ABSAudioBookmark(BaseModel): - """ABSAudioBookmark.""" - library_item_id: Annotated[str, Alias("libraryItemId")] title: str time: float # seconds @@ -111,7 +80,10 @@ class ABSAudioBookmark(BaseModel): @dataclass class ABSUserPermissions(BaseModel): - """ABSUserPermissions.""" + """ABSUserPermissions. No variants. + + https://api.audiobookshelf.org/#user-permissions + """ download: bool update: bool @@ -122,48 +94,11 @@ class ABSUserPermissions(BaseModel): access_explicit_content: Annotated[bool, Alias("accessExplicitContent")] -@dataclass -class ABSUser(BaseModel): - """ABSUser. - - only attributes we need for mass - https://api.audiobookshelf.org/#user - """ - - id_: Annotated[str, Alias("id")] - username: str - type_: Annotated[str, Alias("type")] - token: str - media_progress: Annotated[list[ABSMediaProgress], Alias("mediaProgress")] - series_hide_from_continue_listening: Annotated[ - list[str], Alias("seriesHideFromContinueListening") - ] - bookmarks: list[ABSAudioBookmark] - is_active: Annotated[bool, Alias("isActive")] - is_locked: Annotated[bool, Alias("isLocked")] - last_seen: Annotated[int | None, Alias("lastSeen")] - created_at: Annotated[int, Alias("createdAt")] - permissions: ABSUserPermissions - libraries_accessible: Annotated[list[str], Alias("librariesAccessible")] - - # this seems to be missing - # item_tags_accessible: Annotated[list[str], Alias("itemTagsAccessible")] - - -@dataclass -class ABSLoginResponse(BaseModel): - """ABSLoginResponse.""" - - user: ABSUser - - # this seems to be missing - # user_default_library_id: Annotated[str, Alias("defaultLibraryId")] - - @dataclass class ABSLibrary(BaseModel): - """ABSLibrary. + """ABSLibrary. No variants. + https://api.audiobookshelf.org/#library Only attributes we need """ @@ -179,17 +114,9 @@ class ABSLibrary(BaseModel): last_update: Annotated[int, Alias("lastUpdate")] -@dataclass -class ABSLibrariesResponse(BaseModel): - """ABSLibrariesResponse.""" - - libraries: list[ABSLibrary] - - -# Schema to enable sessions: @dataclass class ABSDeviceInfo(BaseModel): - """ABSDeviceInfo. + """ABSDeviceInfo. No variants. https://api.audiobookshelf.org/#device-info-parameters https://api.audiobookshelf.org/#device-info @@ -204,20 +131,134 @@ class ABSDeviceInfo(BaseModel): # sdkVersion # meant for an Android client +### Author: https://api.audiobookshelf.org/#author + + @dataclass -class ABSPlayRequest(BaseModel): - """ABSPlayRequest. +class ABSAuthorMinified(BaseModel): + """ABSAuthorMinified. - https://api.audiobookshelf.org/#play-a-library-item-or-podcast-episode + https://api.audiobookshelf.org/#author """ - device_info: Annotated[ABSDeviceInfo, Alias("deviceInfo")] - force_direct_play: Annotated[bool, Alias("forceDirectPlay")] = False - force_transcode: Annotated[bool, Alias("forceTranscode")] = False - supported_mime_types: Annotated[list[str], Alias("supportedMimeTypes")] = field( - default_factory=list - ) - media_player: Annotated[str, Alias("mediaPlayer")] = "unknown" + id_: Annotated[str, Alias("id")] + name: str + + +@dataclass +class ABSAuthor(ABSAuthorMinified): + """ABSAuthor.""" + + asin: str | None + description: str | None + image_path: Annotated[str | None, Alias("imagePath")] + added_at: Annotated[int, Alias("addedAt")] # ms epoch + updated_at: Annotated[int, Alias("updatedAt")] # ms epoch + + +@dataclass +class ABSAuthorExpanded(ABSAuthor): + """ABSAuthorExpanded.""" + + num_books: Annotated[int, Alias("numBooks")] + + +### Series: https://api.audiobookshelf.org/#series + + +@dataclass +class _ABSSeriesBase(BaseModel): + """_ABSSeriesBase.""" + + id_: Annotated[str, Alias("id")] + name: str + + +@dataclass +class ABSSeries(_ABSSeriesBase): + """ABSSeries.""" + + description: str | None + added_at: Annotated[int, Alias("addedAt")] # ms epoch + updated_at: Annotated[int, Alias("updatedAt")] # ms epoch + + +@dataclass +class ABSSeriesNumBooks(_ABSSeriesBase): + """ABSSeriesNumBooks.""" + + name_ignore_prefix: Annotated[str, Alias("nameIgnorePrefix")] + library_item_ids: Annotated[list[str], Alias("libraryItemIds")] + num_books: Annotated[int, Alias("numBooks")] + + +@dataclass +class ABSSeriesSequence(BaseModel): + """Series Sequence. + + https://api.audiobookshelf.org/#series + """ + + id_: Annotated[str, Alias("id")] + name: str + sequence: str | None + + +# another variant, ABSSeriesBooks is further down + + +### https://api.audiobookshelf.org/#media-progress + + +@dataclass +class ABSMediaProgress(BaseModel): + """ABSMediaProgress.""" + + id_: Annotated[str, Alias("id")] + library_item_id: Annotated[str, Alias("libraryItemId")] + episode_id: Annotated[str, Alias("episodeId")] + duration: float # seconds + progress: float # percent 0->1 + current_time: Annotated[float, Alias("currentTime")] # seconds + is_finished: Annotated[bool, Alias("isFinished")] + hide_from_continue_listening: Annotated[bool, Alias("hideFromContinueListening")] + last_update: Annotated[int, Alias("lastUpdate")] # ms epoch + started_at: Annotated[int, Alias("startedAt")] # ms epoch + finished_at: Annotated[int | None, Alias("finishedAt")] # ms epoch + + +# two additional progress variants, 'with media' book and podcast, further down. + + +@dataclass +class ABSUser(BaseModel): + """ABSUser. + + only attributes we need for mass + https://api.audiobookshelf.org/#user + """ + + id_: Annotated[str, Alias("id")] + username: str + type_: Annotated[str, Alias("type")] + token: str + media_progress: Annotated[list[ABSMediaProgress], Alias("mediaProgress")] + series_hide_from_continue_listening: Annotated[ + list[str], Alias("seriesHideFromContinueListening") + ] + bookmarks: list[ABSAudioBookmark] + is_active: Annotated[bool, Alias("isActive")] + is_locked: Annotated[bool, Alias("isLocked")] + last_seen: Annotated[int | None, Alias("lastSeen")] + created_at: Annotated[int, Alias("createdAt")] + permissions: ABSUserPermissions + libraries_accessible: Annotated[list[str], Alias("librariesAccessible")] + + # this seems to be missing + # item_tags_accessible: Annotated[list[str], Alias("itemTagsAccessible")] + + +# two additional user variants do exist class ABSPlayMethod(Enum): @@ -229,12 +270,12 @@ class ABSPlayMethod(Enum): LOCAL = 3 +### https://api.audiobookshelf.org/#playback-session + + @dataclass class ABSPlaybackSession(BaseModel): - """ABSPlaybackSessionExpanded. - - https://api.audiobookshelf.org/#play-method - """ + """ABSPlaybackSession.""" id_: Annotated[str, Alias("id")] user_id: Annotated[str, Alias("userId")] @@ -265,10 +306,7 @@ class ABSPlaybackSession(BaseModel): @dataclass class ABSPlaybackSessionExpanded(ABSPlaybackSession): - """ABSPlaybackSessionExpanded. - - https://api.audiobookshelf.org/#play-method - """ + """ABSPlaybackSessionExpanded.""" audio_tracks: Annotated[list[ABSAudioTrack], Alias("audioTracks")] @@ -276,34 +314,12 @@ class ABSPlaybackSessionExpanded(ABSPlaybackSession): # libraryItem: -@dataclass -class ABSSessionUpdate(BaseModel): - """ - ABSSessionUpdate. - - Can be used as optional data to sync or closing request. - unit is seconds - """ - - current_time: Annotated[float, Alias("currentTime")] - time_listened: Annotated[float, Alias("timeListened")] - duration: float - - -@dataclass -class ABSSessionsResponse(BaseModel): - """Response to GET http://abs.example.com/api/me/listening-sessions.""" - - total: int - num_pages: Annotated[int, Alias("numPages")] - items_per_page: Annotated[int, Alias("itemsPerPage")] - sessions: list[ABSPlaybackSession] +### https://api.audiobookshelf.org/#podcast-metadata -## REWORK @dataclass -class ABSPodcastMetaDataNormal(BaseModel): - """ABSPodcastMetaDataNormal.""" +class ABSPodcastMetadata(BaseModel): + """ABSPodcastMetadata.""" title: str | None author: str | None @@ -321,18 +337,20 @@ class ABSPodcastMetaDataNormal(BaseModel): @dataclass -class ABSPodcastMetaDataMinified(ABSPodcastMetaDataNormal): - """ABSPodcastMetaDataMinified.""" +class ABSPodcastMetadataMinified(ABSPodcastMetadata): + """ABSPodcastMetadataMinified.""" title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")] -ABSPodcastMetaDataExpanded = ABSPodcastMetaDataMinified +ABSPodcastMetaDataExpanded = ABSPodcastMetadataMinified + +### https://api.audiobookshelf.org/#podcast-episode @dataclass -class ABSPodcastEpisodeNormal(BaseModel): - """ABSPodcastEpisodeNormal.""" +class ABSPodcastEpisode(BaseModel): + """ABSPodcastEpisode.""" library_item_id: Annotated[str, Alias("libraryItemId")] id_: Annotated[str, Alias("id")] @@ -383,33 +401,36 @@ class ABSPodcastEpisodeExpanded(BaseModel): @dataclass -class ABSPodcastBase(BaseModel): - """ABSPodcastNormal.""" +class _ABSPodcastBase(BaseModel): + """_ABSPodcastBase.""" cover_path: Annotated[str, Alias("coverPath")] +### https://api.audiobookshelf.org/#podcast + + @dataclass -class ABSPodcastNormal(BaseModel): - """ABSPodcastNormal.""" +class ABSPodcast(_ABSPodcastBase): + """ABSPodcast.""" - metadata: ABSPodcastMetaDataNormal + metadata: ABSPodcastMetadata library_item_id: Annotated[str, Alias("libraryItemId")] tags: list[str] - episodes: list[ABSPodcastEpisodeNormal] + episodes: list[ABSPodcastEpisode] @dataclass -class ABSPodcastMinified(ABSPodcastBase): +class ABSPodcastMinified(_ABSPodcastBase): """ABSPodcastMinified.""" - metadata: ABSPodcastMetaDataMinified + metadata: ABSPodcastMetadataMinified size: int # bytes num_episodes: Annotated[int, Alias("numEpisodes")] = 0 @dataclass -class ABSPodcastExpanded(ABSPodcastBase): +class ABSPodcastExpanded(_ABSPodcastBase): """ABSPodcastEpisodeExpanded.""" size: int # bytes @@ -417,9 +438,12 @@ class ABSPodcastExpanded(ABSPodcastBase): episodes: list[ABSPodcastEpisodeExpanded] +### https://api.audiobookshelf.org/#book-metadata + + @dataclass -class ABSAudioBookMetaDataBase(BaseModel): - """ABSAudioBookMetaDataMinified.""" +class _ABSBookMetadataBase(BaseModel): + """_ABSBookMetadataBase.""" title: str subtitle: str @@ -435,8 +459,8 @@ class ABSAudioBookMetaDataBase(BaseModel): @dataclass -class ABSAudioBookMetaDataNormal(ABSAudioBookMetaDataBase): - """ABSAudioBookMetaDataNormal.""" +class ABSBookMetadata(_ABSBookMetadataBase): + """ABSBookMetadata.""" authors: list[ABSAuthorMinified] narrators: list[str] @@ -444,8 +468,8 @@ class ABSAudioBookMetaDataNormal(ABSAudioBookMetaDataBase): @dataclass -class ABSAudioBookMetaDataMinified(ABSAudioBookMetaDataBase): - """ABSAudioBookMetaDataMinified.""" +class ABSBookMetadataMinified(_ABSBookMetadataBase): + """ABSBookMetadataMinified.""" # these are normally there title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")] @@ -456,34 +480,37 @@ class ABSAudioBookMetaDataMinified(ABSAudioBookMetaDataBase): @dataclass -class ABSAudioBookMetaDataExpanded(ABSAudioBookMetaDataNormal, ABSAudioBookMetaDataMinified): +class ABSBookMetadataExpanded(ABSBookMetadata, ABSBookMetadataMinified): """ABSAudioBookMetaDataExpanded.""" +### https://api.audiobookshelf.org/#book + + @dataclass -class ABSAudioBookBase(BaseModel): - """ABSAudioBookBase.""" +class _ABSBookBase(BaseModel): + """_ABSBookBase.""" tags: list[str] cover_path: Annotated[str | None, Alias("coverPath")] @dataclass -class ABSAudioBookNormal(ABSAudioBookBase): - """ABSAudioBookNormal.""" +class ABSBook(_ABSBookBase): + """ABSBook.""" library_item_id: Annotated[str, Alias("libraryItemId")] - metadata: ABSAudioBookMetaDataNormal + metadata: ABSBookMetadata # audioFiles - chapters: list[ABSAudioBookChapter] + chapters: list[ABSBookChapter] # ebookFile @dataclass -class ABSAudioBookMinified(ABSAudioBookBase): - """ABSAudioBookBase.""" +class ABSBookMinified(_ABSBookBase): + """ABSBookBase.""" - metadata: ABSAudioBookMetaDataMinified + metadata: ABSBookMetadataMinified num_tracks: Annotated[int, Alias("numTracks")] num_audiofiles: Annotated[int, Alias("numAudioFiles")] num_chapters: Annotated[int, Alias("numChapters")] @@ -493,20 +520,23 @@ class ABSAudioBookMinified(ABSAudioBookBase): @dataclass -class ABSAudioBookExpanded(ABSAudioBookBase): - """ABSAudioBookExpanded.""" +class ABSBookExpanded(_ABSBookBase): + """ABSBookExpanded.""" library_item_id: Annotated[str, Alias("libraryItemId")] - metadata: ABSAudioBookMetaDataExpanded - chapters: list[ABSAudioBookChapter] + metadata: ABSBookMetadataExpanded + chapters: list[ABSBookChapter] duration: float size: int # bytes tracks: list[ABSAudioTrack] +### https://api.audiobookshelf.org/#library-item + + @dataclass -class ABSLibraryItemBase(BaseModel): - """ABSLibraryItemBase.""" +class _ABSLibraryItemBase(BaseModel): + """_ABSLibraryItemBase.""" id_: Annotated[str, Alias("id")] ino: str @@ -526,8 +556,8 @@ class ABSLibraryItemBase(BaseModel): @dataclass -class ABSLibraryItemNormal(ABSLibraryItemBase): - """ABSLibraryItemNormal.""" +class _ABSLibraryItem(_ABSLibraryItemBase): + """ABSLibraryItem.""" last_scan: Annotated[int | None, Alias("lastScan")] # ms epoch scan_version: Annotated[str | None, Alias("scanVersion")] @@ -535,21 +565,33 @@ class ABSLibraryItemNormal(ABSLibraryItemBase): @dataclass -class ABSLibraryItemNormalBook(ABSLibraryItemNormal): - """ABSLibraryItemNormalBook.""" +class ABSLibraryItemBook(_ABSLibraryItem): + """ABSLibraryItemBook.""" + + media: ABSBook + + +@dataclass +class ABSLibraryItemBookSeries(ABSLibraryItemBook): + """ABSLibraryItemNormalBookSeries. - media: ABSAudioBookNormal + Special class, when having the scheme of SeriesBooks, see + https://api.audiobookshelf.org/#series, it gets an extra + sequence key. + """ + + sequence: int @dataclass -class ABSLibraryItemNormalPodcast(ABSLibraryItemNormal): - """ABSLibraryItemNormalBook.""" +class ABSLibraryItemPodcast(_ABSLibraryItem): + """ABSLibraryItemPodcast.""" - media: ABSPodcastNormal + media: ABSPodcast @dataclass -class ABSLibraryItemMinified(ABSLibraryItemBase): +class _ABSLibraryItemMinified(_ABSLibraryItemBase): """ABSLibraryItemMinified.""" num_files: Annotated[int, Alias("numFiles")] @@ -557,40 +599,100 @@ class ABSLibraryItemMinified(ABSLibraryItemBase): @dataclass -class ABSLibraryItemMinifiedBook(ABSLibraryItemMinified): +class ABSLibraryItemMinifiedBook(_ABSLibraryItemMinified): """ABSLibraryItemMinifiedBook.""" - media: ABSAudioBookMinified + media: ABSBookMinified @dataclass -class ABSLibraryItemMinifiedPodcast(ABSLibraryItemMinified): +class ABSLibraryItemMinifiedPodcast(_ABSLibraryItemMinified): """ABSLibraryItemMinifiedBook.""" media: ABSPodcastMinified @dataclass -class ABSLibraryItemExpanded(ABSLibraryItemBase): +class _ABSLibraryItemExpanded(_ABSLibraryItemBase): """ABSLibraryItemExpanded.""" size: int # bytes @dataclass -class ABSLibraryItemExpandedBook(ABSLibraryItemExpanded): +class ABSLibraryItemExpandedBook(_ABSLibraryItemExpanded): """ABSLibraryItemExpanded.""" - media: ABSAudioBookExpanded + media: ABSBookExpanded @dataclass -class ABSLibraryItemExpandedPodcast(ABSLibraryItemExpanded): +class ABSLibraryItemExpandedPodcast(_ABSLibraryItemExpanded): """ABSLibraryItemExpanded.""" media: ABSPodcastExpanded +# extra classes down here so they can make proper references + + +@dataclass +class ABSSeriesBooks(_ABSSeriesBase): + """ABSSeriesBooks.""" + + added_at: Annotated[int, Alias("addedAt")] # ms epoch + name_ignore_prefix: Annotated[str, Alias("nameIgnorePrefix")] + name_ignore_prefix_sort: Annotated[str, Alias("nameIgnorePrefixSort")] + type_: Annotated[str, Alias("type")] + books: list[ABSLibraryItemBookSeries] + total_duration: Annotated[float, Alias("totalDuration")] # s + + +@dataclass +class ABSMediaProgressWithMediaBook(ABSMediaProgress): + """ABSMediaProgressWithMediaBook.""" + + media: ABSBookExpanded + + +@dataclass +class ABSMediaProgressWithMediaPodcast(ABSMediaProgress): + """ABSMediaProgressWithMediaBook.""" + + media: ABSPodcastExpanded + episode: ABSPodcastEpisode + + +### Response to API Requests + + +@dataclass +class ABSLoginResponse(BaseModel): + """ABSLoginResponse.""" + + user: ABSUser + + # this seems to be missing + # user_default_library_id: Annotated[str, Alias("defaultLibraryId")] + + +@dataclass +class ABSLibrariesResponse(BaseModel): + """ABSLibrariesResponse.""" + + libraries: list[ABSLibrary] + + +@dataclass +class ABSSessionsResponse(BaseModel): + """Response to GET http://abs.example.com/api/me/listening-sessions.""" + + total: int + num_pages: Annotated[int, Alias("numPages")] + items_per_page: Annotated[int, Alias("itemsPerPage")] + sessions: list[ABSPlaybackSession] + + @dataclass class ABSLibrariesItemsMinifiedBookResponse(BaseModel): """ABSLibrariesItemsResponse. @@ -612,3 +714,36 @@ class ABSLibrariesItemsMinifiedPodcastResponse(BaseModel): """ results: list[ABSLibraryItemMinifiedPodcast] + + +### Requests to API we can make + + +@dataclass +class ABSPlayRequest(BaseModel): + """ABSPlayRequest. + + https://api.audiobookshelf.org/#play-a-library-item-or-podcast-episode + """ + + device_info: Annotated[ABSDeviceInfo, Alias("deviceInfo")] + force_direct_play: Annotated[bool, Alias("forceDirectPlay")] = False + force_transcode: Annotated[bool, Alias("forceTranscode")] = False + supported_mime_types: Annotated[list[str], Alias("supportedMimeTypes")] = field( + default_factory=list + ) + media_player: Annotated[str, Alias("mediaPlayer")] = "unknown" + + +@dataclass +class ABSSessionUpdate(BaseModel): + """ + ABSSessionUpdate. + + Can be used as optional data to sync or closing request. + unit is seconds + """ + + current_time: Annotated[float, Alias("currentTime")] + time_listened: Annotated[float, Alias("timeListened")] + duration: float From b5dad38390c64395b7f144e9ca8eb05132e3f900 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:35:07 +0100 Subject: [PATCH 04/16] add get parameters for consistency --- .../providers/audiobookshelf/abs_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index 6d0dd56d9..37598815a 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -172,11 +172,18 @@ async def get_all_podcasts(self) -> AsyncGenerator[ABSLibraryItemExpandedPodcast yield podcast async def _get_lib_items(self, lib: ABSLibrary) -> AsyncGenerator[bytes]: - """Get library items with pagination.""" + """Get library items with pagination. + + Note: + - minified=1 -> minified items. However, there appears to be + a bug in abs, so we always get minified items. Still there for + consistency + - collapseseries=0 -> even if books are part of a series, they will be single items + """ page_cnt = 0 while True: data = await self._get( - f"/libraries/{lib.id_}/items", + f"/libraries/{lib.id_}/items?minified=1&collapseseries=0", params={"limit": LIMIT_ITEMS_PER_PAGE, "page": page_cnt}, ) page_cnt += 1 From 26bd9646c852b779445d946ab7759d417eaa708f Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:04:47 +0100 Subject: [PATCH 05/16] fix: track index can be None in ABS v2.18.0 --- music_assistant/providers/audiobookshelf/abs_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_assistant/providers/audiobookshelf/abs_schema.py b/music_assistant/providers/audiobookshelf/abs_schema.py index 91c71ede4..c4a71a7ff 100644 --- a/music_assistant/providers/audiobookshelf/abs_schema.py +++ b/music_assistant/providers/audiobookshelf/abs_schema.py @@ -42,7 +42,7 @@ class ABSAudioTrack(BaseModel): https://api.audiobookshelf.org/#audio-track """ - index: int + index: int | None start_offset: Annotated[float, Alias("startOffset")] = 0.0 duration: float = 0.0 title: str = "" From 7d153cb54a26f8920e0b85d72b017ffb609808fa Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Tue, 21 Jan 2025 07:02:41 +0100 Subject: [PATCH 06/16] less api calls for audiobooks --- .../providers/audiobookshelf/__init__.py | 58 +++++++++++-------- .../providers/audiobookshelf/abs_client.py | 17 ++---- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index 353a8fe6c..8a6760bfc 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -40,6 +40,7 @@ ABSLibrary, ABSLibraryItemExpandedBook, ABSLibraryItemExpandedPodcast, + ABSLibraryItemMinifiedBook, ABSPlaybackSessionExpanded, ABSPodcastEpisodeExpanded, ) @@ -309,7 +310,9 @@ async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode: episode_cnt += 1 raise MediaNotFoundError("Episode not found") - async def _parse_audiobook(self, abs_audiobook: ABSLibraryItemExpandedBook) -> Audiobook: + async def _parse_audiobook( + self, abs_audiobook: ABSLibraryItemExpandedBook | ABSLibraryItemMinifiedBook + ) -> Audiobook: mass_audiobook = Audiobook( item_id=abs_audiobook.id_, provider=self.lookup_key, @@ -323,8 +326,6 @@ async def _parse_audiobook(self, abs_audiobook: ABSLibraryItemExpandedBook) -> A ) }, publisher=abs_audiobook.media.metadata.publisher, - authors=UniqueList([x.name for x in abs_audiobook.media.metadata.authors]), - narrators=UniqueList(abs_audiobook.media.metadata.narrators), ) mass_audiobook.metadata.description = abs_audiobook.media.metadata.description if abs_audiobook.media.metadata.language is not None: @@ -333,24 +334,7 @@ async def _parse_audiobook(self, abs_audiobook: ABSLibraryItemExpandedBook) -> A if abs_audiobook.media.metadata.genres is not None: mass_audiobook.metadata.genres = set(abs_audiobook.media.metadata.genres) - # chapters - chapters = [] - for idx, chapter in enumerate(abs_audiobook.media.chapters): - chapters.append( - MediaItemChapter( - position=idx + 1, # chapter starting at 1 - name=chapter.title, - start=chapter.start, - end=chapter.end, - ) - ) - mass_audiobook.metadata.chapters = chapters - mass_audiobook.metadata.explicit = abs_audiobook.media.metadata.explicit - progress, finished = await self._client.get_audiobook_progress_ms(abs_audiobook.id_) - if progress is not None: - mass_audiobook.resume_position_ms = progress - mass_audiobook.fully_played = finished # cover base_url = f"{self.config.get_value(CONF_URL)}" @@ -360,11 +344,37 @@ async def _parse_audiobook(self, abs_audiobook: ABSLibraryItemExpandedBook) -> A [MediaItemImage(type=ImageType.THUMB, path=cover_url, provider=self.lookup_key)] ) + # expanded version + if isinstance(abs_audiobook, ABSLibraryItemExpandedBook): + authors = UniqueList([x.name for x in abs_audiobook.media.metadata.authors]) + narrators = UniqueList(abs_audiobook.media.metadata.narrators) + mass_audiobook.authors = authors + mass_audiobook.narrators = narrators + chapters = [] + for idx, chapter in enumerate(abs_audiobook.media.chapters): + chapters.append( + MediaItemChapter( + position=idx + 1, # chapter starting at 1 + name=chapter.title, + start=chapter.start, + end=chapter.end, + ) + ) + mass_audiobook.metadata.chapters = chapters + + progress, finished = await self._client.get_audiobook_progress_ms(abs_audiobook.id_) + if progress is not None: + mass_audiobook.resume_position_ms = progress + mass_audiobook.fully_played = finished + elif isinstance(abs_audiobook, ABSLibraryItemMinifiedBook): + mass_audiobook.authors = UniqueList([abs_audiobook.media.metadata.author_name]) + mass_audiobook.narrators = UniqueList([abs_audiobook.media.metadata.narrator_name]) + return mass_audiobook async def get_library_audiobooks(self) -> AsyncGenerator[Audiobook, None]: """Get Audiobook libraries.""" - async for abs_audiobook in self._client.get_all_audiobooks(): + async for abs_audiobook in self._client.get_all_audiobooks_minified(): mass_audiobook = await self._parse_audiobook(abs_audiobook) yield mass_audiobook @@ -557,7 +567,9 @@ async def _browse_lib( raise MediaNotFoundError("Lib missing.") def get_item_mapping( - item: ABSLibraryItemExpandedBook | ABSLibraryItemExpandedPodcast, + item: ABSLibraryItemExpandedBook + | ABSLibraryItemMinifiedBook + | ABSLibraryItemExpandedPodcast, ) -> ItemMapping: title = item.media.metadata.title if title is None: @@ -578,7 +590,7 @@ def get_item_mapping( async for podcast in self._client.get_all_podcasts_by_library(library): items.append(get_item_mapping(podcast)) elif media_type == MediaType.AUDIOBOOK: - async for audiobook in self._client.get_all_audiobooks_by_library(library): + async for audiobook in self._client.get_all_audiobooks_by_library_minified(library): items.append(get_item_mapping(audiobook)) else: raise RuntimeError(f"Media type must not be {media_type}") diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index 37598815a..41e77dd4f 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -301,29 +301,22 @@ async def update_audiobook_progress( endpoint = f"me/progress/{audiobook_id}" await self._update_progress(endpoint, progress_s, duration_s, is_finished) - async def get_all_audiobooks(self) -> AsyncGenerator[ABSLibraryItemExpandedBook]: + async def get_all_audiobooks_minified(self) -> AsyncGenerator[ABSLibraryItemMinifiedBook]: """Get all audiobooks.""" for library in self.audiobook_libraries: - async for book in self.get_all_audiobooks_by_library(library): + async for book in self.get_all_audiobooks_by_library_minified(library): yield book - async def get_all_audiobooks_by_library( + async def get_all_audiobooks_by_library_minified( self, lib: ABSLibrary - ) -> AsyncGenerator[ABSLibraryItemExpandedBook]: + ) -> AsyncGenerator[ABSLibraryItemMinifiedBook]: """Get all Audiobooks in a library.""" async for audiobook_data in self._get_lib_items(lib): audiobook_list = ABSLibrariesItemsMinifiedBookResponse.from_json(audiobook_data).results if not audiobook_list: # [] if page exceeds return - async def _get_id( - alist: list[ABSLibraryItemMinifiedBook] = audiobook_list, - ) -> AsyncGenerator[str]: - for entry in alist: - yield entry.id_ - - async for id_ in _get_id(): - audiobook = await self.get_audiobook(id_) + for audiobook in audiobook_list: yield audiobook async def get_audiobook(self, id_: str) -> ABSLibraryItemExpandedBook: From d9b7dab48ee41a04a6b51e56226e5546c016edc3 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Tue, 21 Jan 2025 07:14:03 +0100 Subject: [PATCH 07/16] less api calls for podcast --- .../providers/audiobookshelf/__init__.py | 32 +++++++++++++------ .../providers/audiobookshelf/abs_client.py | 13 ++------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index 8a6760bfc..dd8a43a03 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -41,6 +41,7 @@ ABSLibraryItemExpandedBook, ABSLibraryItemExpandedPodcast, ABSLibraryItemMinifiedBook, + ABSLibraryItemMinifiedPodcast, ABSPlaybackSessionExpanded, ABSPodcastEpisodeExpanded, ) @@ -175,7 +176,9 @@ async def sync_library(self, media_types: tuple[MediaType, ...]) -> None: await self._client.sync() await super().sync_library(media_types=media_types) - def _parse_podcast(self, abs_podcast: ABSLibraryItemExpandedPodcast) -> Podcast: + def _parse_podcast( + self, abs_podcast: ABSLibraryItemExpandedPodcast | ABSLibraryItemMinifiedPodcast + ) -> Podcast: """Translate ABSPodcast to MassPodcast.""" title = abs_podcast.media.metadata.title # Per API doc title may be None. @@ -186,7 +189,6 @@ def _parse_podcast(self, abs_podcast: ABSLibraryItemExpandedPodcast) -> Podcast: name=title, publisher=abs_podcast.media.metadata.author, provider=self.lookup_key, - total_episodes=len(abs_podcast.media.episodes), provider_mappings={ ProviderMapping( item_id=abs_podcast.id_, @@ -210,6 +212,11 @@ def _parse_podcast(self, abs_podcast: ABSLibraryItemExpandedPodcast) -> Podcast: mass_podcast.metadata.genres = set(abs_podcast.media.metadata.genres) mass_podcast.metadata.release_date = abs_podcast.media.metadata.release_date + if isinstance(abs_podcast, ABSLibraryItemExpandedPodcast): + mass_podcast.total_episodes = len(abs_podcast.media.episodes) + elif isinstance(abs_podcast, ABSLibraryItemMinifiedPodcast): + mass_podcast.total_episodes = abs_podcast.media.num_episodes + return mass_podcast async def _parse_podcast_episode( @@ -217,6 +224,7 @@ async def _parse_podcast_episode( episode: ABSPodcastEpisodeExpanded, prov_podcast_id: str, fallback_episode_cnt: int | None = None, + add_progress: bool = False, # progress only needed on playback, saves one api call ) -> PodcastEpisode: """Translate ABSPodcastEpisode to MassPodcastEpisode. @@ -257,12 +265,13 @@ async def _parse_podcast_episode( ) }, ) - progress, finished = await self._client.get_podcast_progress_ms( - prov_podcast_id, episode.id_ - ) - if progress is not None: - mass_episode.resume_position_ms = progress - mass_episode.fully_played = finished + if add_progress: + progress, finished = await self._client.get_podcast_progress_ms( + prov_podcast_id, episode.id_ + ) + if progress is not None: + mass_episode.resume_position_ms = progress + mass_episode.fully_played = finished # cover image url_base = f"{self.config.get_value(CONF_URL)}" @@ -305,7 +314,9 @@ async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode: episode_cnt = 1 for abs_episode in abs_podcast.media.episodes: if abs_episode.id_ == e_id: - return await self._parse_podcast_episode(abs_episode, prov_podcast_id, episode_cnt) + return await self._parse_podcast_episode( + abs_episode, prov_podcast_id, episode_cnt, add_progress=True + ) episode_cnt += 1 raise MediaNotFoundError("Episode not found") @@ -569,7 +580,8 @@ async def _browse_lib( def get_item_mapping( item: ABSLibraryItemExpandedBook | ABSLibraryItemMinifiedBook - | ABSLibraryItemExpandedPodcast, + | ABSLibraryItemExpandedPodcast + | ABSLibraryItemMinifiedPodcast, ) -> ItemMapping: title = item.media.metadata.title if title is None: diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index 41e77dd4f..c72d71274 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -165,7 +165,7 @@ async def sync(self) -> None: self.podcast_libraries.append(library) self.user = await self.get_authenticated_user() - async def get_all_podcasts(self) -> AsyncGenerator[ABSLibraryItemExpandedPodcast]: + async def get_all_podcasts(self) -> AsyncGenerator[ABSLibraryItemMinifiedPodcast]: """Get all available podcasts.""" for library in self.podcast_libraries: async for podcast in self.get_all_podcasts_by_library(library): @@ -191,21 +191,14 @@ async def _get_lib_items(self, lib: ABSLibrary) -> AsyncGenerator[bytes]: async def get_all_podcasts_by_library( self, lib: ABSLibrary - ) -> AsyncGenerator[ABSLibraryItemExpandedPodcast]: + ) -> AsyncGenerator[ABSLibraryItemMinifiedPodcast]: """Get all podcasts in a library.""" async for podcast_data in self._get_lib_items(lib): podcast_list = ABSLibrariesItemsMinifiedPodcastResponse.from_json(podcast_data).results if not podcast_list: # [] if page exceeds return - async def _get_id( - plist: list[ABSLibraryItemMinifiedPodcast] = podcast_list, - ) -> AsyncGenerator[str]: - for entry in plist: - yield entry.id_ - - async for id_ in _get_id(): - podcast = await self.get_podcast(id_) + for podcast in podcast_list: yield podcast async def get_podcast(self, id_: str) -> ABSLibraryItemExpandedPodcast: From 91bbef768139ffdf2c5ceac6f8e51efe3af749f6 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Tue, 21 Jan 2025 07:17:19 +0100 Subject: [PATCH 08/16] renamed functions to include return type --- .../providers/audiobookshelf/__init__.py | 16 ++++++++-------- .../providers/audiobookshelf/abs_client.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index dd8a43a03..ebc34e58d 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -285,18 +285,18 @@ async def _parse_podcast_episode( async def get_library_podcasts(self) -> AsyncGenerator[Podcast, None]: """Retrieve library/subscribed podcasts from the provider.""" - async for abs_podcast in self._client.get_all_podcasts(): + async for abs_podcast in self._client.get_all_podcasts_minified(): mass_podcast = self._parse_podcast(abs_podcast) yield mass_podcast async def get_podcast(self, prov_podcast_id: str) -> Podcast: """Get single podcast.""" - abs_podcast = await self._client.get_podcast(prov_podcast_id) + abs_podcast = await self._client.get_podcast_expanded(prov_podcast_id) return self._parse_podcast(abs_podcast) async def get_podcast_episodes(self, prov_podcast_id: str) -> list[PodcastEpisode]: """Get all podcast episodes of podcast.""" - abs_podcast = await self._client.get_podcast(prov_podcast_id) + abs_podcast = await self._client.get_podcast_expanded(prov_podcast_id) episode_list = [] episode_cnt = 1 for abs_episode in abs_podcast.media.episodes: @@ -310,7 +310,7 @@ async def get_podcast_episodes(self, prov_podcast_id: str) -> list[PodcastEpisod async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode: """Get single podcast episode.""" prov_podcast_id, e_id = prov_episode_id.split(" ") - abs_podcast = await self._client.get_podcast(prov_podcast_id) + abs_podcast = await self._client.get_podcast_expanded(prov_podcast_id) episode_cnt = 1 for abs_episode in abs_podcast.media.episodes: if abs_episode.id_ == e_id: @@ -391,7 +391,7 @@ async def get_library_audiobooks(self) -> AsyncGenerator[Audiobook, None]: async def get_audiobook(self, prov_audiobook_id: str) -> Audiobook: """Get a single audiobook.""" - abs_audiobook = await self._client.get_audiobook(prov_audiobook_id) + abs_audiobook = await self._client.get_audiobook_expanded(prov_audiobook_id) return await self._parse_audiobook(abs_audiobook) async def get_streamdetails_from_playback_session( @@ -440,7 +440,7 @@ async def get_stream_details( if media_type == MediaType.PODCAST_EPISODE: return await self._get_stream_details_podcast_episode(item_id) elif media_type == MediaType.AUDIOBOOK: - abs_audiobook = await self._client.get_audiobook(item_id) + abs_audiobook = await self._client.get_audiobook_expanded(item_id) tracks = abs_audiobook.media.tracks if len(tracks) == 0: raise MediaNotFoundError("Stream not found") @@ -482,7 +482,7 @@ async def _get_stream_details_podcast_episode(self, podcast_id: str) -> StreamDe abs_podcast_id, abs_episode_id = podcast_id.split(" ") abs_episode = None - abs_podcast = await self._client.get_podcast(abs_podcast_id) + abs_podcast = await self._client.get_podcast_expanded(abs_podcast_id) for abs_episode in abs_podcast.media.episodes: if abs_episode.id_ == abs_episode_id: break @@ -599,7 +599,7 @@ def get_item_mapping( items: list[MediaItemType | ItemMapping] = [] if media_type == MediaType.PODCAST: - async for podcast in self._client.get_all_podcasts_by_library(library): + async for podcast in self._client.get_all_podcasts_by_library_minified(library): items.append(get_item_mapping(podcast)) elif media_type == MediaType.AUDIOBOOK: async for audiobook in self._client.get_all_audiobooks_by_library_minified(library): diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index c72d71274..538f39228 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -165,10 +165,10 @@ async def sync(self) -> None: self.podcast_libraries.append(library) self.user = await self.get_authenticated_user() - async def get_all_podcasts(self) -> AsyncGenerator[ABSLibraryItemMinifiedPodcast]: + async def get_all_podcasts_minified(self) -> AsyncGenerator[ABSLibraryItemMinifiedPodcast]: """Get all available podcasts.""" for library in self.podcast_libraries: - async for podcast in self.get_all_podcasts_by_library(library): + async for podcast in self.get_all_podcasts_by_library_minified(library): yield podcast async def _get_lib_items(self, lib: ABSLibrary) -> AsyncGenerator[bytes]: @@ -189,7 +189,7 @@ async def _get_lib_items(self, lib: ABSLibrary) -> AsyncGenerator[bytes]: page_cnt += 1 yield data - async def get_all_podcasts_by_library( + async def get_all_podcasts_by_library_minified( self, lib: ABSLibrary ) -> AsyncGenerator[ABSLibraryItemMinifiedPodcast]: """Get all podcasts in a library.""" @@ -201,7 +201,7 @@ async def get_all_podcasts_by_library( for podcast in podcast_list: yield podcast - async def get_podcast(self, id_: str) -> ABSLibraryItemExpandedPodcast: + async def get_podcast_expanded(self, id_: str) -> ABSLibraryItemExpandedPodcast: """Get a single Podcast by ID.""" # this endpoint gives more podcast extra data data = await self._get(f"items/{id_}?expanded=1") @@ -312,7 +312,7 @@ async def get_all_audiobooks_by_library_minified( for audiobook in audiobook_list: yield audiobook - async def get_audiobook(self, id_: str) -> ABSLibraryItemExpandedBook: + async def get_audiobook_expanded(self, id_: str) -> ABSLibraryItemExpandedBook: """Get a single Audiobook by ID.""" # this endpoint gives more audiobook extra data audiobook = await self._get(f"items/{id_}?expanded=1") From cee0537451549f5c92f0ac81b91a8a48bb682628 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:38:15 +0100 Subject: [PATCH 09/16] fix progress episode --- .../providers/audiobookshelf/__init__.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index ebc34e58d..2112a39b0 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -224,7 +224,6 @@ async def _parse_podcast_episode( episode: ABSPodcastEpisodeExpanded, prov_podcast_id: str, fallback_episode_cnt: int | None = None, - add_progress: bool = False, # progress only needed on playback, saves one api call ) -> PodcastEpisode: """Translate ABSPodcastEpisode to MassPodcastEpisode. @@ -265,13 +264,12 @@ async def _parse_podcast_episode( ) }, ) - if add_progress: - progress, finished = await self._client.get_podcast_progress_ms( - prov_podcast_id, episode.id_ - ) - if progress is not None: - mass_episode.resume_position_ms = progress - mass_episode.fully_played = finished + progress, finished = await self._client.get_podcast_progress_ms( + prov_podcast_id, episode.id_ + ) + if progress is not None: + mass_episode.resume_position_ms = progress + mass_episode.fully_played = finished # cover image url_base = f"{self.config.get_value(CONF_URL)}" @@ -314,9 +312,7 @@ async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode: episode_cnt = 1 for abs_episode in abs_podcast.media.episodes: if abs_episode.id_ == e_id: - return await self._parse_podcast_episode( - abs_episode, prov_podcast_id, episode_cnt, add_progress=True - ) + return await self._parse_podcast_episode(abs_episode, prov_podcast_id, episode_cnt) episode_cnt += 1 raise MediaNotFoundError("Episode not found") From 5a5b026e77471eac9b78a0ef4812d5c259b7fba7 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:39:28 +0100 Subject: [PATCH 10/16] sync client once, not twice during setup --- music_assistant/providers/audiobookshelf/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index 2112a39b0..40c6b0b39 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -142,7 +142,6 @@ async def handle_async_init(self) -> None: except RuntimeError: # login details were not correct raise LoginFailed(f"Login to abs instance at {base_url} failed.") - await self._client.sync() # this will be provided when creating sessions or receive already opened sessions self.device_info = ABSDeviceInfo( From 1d9345bcc6d37746dca7abe05400c363a28ff9b1 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Wed, 22 Jan 2025 23:21:04 +0100 Subject: [PATCH 11/16] remove api calls in browse Ids associated with libs are stored in client for now. Something for cache controller? --- .../providers/audiobookshelf/__init__.py | 42 ++++++------------- .../providers/audiobookshelf/abs_client.py | 30 +++++++++---- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index 40c6b0b39..16ca67f3d 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -34,10 +34,9 @@ from music_assistant_models.streamdetails import StreamDetails from music_assistant.models.music_provider import MusicProvider -from music_assistant.providers.audiobookshelf.abs_client import ABSClient +from music_assistant.providers.audiobookshelf.abs_client import ABSClient, LibraryWithItemIDs from music_assistant.providers.audiobookshelf.abs_schema import ( ABSDeviceInfo, - ABSLibrary, ABSLibraryItemExpandedBook, ABSLibraryItemExpandedPodcast, ABSLibraryItemMinifiedBook, @@ -536,7 +535,7 @@ async def on_played( ) async def _browse_root( - self, library_list: list[ABSLibrary], item_path: str + self, library_list: list[LibraryWithItemIDs], item_path: str ) -> Sequence[MediaItemType | ItemMapping]: """Browse root folder in browse view. @@ -558,7 +557,7 @@ async def _browse_root( async def _browse_lib( self, library_id: str, - library_list: list[ABSLibrary], + library_list: list[LibraryWithItemIDs], media_type: MediaType, ) -> Sequence[MediaItemType | ItemMapping]: """Browse lib folder in browse view. @@ -572,33 +571,16 @@ async def _browse_lib( if library is None: raise MediaNotFoundError("Lib missing.") - def get_item_mapping( - item: ABSLibraryItemExpandedBook - | ABSLibraryItemMinifiedBook - | ABSLibraryItemExpandedPodcast - | ABSLibraryItemMinifiedPodcast, - ) -> ItemMapping: - title = item.media.metadata.title - if title is None: - title = "UNKNOWN" - token = self._client.token - url = f"{self.config.get_value(CONF_URL)}/api/items/{item.id_}/cover?token={token}" - image = MediaItemImage(type=ImageType.THUMB, path=url, provider=self.lookup_key) - return ItemMapping( - media_type=media_type, - item_id=item.id_, - provider=self.lookup_key, - name=title, - image=image, - ) - items: list[MediaItemType | ItemMapping] = [] - if media_type == MediaType.PODCAST: - async for podcast in self._client.get_all_podcasts_by_library_minified(library): - items.append(get_item_mapping(podcast)) - elif media_type == MediaType.AUDIOBOOK: - async for audiobook in self._client.get_all_audiobooks_by_library_minified(library): - items.append(get_item_mapping(audiobook)) + if media_type in [MediaType.PODCAST, MediaType.AUDIOBOOK]: + for item_id in library.item_ids: + mass_item = await self.mass.music.get_library_item_by_prov_id( + media_type=media_type, + item_id=item_id, + provider_instance_id_or_domain=self.instance_id, + ) + if mass_item is not None: + items.append(mass_item) else: raise RuntimeError(f"Media type must not be {media_type}") return items diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index 538f39228..265226894 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -5,6 +5,7 @@ import logging from collections.abc import AsyncGenerator +from dataclasses import dataclass, field from enum import Enum from typing import Any @@ -16,7 +17,6 @@ ABSLibrariesItemsMinifiedBookResponse, ABSLibrariesItemsMinifiedPodcastResponse, ABSLibrariesResponse, - ABSLibrary, ABSLibraryItemExpandedBook, ABSLibraryItemExpandedPodcast, ABSLibraryItemMinifiedBook, @@ -35,6 +35,15 @@ LIMIT_ITEMS_PER_PAGE = 10 +@dataclass +class LibraryWithItemIDs: + """Helper class to store ABSLibrary, and the ids of the items associated.""" + + id_: str + name: str = "" + item_ids: UniqueList[str] = field(default_factory=UniqueList[str]) + + class ABSStatus(Enum): """ABS Status Enum.""" @@ -50,8 +59,8 @@ class ABSClient: def __init__(self) -> None: """Client authorization.""" - self.podcast_libraries: list[ABSLibrary] = [] - self.audiobook_libraries: list[ABSLibrary] = [] + self.podcast_libraries: list[LibraryWithItemIDs] = [] + self.audiobook_libraries: list[LibraryWithItemIDs] = [] self.user: ABSUser self.check_ssl: bool # I would like to receive opened sessions via the API, however, it appears @@ -159,10 +168,11 @@ async def sync(self) -> None: for library in libraries.libraries: media_type = library.media_type if library.id_ not in ids: + _library = LibraryWithItemIDs(library.id_, library.name) if media_type == "book": - self.audiobook_libraries.append(library) + self.audiobook_libraries.append(_library) elif media_type == "podcast": - self.podcast_libraries.append(library) + self.podcast_libraries.append(_library) self.user = await self.get_authenticated_user() async def get_all_podcasts_minified(self) -> AsyncGenerator[ABSLibraryItemMinifiedPodcast]: @@ -171,7 +181,7 @@ async def get_all_podcasts_minified(self) -> AsyncGenerator[ABSLibraryItemMinifi async for podcast in self.get_all_podcasts_by_library_minified(library): yield podcast - async def _get_lib_items(self, lib: ABSLibrary) -> AsyncGenerator[bytes]: + async def _get_lib_items(self, lib: LibraryWithItemIDs) -> AsyncGenerator[bytes]: """Get library items with pagination. Note: @@ -190,7 +200,7 @@ async def _get_lib_items(self, lib: ABSLibrary) -> AsyncGenerator[bytes]: yield data async def get_all_podcasts_by_library_minified( - self, lib: ABSLibrary + self, lib: LibraryWithItemIDs ) -> AsyncGenerator[ABSLibraryItemMinifiedPodcast]: """Get all podcasts in a library.""" async for podcast_data in self._get_lib_items(lib): @@ -199,6 +209,8 @@ async def get_all_podcasts_by_library_minified( return for podcast in podcast_list: + # store ids of library items for later use + lib.item_ids.append(podcast.id_) yield podcast async def get_podcast_expanded(self, id_: str) -> ABSLibraryItemExpandedPodcast: @@ -301,7 +313,7 @@ async def get_all_audiobooks_minified(self) -> AsyncGenerator[ABSLibraryItemMini yield book async def get_all_audiobooks_by_library_minified( - self, lib: ABSLibrary + self, lib: LibraryWithItemIDs ) -> AsyncGenerator[ABSLibraryItemMinifiedBook]: """Get all Audiobooks in a library.""" async for audiobook_data in self._get_lib_items(lib): @@ -310,6 +322,8 @@ async def get_all_audiobooks_by_library_minified( return for audiobook in audiobook_list: + # store ids of library items for later use + lib.item_ids.append(audiobook.id_) yield audiobook async def get_audiobook_expanded(self, id_: str) -> ABSLibraryItemExpandedBook: From d1f3be23eb2a73c0f6eac3518dd5b33219a875aa Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:49:59 +0100 Subject: [PATCH 12/16] comment unused schema definitions --- .../providers/audiobookshelf/abs_client.py | 2 +- .../providers/audiobookshelf/abs_schema.py | 186 +++++++++--------- 2 files changed, 94 insertions(+), 94 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index 265226894..5de9c2426 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -417,7 +417,7 @@ async def get_all_closed_playback_sessions(self) -> AsyncGenerator[ABSPlaybackSe page_cnt += 1 sessions = ABSSessionsResponse.from_json(data).sessions - self.logger.debug([session.device_info for session in sessions]) + # self.logger.debug([session.device_info for session in sessions]) if sessions: for session in sessions: yield session diff --git a/music_assistant/providers/audiobookshelf/abs_schema.py b/music_assistant/providers/audiobookshelf/abs_schema.py index c4a71a7ff..246ac4302 100644 --- a/music_assistant/providers/audiobookshelf/abs_schema.py +++ b/music_assistant/providers/audiobookshelf/abs_schema.py @@ -42,10 +42,10 @@ class ABSAudioTrack(BaseModel): https://api.audiobookshelf.org/#audio-track """ - index: int | None - start_offset: Annotated[float, Alias("startOffset")] = 0.0 - duration: float = 0.0 - title: str = "" + # index: int | None + # start_offset: Annotated[float, Alias("startOffset")] = 0.0 + # duration: float = 0.0 + # title: str = "" content_url: Annotated[str, Alias("contentUrl")] = "" mime_type: str = "" # metadata: # not needed for mass application @@ -85,13 +85,13 @@ class ABSUserPermissions(BaseModel): https://api.audiobookshelf.org/#user-permissions """ - download: bool - update: bool - delete: bool - upload: bool - access_all_libraries: Annotated[bool, Alias("accessAllLibraries")] - access_all_tags: Annotated[bool, Alias("accessAllTags")] - access_explicit_content: Annotated[bool, Alias("accessExplicitContent")] + # download: bool + # update: bool + # delete: bool + # upload: bool + # access_all_libraries: Annotated[bool, Alias("accessAllLibraries")] + # access_all_tags: Annotated[bool, Alias("accessAllTags")] + # access_explicit_content: Annotated[bool, Alias("accessExplicitContent")] @dataclass @@ -108,10 +108,10 @@ class ABSLibrary(BaseModel): # displayOrder: Integer # icon: String media_type: Annotated[str, Alias("mediaType")] - provider: str + # provider: str # settings - created_at: Annotated[int, Alias("createdAt")] - last_update: Annotated[int, Alias("lastUpdate")] + # created_at: Annotated[int, Alias("createdAt")] + # last_update: Annotated[int, Alias("lastUpdate")] @dataclass @@ -149,11 +149,11 @@ class ABSAuthorMinified(BaseModel): class ABSAuthor(ABSAuthorMinified): """ABSAuthor.""" - asin: str | None + # asin: str | None description: str | None - image_path: Annotated[str | None, Alias("imagePath")] - added_at: Annotated[int, Alias("addedAt")] # ms epoch - updated_at: Annotated[int, Alias("updatedAt")] # ms epoch + # image_path: Annotated[str | None, Alias("imagePath")] + # added_at: Annotated[int, Alias("addedAt")] # ms epoch + # updated_at: Annotated[int, Alias("updatedAt")] # ms epoch @dataclass @@ -179,8 +179,8 @@ class ABSSeries(_ABSSeriesBase): """ABSSeries.""" description: str | None - added_at: Annotated[int, Alias("addedAt")] # ms epoch - updated_at: Annotated[int, Alias("updatedAt")] # ms epoch + # added_at: Annotated[int, Alias("addedAt")] # ms epoch + # updated_at: Annotated[int, Alias("updatedAt")] # ms epoch @dataclass @@ -221,10 +221,10 @@ class ABSMediaProgress(BaseModel): progress: float # percent 0->1 current_time: Annotated[float, Alias("currentTime")] # seconds is_finished: Annotated[bool, Alias("isFinished")] - hide_from_continue_listening: Annotated[bool, Alias("hideFromContinueListening")] - last_update: Annotated[int, Alias("lastUpdate")] # ms epoch - started_at: Annotated[int, Alias("startedAt")] # ms epoch - finished_at: Annotated[int | None, Alias("finishedAt")] # ms epoch + # hide_from_continue_listening: Annotated[bool, Alias("hideFromContinueListening")] + # last_update: Annotated[int, Alias("lastUpdate")] # ms epoch + # started_at: Annotated[int, Alias("startedAt")] # ms epoch + # finished_at: Annotated[int | None, Alias("finishedAt")] # ms epoch # two additional progress variants, 'with media' book and podcast, further down. @@ -243,15 +243,15 @@ class ABSUser(BaseModel): type_: Annotated[str, Alias("type")] token: str media_progress: Annotated[list[ABSMediaProgress], Alias("mediaProgress")] - series_hide_from_continue_listening: Annotated[ - list[str], Alias("seriesHideFromContinueListening") - ] - bookmarks: list[ABSAudioBookmark] - is_active: Annotated[bool, Alias("isActive")] - is_locked: Annotated[bool, Alias("isLocked")] - last_seen: Annotated[int | None, Alias("lastSeen")] - created_at: Annotated[int, Alias("createdAt")] - permissions: ABSUserPermissions + # series_hide_from_continue_listening: Annotated[ + # list[str], Alias("seriesHideFromContinueListening") + # ] + # bookmarks: list[ABSAudioBookmark] + # is_active: Annotated[bool, Alias("isActive")] + # is_locked: Annotated[bool, Alias("isLocked")] + # last_seen: Annotated[int | None, Alias("lastSeen")] + # created_at: Annotated[int, Alias("createdAt")] + # permissions: ABSUserPermissions libraries_accessible: Annotated[list[str], Alias("librariesAccessible")] # this seems to be missing @@ -278,30 +278,30 @@ class ABSPlaybackSession(BaseModel): """ABSPlaybackSession.""" id_: Annotated[str, Alias("id")] - user_id: Annotated[str, Alias("userId")] - library_id: Annotated[str, Alias("libraryId")] + # user_id: Annotated[str, Alias("userId")] + # library_id: Annotated[str, Alias("libraryId")] library_item_id: Annotated[str, Alias("libraryItemId")] episode_id: Annotated[str | None, Alias("episodeId")] media_type: Annotated[str, Alias("mediaType")] # media_metadata: Annotated[ABSPodcastMetaData | ABSAudioBookMetaData, Alias("mediaMetadata")] # chapters: list[ABSAudioBookChapter] display_title: Annotated[str, Alias("displayTitle")] - display_author: Annotated[str, Alias("displayAuthor")] - cover_path: Annotated[str, Alias("coverPath")] - duration: float + # display_author: Annotated[str, Alias("displayAuthor")] + # cover_path: Annotated[str, Alias("coverPath")] + # duration: float # 0: direct play, 1: direct stream, 2: transcode, 3: local - play_method: Annotated[ABSPlayMethod, Alias("playMethod")] - media_player: Annotated[str, Alias("mediaPlayer")] - device_info: Annotated[ABSDeviceInfo, Alias("deviceInfo")] - server_version: Annotated[str, Alias("serverVersion")] + # play_method: Annotated[ABSPlayMethod, Alias("playMethod")] + # media_player: Annotated[str, Alias("mediaPlayer")] + # device_info: Annotated[ABSDeviceInfo, Alias("deviceInfo")] + # server_version: Annotated[str, Alias("serverVersion")] # YYYY-MM-DD - date: str - day_of_week: Annotated[str, Alias("dayOfWeek")] - time_listening: Annotated[float, Alias("timeListening")] # s - start_time: Annotated[float, Alias("startTime")] # s - current_time: Annotated[float, Alias("currentTime")] # s - started_at: Annotated[int, Alias("startedAt")] # ms since Unix Epoch - updated_at: Annotated[int, Alias("updatedAt")] # ms since Unix Epoch + # date: str + # day_of_week: Annotated[str, Alias("dayOfWeek")] + # time_listening: Annotated[float, Alias("timeListening")] # s + # start_time: Annotated[float, Alias("startTime")] # s + # current_time: Annotated[float, Alias("currentTime")] # s + # started_at: Annotated[int, Alias("startedAt")] # ms since Unix Epoch + # updated_at: Annotated[int, Alias("updatedAt")] # ms since Unix Epoch @dataclass @@ -326,21 +326,21 @@ class ABSPodcastMetadata(BaseModel): description: str | None release_date: Annotated[str | None, Alias("releaseDate")] genres: list[str] | None - feed_url: Annotated[str | None, Alias("feedUrl")] - image_url: Annotated[str | None, Alias("imageUrl")] - itunes_page_url: Annotated[str | None, Alias("itunesPageUrl")] - itunes_id: Annotated[int | None, Alias("itunesId")] - itunes_artist_id: Annotated[int | None, Alias("itunesArtistId")] + # feed_url: Annotated[str | None, Alias("feedUrl")] + # image_url: Annotated[str | None, Alias("imageUrl")] + # itunes_page_url: Annotated[str | None, Alias("itunesPageUrl")] + # itunes_id: Annotated[int | None, Alias("itunesId")] + # itunes_artist_id: Annotated[int | None, Alias("itunesArtistId")] explicit: bool language: str | None - type_: Annotated[str | None, Alias("type")] + # type_: Annotated[str | None, Alias("type")] @dataclass class ABSPodcastMetadataMinified(ABSPodcastMetadata): """ABSPodcastMetadataMinified.""" - title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")] + # title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")] ABSPodcastMetaDataExpanded = ABSPodcastMetadataMinified @@ -359,15 +359,15 @@ class ABSPodcastEpisode(BaseModel): published_at: Annotated[int | None, Alias("publishedAt")] # ms posix epoch added_at: Annotated[int | None, Alias("addedAt")] # ms posix epoch updated_at: Annotated[int | None, Alias("updatedAt")] # ms posix epoch - season: str = "" + # season: str = "" episode: str = "" - episode_type: Annotated[str, Alias("episodeType")] = "" + # episode_type: Annotated[str, Alias("episodeType")] = "" title: str = "" subtitle: str = "" description: str = "" - enclosure: str = "" + # enclosure: str = "" pub_date: Annotated[str, Alias("pubDate")] = "" - guid: str = "" + # guid: str = "" # chapters @@ -384,18 +384,18 @@ class ABSPodcastEpisodeExpanded(BaseModel): # audio_file: # not needed for mass application published_at: Annotated[int | None, Alias("publishedAt")] # ms posix epoch added_at: Annotated[int | None, Alias("addedAt")] # ms posix epoch - updated_at: Annotated[int | None, Alias("updatedAt")] # ms posix epoch + # updated_at: Annotated[int | None, Alias("updatedAt")] # ms posix epoch audio_track: Annotated[ABSAudioTrack, Alias("audioTrack")] - size: int # in bytes - season: str = "" + # size: int # in bytes + # season: str = "" episode: str = "" - episode_type: Annotated[str, Alias("episodeType")] = "" + # episode_type: Annotated[str, Alias("episodeType")] = "" title: str = "" subtitle: str = "" description: str = "" - enclosure: str = "" - pub_date: Annotated[str, Alias("pubDate")] = "" - guid: str = "" + # enclosure: str = "" + # pub_date: Annotated[str, Alias("pubDate")] = "" + # guid: str = "" # chapters duration: float = 0.0 @@ -425,7 +425,7 @@ class ABSPodcastMinified(_ABSPodcastBase): """ABSPodcastMinified.""" metadata: ABSPodcastMetadataMinified - size: int # bytes + # size: int # bytes num_episodes: Annotated[int, Alias("numEpisodes")] = 0 @@ -452,8 +452,8 @@ class _ABSBookMetadataBase(BaseModel): published_date: Annotated[str | None, Alias("publishedDate")] publisher: str | None description: str | None - isbn: str | None - asin: str | None + # isbn: str | None + # asin: str | None language: str | None explicit: bool @@ -472,9 +472,9 @@ class ABSBookMetadataMinified(_ABSBookMetadataBase): """ABSBookMetadataMinified.""" # these are normally there - title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")] + # title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")] author_name: Annotated[str, Alias("authorName")] - author_name_lf: Annotated[str, Alias("authorNameLF")] + # author_name_lf: Annotated[str, Alias("authorNameLF")] narrator_name: Annotated[str, Alias("narratorName")] series_name: Annotated[str, Alias("seriesName")] @@ -511,11 +511,11 @@ class ABSBookMinified(_ABSBookBase): """ABSBookBase.""" metadata: ABSBookMetadataMinified - num_tracks: Annotated[int, Alias("numTracks")] - num_audiofiles: Annotated[int, Alias("numAudioFiles")] + # num_tracks: Annotated[int, Alias("numTracks")] + # num_audiofiles: Annotated[int, Alias("numAudioFiles")] num_chapters: Annotated[int, Alias("numChapters")] duration: float # in s - size: int # in bytes + # size: int # in bytes # ebookFormat @@ -539,19 +539,19 @@ class _ABSLibraryItemBase(BaseModel): """_ABSLibraryItemBase.""" id_: Annotated[str, Alias("id")] - ino: str - library_id: Annotated[str, Alias("libraryId")] - folder_id: Annotated[str, Alias("folderId")] - path: str - relative_path: Annotated[str, Alias("relPath")] - is_file: Annotated[bool, Alias("isFile")] - last_modified_ms: Annotated[int, Alias("mtimeMs")] # epoch - last_changed_ms: Annotated[int, Alias("ctimeMs")] # epoch - birthtime_ms: Annotated[int, Alias("birthtimeMs")] # epoch - added_at: Annotated[int, Alias("addedAt")] # ms epoch - updated_at: Annotated[int, Alias("updatedAt")] # ms epoch - is_missing: Annotated[bool, Alias("isMissing")] - is_invalid: Annotated[bool, Alias("isInvalid")] + # ino: str + # library_id: Annotated[str, Alias("libraryId")] + # folder_id: Annotated[str, Alias("folderId")] + # path: str + # relative_path: Annotated[str, Alias("relPath")] + # is_file: Annotated[bool, Alias("isFile")] + # last_modified_ms: Annotated[int, Alias("mtimeMs")] # epoch + # last_changed_ms: Annotated[int, Alias("ctimeMs")] # epoch + # birthtime_ms: Annotated[int, Alias("birthtimeMs")] # epoch + # added_at: Annotated[int, Alias("addedAt")] # ms epoch + # updated_at: Annotated[int, Alias("updatedAt")] # ms epoch + # is_missing: Annotated[bool, Alias("isMissing")] + # is_invalid: Annotated[bool, Alias("isInvalid")] media_type: Annotated[str, Alias("mediaType")] @@ -559,8 +559,8 @@ class _ABSLibraryItemBase(BaseModel): class _ABSLibraryItem(_ABSLibraryItemBase): """ABSLibraryItem.""" - last_scan: Annotated[int | None, Alias("lastScan")] # ms epoch - scan_version: Annotated[str | None, Alias("scanVersion")] + # last_scan: Annotated[int | None, Alias("lastScan")] # ms epoch + # scan_version: Annotated[str | None, Alias("scanVersion")] # libraryFiles @@ -641,9 +641,9 @@ class ABSSeriesBooks(_ABSSeriesBase): """ABSSeriesBooks.""" added_at: Annotated[int, Alias("addedAt")] # ms epoch - name_ignore_prefix: Annotated[str, Alias("nameIgnorePrefix")] - name_ignore_prefix_sort: Annotated[str, Alias("nameIgnorePrefixSort")] - type_: Annotated[str, Alias("type")] + # name_ignore_prefix: Annotated[str, Alias("nameIgnorePrefix")] + # name_ignore_prefix_sort: Annotated[str, Alias("nameIgnorePrefixSort")] + # type_: Annotated[str, Alias("type")] books: list[ABSLibraryItemBookSeries] total_duration: Annotated[float, Alias("totalDuration")] # s From af46c1368605883dae17401c38218ddaf7814840 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:55:25 +0100 Subject: [PATCH 13/16] optionally hide podcasts without episodes --- .../providers/audiobookshelf/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index 16ca67f3d..bc06f1748 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -55,6 +55,8 @@ CONF_USERNAME = "username" CONF_PASSWORD = "password" CONF_VERIFY_SSL = "verify_ssl" +# optionally hide podcasts with no episodes +CONF_HIDE_EMPTY_PODCASTS = "hide_empty_podcasts" async def setup( @@ -109,6 +111,15 @@ async def get_config_entries( category="advanced", default_value=True, ), + ConfigEntry( + key=CONF_HIDE_EMPTY_PODCASTS, + type=ConfigEntryType.BOOLEAN, + label="Hide empty podcasts.", + required=False, + description="This will skip podcasts with no episodes associated.", + category="advanced", + default_value=False, + ), ) @@ -283,6 +294,11 @@ async def get_library_podcasts(self) -> AsyncGenerator[Podcast, None]: """Retrieve library/subscribed podcasts from the provider.""" async for abs_podcast in self._client.get_all_podcasts_minified(): mass_podcast = self._parse_podcast(abs_podcast) + if ( + bool(self.config.get_value(CONF_HIDE_EMPTY_PODCASTS)) + and mass_podcast.total_episodes == 0 + ): + continue yield mass_podcast async def get_podcast(self, prov_podcast_id: str) -> Podcast: From 5028eb867a533f0d8a08f74ba8d37eed77134b71 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:11:33 +0100 Subject: [PATCH 14/16] debug logging, exception handling for mashumaro --- .../providers/audiobookshelf/abs_client.py | 100 +++++++++++------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index 5de9c2426..10b1dec51 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -10,6 +10,7 @@ from typing import Any from aiohttp import ClientSession +from mashumaro.exceptions import InvalidFieldValue, MissingField from music_assistant_models.media_items import UniqueList from music_assistant.providers.audiobookshelf.abs_schema import ( @@ -23,10 +24,8 @@ ABSLibraryItemMinifiedPodcast, ABSLoginResponse, ABSMediaProgress, - ABSPlaybackSession, ABSPlaybackSessionExpanded, ABSPlayRequest, - ABSSessionsResponse, ABSSessionUpdate, ABSUser, ) @@ -162,7 +161,11 @@ async def get_authenticated_user(self) -> ABSUser: async def sync(self) -> None: """Update available book and podcast libraries.""" data = await self._get("libraries") - libraries = ABSLibrariesResponse.from_json(data) + try: + libraries = ABSLibrariesResponse.from_json(data) + except (MissingField, InvalidFieldValue) as exc: + self.logger.warning(exc) + return ids = [x.id_ for x in self.audiobook_libraries] ids.extend([x.id_ for x in self.podcast_libraries]) for library in libraries.libraries: @@ -204,7 +207,13 @@ async def get_all_podcasts_by_library_minified( ) -> AsyncGenerator[ABSLibraryItemMinifiedPodcast]: """Get all podcasts in a library.""" async for podcast_data in self._get_lib_items(lib): - podcast_list = ABSLibrariesItemsMinifiedPodcastResponse.from_json(podcast_data).results + try: + podcast_list = ABSLibrariesItemsMinifiedPodcastResponse.from_json( + podcast_data + ).results + except (MissingField, InvalidFieldValue) as exc: + self.logger.warning(exc) + return if not podcast_list: # [] if page exceeds return @@ -217,7 +226,12 @@ async def get_podcast_expanded(self, id_: str) -> ABSLibraryItemExpandedPodcast: """Get a single Podcast by ID.""" # this endpoint gives more podcast extra data data = await self._get(f"items/{id_}?expanded=1") - return ABSLibraryItemExpandedPodcast.from_json(data) + try: + abs_podcast = ABSLibraryItemExpandedPodcast.from_json(data) + except (MissingField, InvalidFieldValue) as exc: + self.logger.warning(exc) + raise RuntimeError from exc + return abs_podcast async def _get_progress_ms( self, @@ -227,7 +241,11 @@ async def _get_progress_ms( if not data: # entry doesn't exist, so it wasn't played yet return 0, False - abs_media_progress = ABSMediaProgress.from_json(data) + try: + abs_media_progress = ABSMediaProgress.from_json(data) + except (MissingField, InvalidFieldValue) as exc: + self.logger.warning(exc) + return None, False return ( int(abs_media_progress.current_time * 1000), @@ -317,7 +335,13 @@ async def get_all_audiobooks_by_library_minified( ) -> AsyncGenerator[ABSLibraryItemMinifiedBook]: """Get all Audiobooks in a library.""" async for audiobook_data in self._get_lib_items(lib): - audiobook_list = ABSLibrariesItemsMinifiedBookResponse.from_json(audiobook_data).results + try: + audiobook_list = ABSLibrariesItemsMinifiedBookResponse.from_json( + audiobook_data + ).results + except (MissingField, InvalidFieldValue) as exc: + self.logger.warning(exc) + return if not audiobook_list: # [] if page exceeds return @@ -330,7 +354,12 @@ async def get_audiobook_expanded(self, id_: str) -> ABSLibraryItemExpandedBook: """Get a single Audiobook by ID.""" # this endpoint gives more audiobook extra data audiobook = await self._get(f"items/{id_}?expanded=1") - return ABSLibraryItemExpandedBook.from_json(audiobook) + try: + abs_book = ABSLibraryItemExpandedBook.from_json(audiobook) + except (MissingField, InvalidFieldValue) as exc: + self.logger.warning(exc) + raise RuntimeError from exc + return abs_book async def get_playback_session_podcast( self, device_info: ABSDeviceInfo, podcast_id: str, episode_id: str @@ -359,14 +388,6 @@ async def get_playback_session_audiobook( device_info.device_id += f"/{audiobook_id}" return await self._get_playback_session(endpoint, device_info=device_info) - async def get_open_playback_session(self, session_id: str) -> ABSPlaybackSessionExpanded | None: - """Return open playback session.""" - data = await self._get(f"session/{session_id}") - if data: - return ABSPlaybackSessionExpanded.from_json(data) - else: - return None - async def _get_playback_session( self, endpoint: str, device_info: ABSDeviceInfo ) -> ABSPlaybackSessionExpanded: @@ -383,7 +404,12 @@ async def _get_playback_session( supported_mime_types=[], ) data = await self._post(endpoint, data=play_request.to_dict()) - session = ABSPlaybackSessionExpanded.from_json(data) + try: + session = ABSPlaybackSessionExpanded.from_json(data) + except (MissingField, InvalidFieldValue) as exc: + self.logger.warning(exc) + raise RuntimeError from exc + self.logger.debug( f"Got playback session {session.id_} " f"for {session.media_type} named {session.display_title}" @@ -403,26 +429,26 @@ async def sync_playback_session( """Sync an open playback session.""" await self._post(f"session/{playback_session_id}/sync", data=update.to_dict()) - async def get_all_closed_playback_sessions(self) -> AsyncGenerator[ABSPlaybackSession]: - """Get library items with pagination. - - This returns only sessions, which are already closed. - """ - page_cnt = 0 - while True: - data = await self._get( - "me/listening-sessions", - params={"itemsPerPage": LIMIT_ITEMS_PER_PAGE, "page": page_cnt}, - ) - page_cnt += 1 - - sessions = ABSSessionsResponse.from_json(data).sessions - # self.logger.debug([session.device_info for session in sessions]) - if sessions: - for session in sessions: - yield session - else: - return + # async def get_all_closed_playback_sessions(self) -> AsyncGenerator[ABSPlaybackSession]: + # """Get library items with pagination. + # + # This returns only sessions, which are already closed. + # """ + # page_cnt = 0 + # while True: + # data = await self._get( + # "me/listening-sessions", + # params={"itemsPerPage": LIMIT_ITEMS_PER_PAGE, "page": page_cnt}, + # ) + # page_cnt += 1 + # + # sessions = ABSSessionsResponse.from_json(data).sessions + # # self.logger.debug([session.device_info for session in sessions]) + # if sessions: + # for session in sessions: + # yield session + # else: + # return async def close_all_playback_sessions(self) -> None: """Cleanup all playback sessions opened by us.""" From b4fcb0a736e35b17ee08adbeaf46528ad9975a7e Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:32:18 +0100 Subject: [PATCH 15/16] small delay when streaming via a session --- music_assistant/providers/audiobookshelf/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index bc06f1748..6e63f474f 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -5,6 +5,7 @@ from __future__ import annotations +import asyncio from collections.abc import AsyncGenerator, Sequence from typing import TYPE_CHECKING @@ -458,6 +459,8 @@ async def get_stream_details( session = await self._client.get_playback_session_audiobook( device_info=self.device_info, audiobook_id=item_id ) + # small delay, allow abs to launch ffmpeg process + await asyncio.sleep(1) return await self.get_streamdetails_from_playback_session(session) return await self._get_stream_details_audiobook(abs_audiobook) raise MediaNotFoundError("Stream unknown") From 905efdd90aa0df6d3df9c8032d8efc8c99fb2707 Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:33:54 +0100 Subject: [PATCH 16/16] make exception in schema parsing an error --- .../providers/audiobookshelf/abs_client.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/music_assistant/providers/audiobookshelf/abs_client.py b/music_assistant/providers/audiobookshelf/abs_client.py index 10b1dec51..2a5826f7f 100644 --- a/music_assistant/providers/audiobookshelf/abs_client.py +++ b/music_assistant/providers/audiobookshelf/abs_client.py @@ -164,7 +164,7 @@ async def sync(self) -> None: try: libraries = ABSLibrariesResponse.from_json(data) except (MissingField, InvalidFieldValue) as exc: - self.logger.warning(exc) + self.logger.error(exc) return ids = [x.id_ for x in self.audiobook_libraries] ids.extend([x.id_ for x in self.podcast_libraries]) @@ -212,7 +212,7 @@ async def get_all_podcasts_by_library_minified( podcast_data ).results except (MissingField, InvalidFieldValue) as exc: - self.logger.warning(exc) + self.logger.error(exc) return if not podcast_list: # [] if page exceeds return @@ -229,7 +229,7 @@ async def get_podcast_expanded(self, id_: str) -> ABSLibraryItemExpandedPodcast: try: abs_podcast = ABSLibraryItemExpandedPodcast.from_json(data) except (MissingField, InvalidFieldValue) as exc: - self.logger.warning(exc) + self.logger.error(exc) raise RuntimeError from exc return abs_podcast @@ -244,7 +244,7 @@ async def _get_progress_ms( try: abs_media_progress = ABSMediaProgress.from_json(data) except (MissingField, InvalidFieldValue) as exc: - self.logger.warning(exc) + self.logger.error(exc) return None, False return ( @@ -340,7 +340,7 @@ async def get_all_audiobooks_by_library_minified( audiobook_data ).results except (MissingField, InvalidFieldValue) as exc: - self.logger.warning(exc) + self.logger.error(exc) return if not audiobook_list: # [] if page exceeds return @@ -357,7 +357,7 @@ async def get_audiobook_expanded(self, id_: str) -> ABSLibraryItemExpandedBook: try: abs_book = ABSLibraryItemExpandedBook.from_json(audiobook) except (MissingField, InvalidFieldValue) as exc: - self.logger.warning(exc) + self.logger.error(exc) raise RuntimeError from exc return abs_book @@ -407,7 +407,7 @@ async def _get_playback_session( try: session = ABSPlaybackSessionExpanded.from_json(data) except (MissingField, InvalidFieldValue) as exc: - self.logger.warning(exc) + self.logger.error(exc) raise RuntimeError from exc self.logger.debug(