Chore: Audiobookshelf - adapt schema to reflect the naming scheme used in the API...
authorFabian Munkes <105975993+fmunkes@users.noreply.github.com>
Tue, 21 Jan 2025 21:20:14 +0000 (22:20 +0100)
committerGitHub <noreply@github.com>
Tue, 21 Jan 2025 21:20:14 +0000 (22:20 +0100)
music_assistant/providers/audiobookshelf/__init__.py
music_assistant/providers/audiobookshelf/abs_client.py
music_assistant/providers/audiobookshelf/abs_schema.py

index eae17c60987394dee9a1639d27415b6b2b40d9e4..353a8fe6cbf8b3a30ff97578b6207a94a2ddef5f 100644 (file)
@@ -36,11 +36,11 @@ 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_schema import (
-    ABSAudioBook,
     ABSDeviceInfo,
     ABSLibrary,
+    ABSLibraryItemExpandedBook,
+    ABSLibraryItemExpandedPodcast,
     ABSPlaybackSessionExpanded,
-    ABSPodcast,
     ABSPodcastEpisodeExpanded,
 )
 
@@ -174,7 +174,7 @@ class Audiobookshelf(MusicProvider):
         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 @@ class Audiobookshelf(MusicProvider):
             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 @@ class Audiobookshelf(MusicProvider):
             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 @@ class Audiobookshelf(MusicProvider):
             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 @@ class Audiobookshelf(MusicProvider):
         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"
index b4fa79ca5aa4530f5ecc737095ce6cffcc21bd57..37598815a74bc50f7a4644a8056db77b6c016771 100644 (file)
@@ -12,18 +12,20 @@ from aiohttp import ClientSession
 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,31 +165,42 @@ class ABSClient:
                     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):
                 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
             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 +208,11 @@ class ABSClient:
                 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 +301,24 @@ class ABSClient:
         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 +326,11 @@ class ABSClient:
                 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
index 687ed05edaaebe2b0acac1b1d03b77555e207ede..c4a71a7ffeffa40b5209bd18bc0238e86f2c703c 100644 (file)
@@ -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,12 +37,12 @@ class BaseModel(DataClassJSONMixin):
 
 @dataclass
 class ABSAudioTrack(BaseModel):
-    """ABS audioTrack.
+    """ABS audioTrack. No variants.
 
     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 = ""
@@ -43,167 +52,167 @@ class ABSAudioTrack(BaseModel):
 
 
 @dataclass
-class ABSPodcastEpisodeExpanded(BaseModel):
-    """ABSPodcastEpisode.
+class ABSBookChapter(BaseModel):
+    """
+    ABSBookChapter. No variants.
 
-    https://api.audiobookshelf.org/#podcast-episode
+    https://api.audiobookshelf.org/#book-chapter
     """
 
-    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
+    id_: Annotated[int, Alias("id")]
+    start: float
+    end: float
+    title: str
 
 
 @dataclass
-class ABSPodcastMetaData(BaseModel):
-    """PodcastMetaData https://api.audiobookshelf.org/?shell#podcasts."""
+class ABSAudioBookmark(BaseModel):
+    """ABSAudioBookmark. No variants.
 
-    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")]
+    https://api.audiobookshelf.org/#audio-bookmark
+    """
+
+    library_item_id: Annotated[str, Alias("libraryItemId")]
+    title: str
+    time: float  # seconds
+    created_at: Annotated[int, Alias("createdAt")]  # unix epoch ms
 
 
 @dataclass
-class ABSPodcastMedia(BaseModel):
-    """ABSPodcastMedia."""
+class ABSUserPermissions(BaseModel):
+    """ABSUserPermissions. No variants.
 
-    metadata: ABSPodcastMetaData
-    cover_path: Annotated[str, Alias("coverPath")]
-    episodes: list[ABSPodcastEpisodeExpanded]
-    num_episodes: Annotated[int, Alias("numEpisodes")] = 0
+    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")]
 
 
 @dataclass
-class ABSPodcast(BaseModel):
-    """ABSPodcast.
+class ABSLibrary(BaseModel):
+    """ABSLibrary. No variants.
 
-    Depending on endpoint we get different results. This class does not
-    fully reflect https://api.audiobookshelf.org/#podcast.
+    https://api.audiobookshelf.org/#library
+    Only attributes we need
     """
 
     id_: Annotated[str, Alias("id")]
