Skip to content

Commit

Permalink
Fix: Some small fixes for audiobook/podcast support
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Dec 26, 2024
1 parent 73b5a7e commit 698dcd6
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 22 deletions.
14 changes: 8 additions & 6 deletions music_assistant/controllers/media/audiobooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ async def _add_library_item(self, item: Audiobook) -> int:
"external_ids": serialize_to_json(item.external_ids),
"publisher": item.publisher,
"total_chapters": item.total_chapters,
"authors": item.authors,
"narrators": item.narrators,
"authors": serialize_to_json(item.authors),
"narrators": serialize_to_json(item.narrators),
},
)
# update/set provider_mappings table
Expand Down Expand Up @@ -191,10 +191,12 @@ async def _update_library_item(
),
"publisher": cur_item.publisher or update.publisher,
"total_chapters": cur_item.total_chapters or update.total_chapters,
"authors": update.authors if overwrite else cur_item.authors or update.authors,
"narrators": update.narrators
if overwrite
else cur_item.narrators or update.narrators,
"authors": serialize_to_json(
update.authors if overwrite else cur_item.authors or update.authors
),
"narrators": serialize_to_json(
update.narrators if overwrite else cur_item.narrators or update.narrators
),
},
)
# update/set provider_mappings table
Expand Down
18 changes: 8 additions & 10 deletions music_assistant/controllers/music.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,7 +1094,7 @@ async def __migrate_database(self, prev_version: int) -> None:
"Migrating database from version %s to %s", prev_version, DB_SCHEMA_VERSION
)

