Chore: Small optimization to sync logic
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 17 Jan 2025 14:14:30 +0000 (15:14 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 17 Jan 2025 14:14:30 +0000 (15:14 +0100)
music_assistant/controllers/media/albums.py
music_assistant/controllers/media/artists.py
music_assistant/controllers/media/base.py
music_assistant/controllers/media/tracks.py
music_assistant/controllers/music.py
music_assistant/models/music_provider.py

index b81fa7d90a31e0f717d8a47aae575f4b9bf76f89..481707932de2c2bbdd3266438e3b4585431407db 100644 (file)
@@ -6,26 +6,11 @@ import contextlib
 from collections.abc import Iterable
 from typing import TYPE_CHECKING, Any
 
-from music_assistant_models.enums import (
-    AlbumType,
-    CacheCategory,
-    MediaType,
-    ProviderFeature,
-)
-from music_assistant_models.errors import InvalidDataError, MediaNotFoundError
-from music_assistant_models.media_items import (
-    Album,
-    Artist,
-    ItemMapping,
-    Track,
-    UniqueList,
-)
+from music_assistant_models.enums import AlbumType, CacheCategory, MediaType, ProviderFeature
+from music_assistant_models.errors import InvalidDataError, MediaNotFoundError, MusicAssistantError
+from music_assistant_models.media_items import Album, Artist, ItemMapping, Track, UniqueList
 
-from music_assistant.constants import (
-    DB_TABLE_ALBUM_ARTISTS,
-    DB_TABLE_ALBUM_TRACKS,
-    DB_TABLE_ALBUMS,
-)
+from music_assistant.constants import DB_TABLE_ALBUM_ARTISTS, DB_TABLE_ALBUM_TRACKS, DB_TABLE_ALBUMS
 from music_assistant.controllers.media.base import MediaControllerBase
 from music_assistant.helpers.compare import (
     compare_album,
@@ -199,11 +184,13 @@ class AlbumsController(MediaControllerBase[Album, Album]):
             sql_query += f" WHERE {' AND '.join(query_parts)}"
         return await self.mass.music.database.get_count_from_query(sql_query, query_params)
 
-    async def remove_item_from_library(self, item_id: str | int) -> None:
+    async def remove_item_from_library(self, item_id: str | int, recursive: bool = True) -> None:
         """Delete record from the database."""
         db_id = int(item_id)  # ensure integer
         # recursively also remove album tracks
         for db_track in await self.get_library_album_tracks(db_id):
+            if not recursive:
+                raise MusicAssistantError("Album still has tracks linked")
             with contextlib.suppress(MediaNotFoundError):
                 await self.mass.music.tracks.remove_item_from_library(db_track.item_id)
         # delete entry(s) from albumtracks table
@@ -211,6 +198,7 @@ class AlbumsController(MediaControllerBase[Album, Album]):
         # delete entry(s) from album artists table
         await self.mass.music.database.delete(DB_TABLE_ALBUM_ARTISTS, {"album_id": db_id})
         # delete the album itself from db
+        # this will raise if the item still has references and recursive is false
         await super().remove_item_from_library(item_id)
 
     async def tracks(
index 4f558a3ff4db017063089cabcfe408c22aa0b9c1..80095719f16c3d76f1e38cd136bf236a52018dbd 100644 (file)
@@ -6,20 +6,13 @@ import asyncio
 import contextlib
 from typing import TYPE_CHECKING, Any
 
-from music_assistant_models.enums import (
-    AlbumType,
-    CacheCategory,
-    MediaType,
-    ProviderFeature,
-)
-from music_assistant_models.errors import MediaNotFoundError, ProviderUnavailableError
-from music_assistant_models.media_items import (
-    Album,
-    Artist,
-    ItemMapping,
-    Track,
-    UniqueList,
+from music_assistant_models.enums import AlbumType, CacheCategory, MediaType, ProviderFeature
+from music_assistant_models.errors import (
+    MediaNotFoundError,
+    MusicAssistantError,
+    ProviderUnavailableError,
 )
+from music_assistant_models.media_items import Album, Artist, ItemMapping, Track, UniqueList
 
 from music_assistant.constants import (
     DB_TABLE_ALBUM_ARTISTS,
@@ -176,26 +169,31 @@ class ArtistsController(MediaControllerBase[Artist, Artist | ItemMapping]):
                     result.append(provider_album)
         return result
 
-    async def remove_item_from_library(self, item_id: str | int) -> None:
+    async def remove_item_from_library(self, item_id: str | int, recursive: bool = True) -> None:
         """Delete record from the database."""
         db_id = int(item_id)  # ensure integer
+
         # recursively also remove artist albums
         for db_row in await self.mass.music.database.get_rows_from_query(
             f"SELECT album_id FROM {DB_TABLE_ALBUM_ARTISTS} WHERE artist_id = {db_id}",
             limit=5000,
         ):
+            if not recursive:
+                raise MusicAssistantError("Artist still has albums linked")
             with contextlib.suppress(MediaNotFoundError):
                 await self.mass.music.albums.remove_item_from_library(db_row["album_id"])
-
         # recursively also remove artist tracks
         for db_row in await self.mass.music.database.get_rows_from_query(
             f"SELECT track_id FROM {DB_TABLE_TRACK_ARTISTS} WHERE artist_id = {db_id}",
             limit=5000,
         ):
+            if not recursive:
+                raise MusicAssistantError("Artist still has tracks linked")
             with contextlib.suppress(MediaNotFoundError):
                 await self.mass.music.tracks.remove_item_from_library(db_row["track_id"])
 
         # delete the artist itself from db
+        # this will raise if the item still has references and recursive is false
         await super().remove_item_from_library(db_id)
 
     async def get_provider_artist_toptracks(
index 16481dcd5fb69f59bba598d29e9f3b6ebc68cf6d..e3d5291b400613def29c140ab8f529d435758373 100644 (file)
@@ -26,11 +26,7 @@ from music_assistant_models.media_items import (
     Track,
 )
 
-from music_assistant.constants import (
-    DB_TABLE_PLAYLOG,
-    DB_TABLE_PROVIDER_MAPPINGS,
-    MASS_LOGGER_NAME,
-)
+from music_assistant.constants import DB_TABLE_PLAYLOG, DB_TABLE_PROVIDER_MAPPINGS, MASS_LOGGER_NAME
 from music_assistant.helpers.compare import compare_media_item
 from music_assistant.helpers.json import json_loads, serialize_to_json
 
@@ -177,7 +173,7 @@ class MediaControllerBase(Generic[ItemCls, LibraryUpdate], metaclass=ABCMeta):
         )
         return library_item
 
-    async def remove_item_from_library(self, item_id: str | int) -> None:
+    async def remove_item_from_library(self, item_id: str | int, recursive: bool = True) -> None:
         """Delete library record from the database."""
         db_id = int(item_id)  # ensure integer
         library_item = await self.get_library_item(db_id)
index 13cf64642edcce9b22c56fc0511890becd47cc39..9842d1bbd6bc18571b361514a834c5b819843b42 100644 (file)
@@ -315,7 +315,7 @@ class TracksController(MediaControllerBase[Track, Track]):
 
         return []
 
-    async def remove_item_from_library(self, item_id: str | int) -> None:
+    async def remove_item_from_library(self, item_id: str | int, recursive: bool = True) -> None:
         """Delete record from the database."""
         db_id = int(item_id)  # ensure integer
         # delete entry(s) from albumtracks table
index e5aa6c4b22d5d44f719c54d5e4937dd6d5cc7071..1d28ee6db29b7c99cc653c36f209bd19f70d2195 100644 (file)
@@ -576,7 +576,7 @@ class MusicController(CoreController):
 
     @api_command("music/library/remove_item")
     async def remove_item_from_library(
-        self, media_type: MediaType, library_item_id: str | int
+        self, media_type: MediaType, library_item_id: str | int, recursive: bool = True
     ) -> None:
         """
         Remove item from the library.
@@ -593,7 +593,7 @@ class MusicController(CoreController):
                 # so we need to be a bit forgiving here
                 with suppress(NotImplementedError):
                     await prov_controller.library_remove(provider_mapping.item_id, item.media_type)
-        await ctrl.remove_item_from_library(library_item_id)
+        await ctrl.remove_item_from_library(library_item_id, recursive)
 
     @api_command("music/library/add_item")
     async def add_item_to_library(
index 785645ffa28df04a272bb3d0d7707817dd6038f6..8ed2131b5f6db1c6739ed1659edcb166417aac8a 100644 (file)
@@ -655,17 +655,23 @@ class MusicProvider(Provider):
                             for x in item.provider_mappings
                             if x.provider_domain != self.domain
                         }
-                        if not remaining_providers and media_type != MediaType.ARTIST:
-                            # 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 we skip artists here to prevent a recursive removal
-                            # of all albums and tracks underneath this artist
-                            await controller.remove_item_from_library(db_id)
-                        else:
-                            # otherwise: just unmark favorite
+                        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 asyncio.sleep(0)  # yield to eventloop
+
             await self.mass.cache.set(
                 media_type.value,
                 list(cur_db_ids),