-    media: ABSPodcastMedia
+    name: str
+    # folders
+    # displayOrder: Integer
+    # icon: String
+    media_type: Annotated[str, Alias("mediaType")]
+    provider: str
+    # settings
+    created_at: Annotated[int, Alias("createdAt")]
+    last_update: Annotated[int, Alias("lastUpdate")]
 
 
 @dataclass
-class ABSAuthorMinified(BaseModel):
-    """ABSAuthor.
+class ABSDeviceInfo(BaseModel):
+    """ABSDeviceInfo. No variants.
 
-    https://api.audiobookshelf.org/#author
+    https://api.audiobookshelf.org/#device-info-parameters
+    https://api.audiobookshelf.org/#device-info
+    https://github.com/advplyr/audiobookshelf/blob/master/server/objects/DeviceInfo.js#L3
     """
 
-    id_: Annotated[str, Alias("id")]
-    name: str
+    device_id: Annotated[str, Alias("deviceId")] = ""
+    client_name: Annotated[str, Alias("clientName")] = ""
+    client_version: Annotated[str, Alias("clientVersion")] = ""
+    manufacturer: str = ""
+    model: str = ""
+    # sdkVersion # meant for an Android client
+
+
+### Author: https://api.audiobookshelf.org/#author
 
 
 @dataclass
-class ABSSeriesSequence(BaseModel):
-    """Series Sequence.
+class ABSAuthorMinified(BaseModel):
+    """ABSAuthorMinified.
 
-    https://api.audiobookshelf.org/#series
+    https://api.audiobookshelf.org/#author
     """
 
     id_: Annotated[str, Alias("id")]
     name: str
-    sequence: str | None
 
 
 @dataclass
-class ABSAudioBookMetaData(BaseModel):
-    """ABSAudioBookMetaData.
-
-    https://api.audiobookshelf.org/#book-metadata
-    """
+class ABSAuthor(ABSAuthorMinified):
+    """ABSAuthor."""
 
-    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
+    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 ABSAudioBookChapter(BaseModel):
-    """
-    ABSAudioBookChapter.
+class ABSAuthorExpanded(ABSAuthor):
+    """ABSAuthorExpanded."""
 
-    https://api.audiobookshelf.org/#book-chapter
-    """
+    num_books: Annotated[int, Alias("numBooks")]
 
-    id_: Annotated[int, Alias("id")]
-    start: float
-    end: float
-    title: str
+
+### Series: https://api.audiobookshelf.org/#series
 
 
 @dataclass
-class ABSAudioBookMedia(BaseModel):
-    """ABSAudioBookMedia.
+class _ABSSeriesBase(BaseModel):
+    """_ABSSeriesBase."""
 
-    Helper class due to API endpoint used.
-    """
+    id_: Annotated[str, Alias("id")]
+    name: str
 
-    metadata: ABSAudioBookMetaData
-    cover_path: Annotated[str, Alias("coverPath")]
-    chapters: list[ABSAudioBookChapter]
-    duration: float
-    tracks: list[ABSAudioTrack]
+
+@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 ABSAudioBook(BaseModel):
-    """ABSAudioBook.
+class ABSSeriesSequence(BaseModel):
+    """Series Sequence.
 