if prev_version <= 4:
if prev_version <= 6:
# unhandled schema version
# we do not try to handle more complex migrations
self.logger.warning(
Expand Down Expand Up @@ -1163,13 +1163,10 @@ async def __migrate_database(self, prev_version: int) -> None:
await self.database.execute("DROP TABLE IF EXISTS track_loudness")

if prev_version <= 9:
try:
await self.database.execute(
f"ALTER TABLE {DB_TABLE_PODCASTS} ADD COLUMN version TEXT"
)
except Exception as err:
if "duplicate column" not in str(err):
raise
# recreate db tables for audiobooks and podcasts due to some mistakes in early version
await self.database.execute(f"DROP TABLE IF EXISTS {DB_TABLE_AUDIOBOOKS}")
await self.database.execute(f"DROP TABLE IF EXISTS {DB_TABLE_PODCASTS}")
await self.__create_database_tables()

# save changes
await self.database.commit()
Expand Down Expand Up @@ -1290,9 +1287,10 @@ async def __create_database_tables(self) -> None:
[item_id] INTEGER PRIMARY KEY AUTOINCREMENT,
[name] TEXT NOT NULL,
[sort_name] TEXT NOT NULL,
[version] TEXT,
[favorite] BOOLEAN DEFAULT 0,
[publisher] TEXT NOT NULL,
[total_chapters] INTEGER NOT NULL,
[total_chapters] INTEGER,
[authors] json NOT NULL,
[narrators] json NOT NULL,
[metadata] json NOT NULL,
Expand All @@ -1312,7 +1310,7 @@ async def __create_database_tables(self) -> None:
[version] TEXT,
[favorite] BOOLEAN DEFAULT 0,
[publisher] TEXT NOT NULL,
[total_episodes] INTEGER NOT NULL,
[total_episodes] INTEGER,
[metadata] json NOT NULL,
[external_ids] json NOT NULL,
[play_count] INTEGER DEFAULT 0,
Expand Down
206 changes: 200 additions & 6 deletions music_assistant/providers/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
from __future__ import annotations

from collections.abc import AsyncGenerator
from random import randint
from typing import TYPE_CHECKING

from music_assistant_models.config_entries import ConfigEntry
from music_assistant_models.enums import (
ConfigEntryType,
ContentType,
ImageType,
MediaType,
Expand All @@ -15,9 +18,14 @@
from music_assistant_models.media_items import (
Album,
Artist,
Audiobook,
AudioFormat,
Chapter,
Episode,
ItemMapping,
MediaItemImage,
MediaItemMetadata,
Podcast,
ProviderMapping,
Track,
UniqueList,
Expand All @@ -28,7 +36,7 @@
from music_assistant.models.music_provider import MusicProvider

if TYPE_CHECKING:
from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig
from music_assistant_models.config_entries import ConfigValueType, ProviderConfig
from music_assistant_models.provider import ProviderManifest

from music_assistant import MusicAssistant
Expand All @@ -49,6 +57,12 @@
remotely_accessible=False,
)

CONF_KEY_NUM_ARTISTS = "num_artists"
CONF_KEY_NUM_ALBUMS = "num_albums"
CONF_KEY_NUM_TRACKS = "num_tracks"
CONF_KEY_NUM_PODCASTS = "num_podcasts"
CONF_KEY_NUM_AUDIOBOOKS = "num_audiobooks"


async def setup(
mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
Expand All @@ -70,7 +84,48 @@ async def get_config_entries(
action: [optional] action key called from config entries UI.
values: the (intermediate) raw values for config entries sent with the action.
"""
return ()
return (
ConfigEntry(
key=CONF_KEY_NUM_ARTISTS,
type=ConfigEntryType.INTEGER,
label="Number of (test) artists",
description="Number of test artists to generate",
default_value=5,
required=False,
),
ConfigEntry(
key=CONF_KEY_NUM_ALBUMS,
type=ConfigEntryType.INTEGER,
label="Number of (test) albums per artist",
description="Number of test albums to generate per artist",
default_value=5,
required=False,
),
ConfigEntry(
key=CONF_KEY_NUM_TRACKS,
type=ConfigEntryType.INTEGER,
label="Number of (test) tracks per album",
description="Number of test tracks to generate per artist-album",
default_value=20,
required=False,
),
ConfigEntry(
key=CONF_KEY_NUM_PODCASTS,
type=ConfigEntryType.INTEGER,
label="Number of (test) podcasts",
description="Number of test podcasts to generate",
default_value=5,
required=False,
),
ConfigEntry(
key=CONF_KEY_NUM_AUDIOBOOKS,
type=ConfigEntryType.INTEGER,
label="Number of (test) audiobooks",
description="Number of test audiobooks to generate",
default_value=5,
required=False,
),
)


class TestProvider(MusicProvider):
Expand All @@ -84,7 +139,18 @@ def is_streaming_provider(self) -> bool:
@property
def supported_features(self) -> set[ProviderFeature]:
"""Return the features supported by this Provider."""
return {ProviderFeature.LIBRARY_TRACKS}
sup_features = {ProviderFeature.BROWSE}
if self.config.get_value(CONF_KEY_NUM_ARTISTS):
sup_features.add(ProviderFeature.LIBRARY_ARTISTS)
if self.config.get_value(CONF_KEY_NUM_ALBUMS):
sup_features.add(ProviderFeature.LIBRARY_ALBUMS)
if self.config.get_value(CONF_KEY_NUM_TRACKS):
sup_features.add(ProviderFeature.LIBRARY_TRACKS)
if self.config.get_value(CONF_KEY_NUM_PODCASTS):
sup_features.add(ProviderFeature.LIBRARY_PODCASTS)
if self.config.get_value(CONF_KEY_NUM_AUDIOBOOKS):
sup_features.add(ProviderFeature.LIBRARY_AUDIOBOOKS)
return sup_features

async def get_track(self, prov_track_id: str) -> Track:
"""Get full track details by id."""
Expand Down Expand Up @@ -142,14 +208,142 @@ async def get_album(self, prov_album_id: str) -> Album:
metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB])),
)

async def get_podcast(self, prov_podcast_id: str) -> Album:
"""Get full podcast details by id."""
return Podcast(
item_id=prov_podcast_id,
provider=self.instance_id,
name=f"Test Podcast {prov_podcast_id}",
metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB])),
provider_mappings={
ProviderMapping(
item_id=prov_podcast_id,
provider_domain=self.domain,
provider_instance=self.instance_id,
)
},
publisher="Test Publisher",
)

async def get_audiobook(self, prov_audiobook_id: str) -> Audiobook:
"""Get full audiobook details by id."""
return Audiobook(
item_id=prov_audiobook_id,
provider=self.instance_id,
name=f"Test Audiobook {prov_audiobook_id}",
metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB])),
provider_mappings={
ProviderMapping(
item_id=prov_audiobook_id,
provider_domain=self.domain,
provider_instance=self.instance_id,
)
},
publisher="Test Publisher",
total_chapters=10,
)

async def get_chapter(self, prov_chapter_id: str) -> Chapter:
"""Get (full) audiobook chapter details by id."""
prov_audiobook_id, chapter_idx = prov_chapter_id.split("_", 2)
return Chapter(
item_id=prov_chapter_id,
provider=self.instance_id,
name=f"Test Chapter {prov_audiobook_id}-{prov_chapter_id}",
duration=5,
audiobook=ItemMapping(
item_id=prov_audiobook_id,
provider=self.instance_id,
name=f"Test Audiobook {prov_audiobook_id}",
media_type=MediaType.AUDIOBOOK,
),
)

async def get_episode(self, prov_episode_id: str) -> Episode:
"""Get (full) podcast episode details by id."""
prov_podcast_id, episode_idx = prov_episode_id.split("_", 2)
return Episode(
item_id=f"{prov_podcast_id}_{episode_idx}",
provider=self.instance_id,
name=f"Test Episode {prov_podcast_id}-{episode_idx}",
duration=5,
podcast=ItemMapping(
item_id=prov_podcast_id,
provider=self.instance_id,
name=f"Test Podcast {prov_podcast_id}",
media_type=MediaType.PODCAST,
),
provider_mappings={
ProviderMapping(
item_id=f"{prov_podcast_id}_{episode_idx}",
provider_domain=self.domain,
provider_instance=self.instance_id,
)
},
metadata=MediaItemMetadata(images=UniqueList([DEFAULT_THUMB])),
episode_number=episode_idx,
)

async def get_library_artists(self) -> AsyncGenerator[Artist, None]:
"""Retrieve library artists from the provider."""
num_artists = self.config.get_value(CONF_KEY_NUM_ARTISTS)
for artist_idx in range(num_artists):
yield await self.get_artist(str(artist_idx))

async def get_library_albums(self) -> AsyncGenerator[Album, None]:
"""Retrieve library albums from the provider."""
num_artists = self.config.get_value(CONF_KEY_NUM_ARTISTS) or 5
num_albums = self.config.get_value(CONF_KEY_NUM_ALBUMS)
for artist_idx in range(num_artists):
for album_idx in range(num_albums):
album_item_id = f"{artist_idx}_{album_idx}"
yield await self.get_album(album_item_id)

async def get_library_tracks(self) -> AsyncGenerator[Track, None]:
"""Retrieve library tracks from the provider."""
for artist_idx in range(50):
for album_idx in range(25):
for track_idx in range(25):
num_artists = self.config.get_value(CONF_KEY_NUM_ARTISTS) or 5
num_albums = self.config.get_value(CONF_KEY_NUM_ALBUMS) or 5
num_tracks = self.config.get_value(CONF_KEY_NUM_TRACKS)
for artist_idx in range(num_artists):
for album_idx in range(num_albums):
for track_idx in range(num_tracks):
track_item_id = f"{artist_idx}_{album_idx}_{track_idx}"
yield await self.get_track(track_item_id)

async def get_library_podcasts(self) -> AsyncGenerator[Track, None]:
"""Retrieve library tracks from the provider."""
num_podcasts = self.config.get_value(CONF_KEY_NUM_PODCASTS)
for podcast_idx in range(num_podcasts):
yield await self.get_podcast(str(podcast_idx))

async def get_library_audiobooks(self) -> AsyncGenerator[Audiobook, None]:
"""Retrieve library audiobooks from the provider."""
num_audiobooks = self.config.get_value(CONF_KEY_NUM_AUDIOBOOKS)
for audiobook_idx in range(num_audiobooks):
yield await self.get_audiobook(str(audiobook_idx))

async def get_audiobook_chapters(
self,
prov_audiobook_id: str,
) -> list[Chapter]:
"""Get all Chapters for given audiobook id."""
num_chapters = randint(5, 75)
return [
await self.get_chapter(f"{prov_audiobook_id}_{chapter_idx}")
for chapter_idx in range(num_chapters)
]

async def get_podcast_episodes(
self,
prov_podcast_id: str,
) -> list[Episode]:
"""Get all Episodes for given podcast id."""
num_episodes = randint(5, 75)
return [
await self.get_episode(f"{prov_podcast_id}_{episode_idx}")
for episode_idx in range(num_episodes)
]

async def get_stream_details(
self, item_id: str, media_type: MediaType = MediaType.TRACK
) -> StreamDetails:
Expand Down

0 comments on commit 698dcd6

Please sign in to comment.