Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions music_assistant/controllers/media/audiobooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from music_assistant_models.enums import MediaType, ProviderFeature
from music_assistant_models.media_items import Audiobook, ProviderMapping, UniqueList
from music_assistant_models.media_items.helpers import AudiobookCollection

from music_assistant.constants import DB_TABLE_AUDIOBOOKS, DB_TABLE_PLAYLOG
from music_assistant.controllers.media.base import MediaControllerBase
Expand Down Expand Up @@ -63,6 +64,7 @@ def __init__(self, mass: MusicAssistant) -> None:
# register (extra) api handlers
api_base = self.api_base
self.mass.register_api_command(f"music/{api_base}/audiobook_versions", self.versions)
self.mass.register_api_command(f"music/{api_base}/collections", self.collections)

async def library_items(
self,
Expand All @@ -73,6 +75,7 @@ async def library_items(
order_by: str = "sort_name",
provider: str | list[str] | None = None,
genre: int | list[int] | None = None,
without_collections: bool | None = None,
**kwargs: Any,
) -> list[Audiobook]:
"""Get in-database audiobooks.
Expand All @@ -84,12 +87,18 @@ async def library_items(
:param order_by: Order by field (e.g. 'sort_name', 'timestamp_added').
:param provider: Filter by provider instance ID (single string or list).
:param genre: Filter by genre id(s).
:param without_collections: Do not return audiobooks which are part of a collection
"""
extra_query_params: dict[str, Any] = {}
extra_query_parts: list[str] = []
extra_join_parts: list[str] = []
if session_user := get_current_user():
extra_join_parts = [f"AND playlog.userid = '{session_user.user_id}'"]
if without_collections:
extra_query_parts = [
"WHERE (json_extract(audiobooks.metadata, '$.collections') IS NULL "
"OR json_extract(audiobooks.metadata, '$.collections') = '[]')",
]
result = await self.get_library_items_by_query(
favorite=favorite,
search=search,
Expand Down Expand Up @@ -371,3 +380,55 @@ async def _set_playlog(self, db_id: int, media_item: Audiobook) -> None:
},
allow_replace=True,
)

async def collections(
self,
) -> list[AudiobookCollection]:
"""Get all available audiobook collections.

:param limit: Maximum number of items to return.
:param offset: Number of items to skip.
"""
# key is the collections' title
collections_dict: dict[str, list[Audiobook]] = {}
audiobooks_with_collections = await self.get_library_items_by_query(
extra_query_parts=[
"WHERE json_extract(audiobooks.metadata, '$.collections') IS NOT NULL "
"AND json_extract(audiobooks.metadata, '$.collections') != '[]'",
],
)
for audiobook in audiobooks_with_collections:
if audiobook.metadata.collections is None:
# this should never happen
continue
for collection_info in audiobook.metadata.collections:
audiobook_list = collections_dict.get(collection_info.title, [])
audiobook_list.append(audiobook)
collections_dict[collection_info.title] = audiobook_list

result: list[AudiobookCollection] = []
# Sort collections, first by number then alphabetically
for collection_title, audiobook_list in collections_dict.items():
audiobooks_with_number: list[tuple[Audiobook, float]] = []
audiobooks_with_string: list[tuple[Audiobook, str]] = []
audiobooks_with_none: list[Audiobook] = []
for audiobook in audiobook_list:
assert audiobook.metadata.collections is not None # for type checking
collection_info = next(
x for x in audiobook.metadata.collections if x.title == collection_title
)
if collection_info.sequence is None:
audiobooks_with_none.append(audiobook)
continue
try:
sort_by = float(collection_info.sequence)
audiobooks_with_number.append((audiobook, sort_by))
except ValueError:
audiobooks_with_string.append((audiobook, str(collection_info.sequence)))
final_list = [x[0] for x in sorted(audiobooks_with_number, key=lambda x: x[1])]
final_list.extend([x[0] for x in sorted(audiobooks_with_string, key=lambda x: x[1])])
final_list.extend(audiobooks_with_none)

result.append(AudiobookCollection(title=collection_title, audiobooks=final_list))

return result
14 changes: 13 additions & 1 deletion music_assistant/providers/audiobookshelf/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
AudioFormat,
ItemMapping,
MediaItemChapter,
MediaItemCollection,
MediaItemImage,
ProviderMapping,
UniqueList,
Expand Down Expand Up @@ -254,7 +255,7 @@ def parse_podcast_episode(

def parse_audiobook(
*,
abs_audiobook: AbsLibraryItemExpandedBook | AbsLibraryItemMinifiedBook,
abs_audiobook: AbsLibraryItemExpandedBook,
instance_id: str,
domain: str,
token: str | None,
Expand Down Expand Up @@ -299,6 +300,17 @@ def parse_audiobook(
year=int(abs_audiobook.media.metadata.published_year), month=1, day=1
)

book_series: list[MediaItemCollection] = []
for abs_series_sequence in abs_audiobook.media.metadata.series:
book_series.append(
MediaItemCollection(
title=abs_series_sequence.name, sequence=abs_series_sequence.sequence
)
)

if book_series:
mass_audiobook.metadata.collections = UniqueList(book_series)

if abs_audiobook.media.metadata.genres is not None:
mass_audiobook.metadata.genres = set(abs_audiobook.media.metadata.genres)

Expand Down