-    Depending on endpoint we get different results. This class does not
-    full reflect https://api.audiobookshelf.org/#book.
+    https://api.audiobookshelf.org/#series
     """
 
     id_: Annotated[str, Alias("id")]
-    media: ABSAudioBookMedia
+    name: str
+    sequence: str | None
+
+
+# another variant, ABSSeriesBooks is further down
+
+
+###  https://api.audiobookshelf.org/#media-progress
 
 
 @dataclass
 class ABSMediaProgress(BaseModel):
-    """ABSMediaProgress.
-
-    https://api.audiobookshelf.org/#media-progress
-    """
+    """ABSMediaProgress."""
 
     id_: Annotated[str, Alias("id")]
     library_item_id: Annotated[str, Alias("libraryItemId")]
@@ -218,27 +227,7 @@ class ABSMediaProgress(BaseModel):
     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
-    created_at: Annotated[int, Alias("createdAt")]  # unix epoch ms
-
-
-@dataclass
-class ABSUserPermissions(BaseModel):
-    """ABSUserPermissions."""
-
-    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")]
+# two additional progress variants, 'with media' book and podcast, further down.
 
 
 @dataclass
@@ -269,91 +258,7 @@ class ABSUser(BaseModel):
     # 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.
-
-    Only attributes we need
-    """
-
-    id_: Annotated[str, Alias("id")]
-    name: str
-    # folders
-    # displayOrder: Integer
-    # icon: String
-    media_type: Annotated[str, Alias("mediaType")]
-    provider: str
-    # settings
-    created_at: Annotated[int, Alias("createdAt")]
-    last_update: Annotated[int, Alias("lastUpdate")]
-
-
-@dataclass
-class ABSLibrariesResponse(BaseModel):
-    """ABSLibrariesResponse."""
-
-    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):
-    """ABSDeviceInfo.
-
-    https://api.audiobookshelf.org/#device-info-parameters
-    https://api.audiobookshelf.org/#device-info
-    https://github.com/advplyr/audiobookshelf/blob/master/server/objects/DeviceInfo.js#L3
-    """
-
-    device_id: Annotated[str, Alias("deviceId")] = ""
-    client_name: Annotated[str, Alias("clientName")] = ""
-    client_version: Annotated[str, Alias("clientVersion")] = ""
-    manufacturer: str = ""
-    model: str = ""
-    # sdkVersion # meant for an Android client
-
-
-@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"
+# two additional user variants do exist
 
 
 class ABSPlayMethod(Enum):
@@ -365,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")]
@@ -401,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")]
 
@@ -412,18 +314,373 @@ class ABSPlaybackSessionExpanded(ABSPlaybackSession):
     # libraryItem:
 
 
+### https://api.audiobookshelf.org/#podcast-metadata
+
+
 @dataclass
-class ABSSessionUpdate(BaseModel):
-    """
-    ABSSessionUpdate.
+class ABSPodcastMetadata(BaseModel):
+    """ABSPodcastMetadata."""
 
-    Can be used as optional data to sync or closing request.
-    unit is seconds
+    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(ABSPodcastMetadata):
+    """ABSPodcastMetadataMinified."""
+
+    title_ignore_prefix: Annotated[str, Alias("titleIgnorePrefix")]
+
+
+ABSPodcastMetaDataExpanded = ABSPodcastMetadataMinified
+
+### https://api.audiobookshelf.org/#podcast-episode
+
+
+@dataclass
+class ABSPodcastEpisode(BaseModel):
+    """ABSPodcastEpisode."""
+
+    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 ABSPodcastEpisodeExpanded(BaseModel):
+    """ABSPodcastEpisode.
+
+    https://api.audiobookshelf.org/#podcast-episode
     """
 
-    current_time: Annotated[float, Alias("currentTime")]
-    time_listened: Annotated[float, Alias("timeListened")]
+    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):
+    """_ABSPodcastBase."""
+
+    cover_path: Annotated[str, Alias("coverPath")]
+
+
+### https://api.audiobookshelf.org/#podcast
+
+
+@dataclass
+class ABSPodcast(_ABSPodcastBase):
+    """ABSPodcast."""
+
+    metadata: ABSPodcastMetadata
+    library_item_id: Annotated[str, Alias("libraryItemId")]
+    tags: list[str]
+    episodes: list[ABSPodcastEpisode]
+
+
+@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]
+
+
+### https://api.audiobookshelf.org/#book-metadata
+
+
+@dataclass
+class _ABSBookMetadataBase(BaseModel):
+    """_ABSBookMetadataBase."""
+
+    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 ABSBookMetadata(_ABSBookMetadataBase):
+    """ABSBookMetadata."""
+
+    authors: list[ABSAuthorMinified]
+    narrators: list[str]
+    series: list[ABSSeriesSequence]
+
+
+@dataclass
+class ABSBookMetadataMinified(_ABSBookMetadataBase):
+    """ABSBookMetadataMinified."""
+
+    # 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 ABSBookMetadataExpanded(ABSBookMetadata, ABSBookMetadataMinified):
+    """ABSAudioBookMetaDataExpanded."""
+
+
+### https://api.audiobookshelf.org/#book
+
+
+@dataclass
+class _ABSBookBase(BaseModel):
+    """_ABSBookBase."""
+
+    tags: list[str]
+    cover_path: Annotated[str | None, Alias("coverPath")]
+
+
+@dataclass
+class ABSBook(_ABSBookBase):
+    """ABSBook."""
+
+    library_item_id: Annotated[str, Alias("libraryItemId")]
+    metadata: ABSBookMetadata
+    # audioFiles
+    chapters: list[ABSBookChapter]
+    # ebookFile
+
+
+@dataclass
+class ABSBookMinified(_ABSBookBase):
+    """ABSBookBase."""
+
+    metadata: ABSBookMetadataMinified
+    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 ABSBookExpanded(_ABSBookBase):
+    """ABSBookExpanded."""
+
+    library_item_id: Annotated[str, Alias("libraryItemId")]
+    metadata: ABSBookMetadataExpanded
+    chapters: list[ABSBookChapter]
     duration: float
+    size: int  # bytes
+    tracks: list[ABSAudioTrack]
+
+
+### https://api.audiobookshelf.org/#library-item
+
+
+@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 _ABSLibraryItem(_ABSLibraryItemBase):
+    """ABSLibraryItem."""
+
+    last_scan: Annotated[int | None, Alias("lastScan")]  # ms epoch
+    scan_version: Annotated[str | None, Alias("scanVersion")]
+    # libraryFiles
+
+
+@dataclass
+class ABSLibraryItemBook(_ABSLibraryItem):
+    """ABSLibraryItemBook."""
+
+    media: ABSBook
+
+
+@dataclass
+class ABSLibraryItemBookSeries(ABSLibraryItemBook):
+    """ABSLibraryItemNormalBookSeries.
+
+    Special class, when having the scheme of SeriesBooks, see
+    https://api.audiobookshelf.org/#series, it gets an extra
+    sequence key.
+    """
+
+    sequence: int
+
+
+@dataclass
+class ABSLibraryItemPodcast(_ABSLibraryItem):
+    """ABSLibraryItemPodcast."""
+
+    media: ABSPodcast
+
+
+@dataclass
+class _ABSLibraryItemMinified(_ABSLibraryItemBase):
+    """ABSLibraryItemMinified."""
+
+    num_files: Annotated[int, Alias("numFiles")]
+    size: int  # bytes
+
+
+@dataclass
+class ABSLibraryItemMinifiedBook(_ABSLibraryItemMinified):
+    """ABSLibraryItemMinifiedBook."""
+
+    media: ABSBookMinified
+
+
+@dataclass
+class ABSLibraryItemMinifiedPodcast(_ABSLibraryItemMinified):
+    """ABSLibraryItemMinifiedBook."""
+
+    media: ABSPodcastMinified
+
+
+@dataclass
+class _ABSLibraryItemExpanded(_ABSLibraryItemBase):
+    """ABSLibraryItemExpanded."""
+
+    size: int  # bytes
+
+
+@dataclass
+class ABSLibraryItemExpandedBook(_ABSLibraryItemExpanded):
+    """ABSLibraryItemExpanded."""
+
+    media: ABSBookExpanded
+
+
+@dataclass
+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
@@ -434,3 +691,59 @@ class ABSSessionsResponse(BaseModel):
     num_pages: Annotated[int, Alias("numPages")]
     items_per_page: Annotated[int, Alias("itemsPerPage")]
     sessions: list[ABSPlaybackSession]
+
+
+@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]
+
+
+### 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