From: Marcel van der Veldt Date: Thu, 26 Dec 2024 13:29:30 +0000 (+0100) Subject: Fix: Some small fixes for audiobook/podcast support X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=698dcd6a4861f13e73ee6b28a472bdceb7ec79dd;p=music-assistant-server.git Fix: Some small fixes for audiobook/podcast support --- diff --git a/music_assistant/controllers/media/audiobooks.py b/music_assistant/controllers/media/audiobooks.py index b7530a81..5cd2fe12 100644 --- a/music_assistant/controllers/media/audiobooks.py +++ b/music_assistant/controllers/media/audiobooks.py @@ -154,8 +154,8 @@ class AudiobooksController(MediaControllerBase[Audiobook]): "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 @@ -191,10 +191,12 @@ class AudiobooksController(MediaControllerBase[Audiobook]): ), "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 diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py index 096bbcf3..7d4f1476 100644 --- a/music_assistant/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -1094,7 +1094,7 @@ class MusicController(CoreController): "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( @@ -1163,13 +1163,10 @@ class MusicController(CoreController): 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() @@ -1290,9 +1287,10 @@ class MusicController(CoreController): [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, @@ -1312,7 +1310,7 @@ class MusicController(CoreController): [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, diff --git a/music_assistant/providers/test/__init__.py b/music_assistant/providers/test/__init__.py index 6d978b2b..be70ead9 100644 --- a/music_assistant/providers/test/__init__.py +++ b/music_assistant/providers/test/__init__.py @@ -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, @@ -15,9 +18,14 @@ from music_assistant_models.enums import ( from music_assistant_models.media_items import ( Album, Artist, + Audiobook, AudioFormat, + Chapter, + Episode, + ItemMapping, MediaItemImage, MediaItemMetadata, + Podcast, ProviderMapping, Track, UniqueList, @@ -28,7 +36,7 @@ from music_assistant.constants import MASS_LOGO, VARIOUS_ARTISTS_FANART 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 @@ -49,6 +57,12 @@ DEFAULT_FANART = MediaItemImage( 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 @@ -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): @@ -84,7 +139,18 @@ class TestProvider(MusicProvider): @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.""" @@ -142,14 +208,142 @@ class TestProvider(MusicProvider): 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: