Fix: Some small fixes for audiobook/podcast support
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 26 Dec 2024 13:29:30 +0000 (14:29 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 26 Dec 2024 13:29:30 +0000 (14:29 +0100)
music_assistant/controllers/media/audiobooks.py
music_assistant/controllers/music.py
music_assistant/providers/test/__init__.py

index b7530a81c7fbbbb1da9cdc6aa7a9e3a817563ac3..5cd2fe122f0d6a38e49bcd460c682e540b53081c 100644 (file)
@@ -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
index 096bbcf34a06c8b050aa21425cc9e7a20a8d5de7..7d4f147643377468889e5062f4d0a936b52e896c 100644 (file)
@@ -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,
index 6d978b2b2b37471c380f20425d7ffae821be255d..be70ead9ca2c93dadac6b890c85f9265ca71fbf2 100644 (file)
@@ -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: