Chore: a couple of small sync tweaks
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 14 Feb 2025 00:31:56 +0000 (01:31 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 14 Feb 2025 00:31:56 +0000 (01:31 +0100)
music_assistant/controllers/music.py
music_assistant/models/music_provider.py
music_assistant/providers/_template_music_provider/__init__.py
music_assistant/providers/audiobookshelf/__init__.py
music_assistant/providers/builtin/__init__.py
music_assistant/providers/filesystem_local/__init__.py

index b94e8977e1815b41c5d19dc758d0abe992a48616..bc00c45db6f1614c4e09e934b93860808e3d8eac 100644 (file)
@@ -200,10 +200,13 @@ class MusicController(CoreController):
         if providers is None:
             providers = [x.instance_id for x in self.providers]
 
-        for provider in self.providers:
-            if provider.instance_id not in providers:
-                continue
-            self._start_provider_sync(provider, media_types)
+        for media_type in media_types:
+            for provider in self.providers:
+                if provider.instance_id not in providers:
+                    continue
+                if not provider.library_supported(media_type):
+                    continue
+                self._start_provider_sync(provider, media_type)
 
     @api_command("music/synctasks")
     def get_running_sync_tasks(self) -> list[SyncTask]:
@@ -985,29 +988,26 @@ class MusicController(CoreController):
                 domains.add(provider.domain)
         return instances
 
-    def _start_provider_sync(
-        self, provider: MusicProvider, media_types: tuple[MediaType, ...]
-    ) -> None:
+    def _start_provider_sync(self, provider: MusicProvider, media_type: MediaType) -> None:
         """Start sync task on provider and track progress."""
         # check if we're not already running a sync task for this provider/mediatype
         for sync_task in self.in_progress_syncs:
             if sync_task.provider_instance != provider.instance_id:
                 continue
-            for media_type in media_types:
-                if media_type in sync_task.media_types:
-                    self.logger.debug(
-                        "Skip sync task for %s because another task is already in progress",
-                        provider.name,
-                    )
-                    return
+            if media_type in sync_task.media_types:
+                self.logger.debug(
+                    "Skip sync task for %s because another task is already in progress",
+                    provider.name,
+                )
+                return
 
         async def run_sync() -> None:
             # Wrap the provider sync into a lock to prevent
             # race conditions when multiple providers are syncing at the same time.
             async with self._sync_lock:
-                await provider.sync_library(media_types)
+                await provider.sync_library(media_type)
             # precache playlist tracks
-            if MediaType.PLAYLIST in media_types:
+            if media_type == MediaType.PLAYLIST:
                 for playlist in await self.playlists.library_items(provider=provider.instance_id):
                     async for _ in self.playlists.tracks(playlist.item_id, playlist.provider):
                         pass
@@ -1017,7 +1017,7 @@ class MusicController(CoreController):
         sync_spec = SyncTask(
             provider_domain=provider.domain,
             provider_instance=provider.instance_id,
-            media_types=media_types,
+            media_types=(media_type,),
             task=task,
         )
         self.in_progress_syncs.append(sync_spec)
index 0d1eedc5d694141b7399f687a910e76e2dd2e4d5..650ff631383e3e0b920008823b91cbc3e9b56dcf 100644 (file)
@@ -7,7 +7,11 @@ from collections.abc import Sequence
 from typing import TYPE_CHECKING, cast
 
 from music_assistant_models.enums import CacheCategory, MediaType, ProviderFeature
-from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError
+from music_assistant_models.errors import (
+    MediaNotFoundError,
+    MusicAssistantError,
+    UnsupportedFeaturedException,
+)
 from music_assistant_models.media_items import (
     Album,
     Artist,
@@ -605,112 +609,111 @@ class MusicProvider(Provider):
             raise NotImplementedError
         return []
 
-    async def sync_library(self, media_types: tuple[MediaType, ...]) -> None:
+    async def sync_library(self, media_type: MediaType) -> None:
         """Run library sync for this provider."""
         # this reference implementation can be overridden
         # with a provider specific approach if needed
-        for media_type in media_types:
-            if not self.library_supported(media_type):
-                continue
-            self.logger.debug("Start sync of %s items.", media_type.value)
-            controller = self.mass.music.get_controller(media_type)
-            cur_db_ids = set()
-            async for prov_item in self._get_library_gen(media_type):
-                library_item = await controller.get_library_item_by_prov_mappings(
-                    prov_item.provider_mappings,
+        if not self.library_supported(media_type):
+            raise UnsupportedFeaturedException("Library sync not supported for this media type")
+        self.logger.debug("Start sync of %s items.", media_type.value)
+        controller = self.mass.music.get_controller(media_type)
+        cur_db_ids = set()
+        async for prov_item in self._get_library_gen(media_type):
+            library_item = await controller.get_library_item_by_prov_mappings(
+                prov_item.provider_mappings,
+            )
+            try:
+                if not library_item and not prov_item.available:
+                    # skip unavailable tracks
+                    self.logger.debug(
+                        "Skipping sync of item %s because it is unavailable",
+                        prov_item.uri,
+                    )
+                    continue
+                if not library_item:
+                    # create full db item
+                    # note that we skip the metadata lookup purely to speed up the sync
+                    # the additional metadata is then lazy retrieved afterwards
+                    if self.is_streaming_provider:
+                        prov_item.favorite = True
+                    library_item = await controller.add_item_to_library(prov_item)
+                elif getattr(library_item, "cache_checksum", None) != getattr(
+                    prov_item, "cache_checksum", None
+                ):
+                    # existing dbitem checksum changed (playlists only)
+                    library_item = await controller.update_item_in_library(
+                        library_item.item_id, prov_item
+                    )
+                if library_item.available != prov_item.available:
+                    # existing item availability changed
+                    library_item = await controller.update_item_in_library(
+                        library_item.item_id, prov_item
+                    )
+                # check if resume_position_ms or fully_played changed (audiobook only)
+                resume_pos_prov = getattr(prov_item, "resume_position_ms", None)
+                fully_played_prov = getattr(prov_item, "fully_played", None)
+                if (
+                    resume_pos_prov is not None
+                    and fully_played_prov is not None
+                    and (
+                        getattr(library_item, "resume_position_ms", None) != resume_pos_prov
+                        or getattr(library_item, "fully_played", None) != fully_played_prov
+                    )
+                ):
+                    library_item = await controller.update_item_in_library(
+                        library_item.item_id, prov_item
+                    )
+                await asyncio.sleep(0)  # yield to eventloop
+            except MusicAssistantError as err:
+                self.logger.warning(
+                    "Skipping sync of item %s - error details: %s",
+                    prov_item.uri,
+                    str(err),
                 )
-                try:
-                    if not library_item and not prov_item.available:
-                        # skip unavailable tracks
-                        self.logger.debug(
-                            "Skipping sync of item %s because it is unavailable",
-                            prov_item.uri,
-                        )
+
+        # process deletions (= no longer in library)
+        cache_category = CacheCategory.LIBRARY_ITEMS
+        cache_base_key = self.instance_id
+
+        prev_library_items: list[int] | None
+        if prev_library_items := await self.mass.cache.get(
+            media_type.value, category=cache_category, base_key=cache_base_key
+        ):
+            for db_id in prev_library_items:
+                if db_id not in cur_db_ids:
+                    try:
+                        item = await controller.get_library_item(db_id)
+                    except MediaNotFoundError:
+                        # edge case: the item is already removed
                         continue
-                    if not library_item:
-                        # create full db item
-                        # note that we skip the metadata lookup purely to speed up the sync
-                        # the additional metadata is then lazy retrieved afterwards
-                        if self.is_streaming_provider:
-                            prov_item.favorite = True
-                        library_item = await controller.add_item_to_library(prov_item)
-                    elif getattr(library_item, "cache_checksum", None) != getattr(
-                        prov_item, "cache_checksum", None
-                    ):
-                        # existing dbitem checksum changed (playlists only)
-                        library_item = await controller.update_item_in_library(
-                            library_item.item_id, prov_item
-                        )
-                    if library_item.available != prov_item.available:
-                        # existing item availability changed
-                        library_item = await controller.update_item_in_library(
-                            library_item.item_id, prov_item
-                        )
-                    # check if resume_position_ms or fully_played changed (audiobook only)
-                    resume_pos_prov = getattr(prov_item, "resume_position_ms", None)
-                    fully_played_prov = getattr(prov_item, "fully_played", None)
-                    if (
-                        resume_pos_prov is not None
-                        and fully_played_prov is not None
-                        and (
-                            getattr(library_item, "resume_position_ms", None) != resume_pos_prov
-                            or getattr(library_item, "fully_played", None) != fully_played_prov
-                        )
-                    ):
-                        library_item = await controller.update_item_in_library(
-                            library_item.item_id, prov_item
+                    remaining_providers = {
+                        x.provider_domain
+                        for x in item.provider_mappings
+                        if x.provider_domain != self.domain
+                    }
+                    if remaining_providers:
+                        continue
+                    # this item is removed from the provider's library
+                    # and we have no other providers attached to it
+                    # it is safe to remove it from the MA library too
+                    # note that we do not remove item's recursively on purpose
+                    try:
+                        await controller.remove_item_from_library(db_id, recursive=False)
+                    except MusicAssistantError as err:
+                        # this is probably because the item still has dependents
+                        self.logger.warning(
+                            "Error removing item %s from library: %s", db_id, str(err)
                         )
+                        # just un-favorite the item if we can't remove it
+                        await controller.set_favorite(db_id, False)
                     await asyncio.sleep(0)  # yield to eventloop
-                except MusicAssistantError as err:
-                    self.logger.warning(
-                        "Skipping sync of item %s - error details: %s",
-                        prov_item.uri,
-                        str(err),
-                    )
 
-            # process deletions (= no longer in library)
-            cache_category = CacheCategory.LIBRARY_ITEMS
-            cache_base_key = self.instance_id
-
-            prev_library_items: list[int] | None
-            if prev_library_items := await self.mass.cache.get(
-                media_type.value, category=cache_category, base_key=cache_base_key
-            ):
-                for db_id in prev_library_items:
-                    if db_id not in cur_db_ids:
-                        try:
-                            item = await controller.get_library_item(db_id)
-                        except MediaNotFoundError:
-                            # edge case: the item is already removed
-                            continue
-                        remaining_providers = {
-                            x.provider_domain
-                            for x in item.provider_mappings
-                            if x.provider_domain != self.domain
-                        }
-                        if remaining_providers:
-                            continue
-                        # this item is removed from the provider's library
-                        # and we have no other providers attached to it
-                        # it is safe to remove it from the MA library too
-                        # note that we do not remove item's recursively on purpose
-                        try:
-                            await controller.remove_item_from_library(db_id, recursive=False)
-                        except MusicAssistantError as err:
-                            # this is probably because the item still has dependents
-                            self.logger.warning(
-                                "Error removing item %s from library: %s", db_id, str(err)
-                            )
-                            # just un-favorite the item if we can't remove it
-                            await controller.set_favorite(db_id, False)
-                        await asyncio.sleep(0)  # yield to eventloop
-
-            await self.mass.cache.set(
-                media_type.value,
-                list(cur_db_ids),
-                category=cache_category,
-                base_key=cache_base_key,
-            )
+        await self.mass.cache.set(
+            media_type.value,
+            list(cur_db_ids),
+            category=cache_category,
+            base_key=cache_base_key,
+        )
 
     # DO NOT OVERRIDE BELOW
 
index 7bb596e37cc6225866d1b9c51080cfb808663cea..b960057509957dde914ad8eedd780051296f8c19 100644 (file)
@@ -507,9 +507,9 @@ class MyDemoMusicprovider(MusicProvider):
         # This is only called if you reported the RECOMMENDATIONS feature in the supported_features.
         return []
 
-    async def sync_library(self, media_types: tuple[MediaType, ...]) -> None:
+    async def sync_library(self, media_type: MediaType) -> None:
         """Run library sync for this provider."""
-        # Run a full sync of the library for the given media types.
+        # Run a full sync of the library for the given media type.
         # This is called by the music controller to sync items from your provider to the library.
         # As a generic rule of thumb the default implementation within the MusicProvider
         # base model should be sufficient for most (streaming) providers.
index 31cc4dc990ea993fbd9c663d993499580a4ca5aa..bb49da71e12d2f62229963ef30fd40e152761180 100644 (file)
@@ -181,10 +181,10 @@ class Audiobookshelf(MusicProvider):
         # For streaming providers return True here but for local file based providers return False.
         return False
 
-    async def sync_library(self, media_types: tuple[MediaType, ...]) -> None:
+    async def sync_library(self, media_type: MediaType) -> None:
         """Run library sync for this provider."""
         await self._client.sync()
-        await super().sync_library(media_types=media_types)
+        await super().sync_library(media_type=media_type)
 
     def _parse_podcast(
         self, abs_podcast: ABSLibraryItemExpandedPodcast | ABSLibraryItemMinifiedPodcast
index d53f9dec4214d167b006310a5bb08e971fc4d3bb..9dacc398e8fb9ccab0c253508297fa1c26696a6b 100644 (file)
@@ -186,15 +186,13 @@ class BuiltinProvider(MusicProvider):
             # always prefer the stored info, such as the name
             parsed_item.name = stored_item["name"]
             if image_url := stored_item.get("image_url"):
-                parsed_item.metadata.images = UniqueList(
-                    [
-                        MediaItemImage(
-                            type=ImageType.THUMB,
-                            path=image_url,
-                            provider=self.domain,
-                            remotely_accessible=image_url.startswith("http"),
-                        )
-                    ]
+                parsed_item.metadata.add_image(
+                    MediaItemImage(
+                        type=ImageType.THUMB,
+                        path=image_url,
+                        provider=self.domain,
+                        remotely_accessible=image_url.startswith("http"),
+                    )
                 )
         return parsed_item
 
@@ -207,15 +205,13 @@ class BuiltinProvider(MusicProvider):
             # always prefer the stored info, such as the name
             parsed_item.name = stored_item["name"]
             if image_url := stored_item.get("image_url"):
-                parsed_item.metadata.images = UniqueList(
-                    [
-                        MediaItemImage(
-                            type=ImageType.THUMB,
-                            path=image_url,
-                            provider=self.domain,
-                            remotely_accessible=image_url.startswith("http"),
-                        )
-                    ]
+                parsed_item.metadata.add_image(
+                    MediaItemImage(
+                        type=ImageType.THUMB,
+                        path=image_url,
+                        provider=self.domain,
+                        remotely_accessible=image_url.startswith("http"),
+                    )
                 )
         return parsed_item
 
@@ -282,15 +278,13 @@ class BuiltinProvider(MusicProvider):
         )
         playlist.cache_checksum = str(stored_item.get("last_updated"))
         if image_url := stored_item.get("image_url"):
-            playlist.metadata.images = UniqueList(
-                [
-                    MediaItemImage(
-                        type=ImageType.THUMB,
-                        path=image_url,
-                        provider=self.domain,
-                        remotely_accessible=image_url.startswith("http"),
-                    )
-                ]
+            playlist.metadata.add_image(
+                MediaItemImage(
+                    type=ImageType.THUMB,
+                    path=image_url,
+                    provider=self.domain,
+                    remotely_accessible=image_url.startswith("http"),
+                )
             )
         return playlist
 
@@ -335,8 +329,21 @@ class BuiltinProvider(MusicProvider):
         for item in stored_items:
             try:
                 yield await self.get_radio(item["item_id"])
-            except MediaNotFoundError as err:
+            except (MediaNotFoundError, InvalidDataError) as err:
                 self.logger.warning("Radio station %s not found: %s", item, err)
+                yield Radio(
+                    item_id=item["item_id"],
+                    provider=self.lookup_key,
+                    name=item["name"],
+                    provider_mappings={
+                        ProviderMapping(
+                            item_id=item["item_id"],
+                            provider_domain=self.domain,
+                            provider_instance=self.instance_id,
+                            available=False,
+                        )
+                    },
+                )
 
     async def library_add(self, item: MediaItemType) -> bool:
         """Add item to provider's library. Return true on success."""
index e1f1c8bd3e8adccbb6bde140d9260fe938c12738..d5eb0325409271ce092407049eef56f50d293b1d 100644 (file)
@@ -300,7 +300,7 @@ class LocalFileSystemProvider(MusicProvider):
                 )
         return items
 
-    async def sync_library(self, media_types: tuple[MediaType, ...]) -> None:
+    async def sync_library(self, media_type: MediaType) -> None:
         """Run library sync for this provider."""
         assert self.mass.music.database
         start_time = time.time()