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
9 changes: 8 additions & 1 deletion music_assistant_models/media_items/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@
RecommendationFolder,
Track,
)
from .metadata import MediaItemChapter, MediaItemImage, MediaItemLink, MediaItemMetadata
from .metadata import (
MediaItemChapter,
MediaItemImage,
MediaItemLink,
MediaItemMetadata,
MediaItemSeries,
)
from .provider_mapping import ProviderMapping

__all__ = [
Expand All @@ -46,6 +52,7 @@
"MediaItemImage",
"MediaItemLink",
"MediaItemMetadata",
"MediaItemSeries",
"MediaItemType",
"Metadata",
"MetadataProvider",
Expand Down
19 changes: 19 additions & 0 deletions music_assistant_models/media_items/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""API helper classes for media items."""

from dataclasses import dataclass, field

from mashumaro import DataClassDictMixin

from .media_item import Audiobook


@dataclass
class AudiobookSeries(DataClassDictMixin):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not too sure about this one. It kinda feels like it belongs in media_item.py, but that also makes it its own MediaItem. @marcelveldt what are your thoughts here?

"""An audiobook series as acquired from the database.

This is used as API response, and not to be used by a provider.
"""

title: str
# sorted list of audiobooks in this series
audiobooks: list[Audiobook] = field(default_factory=list)
15 changes: 15 additions & 0 deletions music_assistant_models/media_items/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ def __hash__(self) -> int:
return hash(self.position)


@dataclass(frozen=True, kw_only=True)
class MediaItemSeries(DataClassDictMixin):
"""Model for a MediaItem's series."""

title: str
# sequence is used for sorting
# we will first sort by number, and then alphabetically
sequence: float | str | None = None

def __hash__(self) -> int:
"""Return custom hash."""
return hash(self.title)


@dataclass(kw_only=True)
class MediaItemMetadata(DataClassDictMixin):
"""Model for a MediaItem's metadata."""
Expand All @@ -95,6 +109,7 @@ class MediaItemMetadata(DataClassDictMixin):
# chapters is a list of available chapters, sorted by position
# most commonly used for audiobooks and podcast episodes
chapters: list[MediaItemChapter] | None = None
series: UniqueList[MediaItemSeries] | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a book be part of multiple series?

Copy link
Copy Markdown
Contributor Author

@fmunkes fmunkes Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stumbled across this myself, as I had only a single series previously. I compared that to abs, and it supports multiple series. I googled a bit around, and apparently, there are some occasions where a book can be part of different aspects of e.g. a fantasy story, or you create two series e.g. for published order and chronological order.
It's probably rare, but I think it is easy to support, too.

A naming alternative could be to not name this series, but collection. In the end a series is just a collection of books, which has a defined order, so a sub-type of a collection. Would just be the wording, logic remains the same - what do you think?

# last_refresh: timestamp the (full) metadata was last collected
last_refresh: int | None = None

Expand Down