A collection of small bugfixes and tweaks (#1305)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 14 May 2024 18:48:50 +0000 (20:48 +0200)
committerGitHub <noreply@github.com>
Tue, 14 May 2024 18:48:50 +0000 (20:48 +0200)
* Fix paged listings total

* Precache album tracks

* Speedup playlist track listings

* Fix random sort for builtin playlists

* Fix local images on Jellfyin

they are considered as not remotely accessible

* catch parse errors in jellyfin

* Fix typo

* Fix browse feature

music_assistant/common/models/media_items.py
music_assistant/server/controllers/media/albums.py
music_assistant/server/controllers/media/artists.py
music_assistant/server/controllers/media/base.py
music_assistant/server/controllers/music.py
music_assistant/server/models/music_provider.py
music_assistant/server/providers/builtin/__init__.py
music_assistant/server/providers/jellyfin/__init__.py
music_assistant/server/providers/radiobrowser/__init__.py

index 366eab78139b336371abd1732359ad12fba9c608..4fa8dd0a202153ff907cd12fc1392787953064b3 100644 (file)
@@ -577,9 +577,8 @@ class PagedItems(Generic[_T]):
         self.limit = limit
         self.offset = offset
         self.total = total
-        if total is None and offset == 0 and count > limit:
-            self.total = count
-        if total is None and offset and count < limit:
+        if total is None and count != limit:
+            # total is important so always calculate it from count if omitted
             self.total = offset + count
 
     def to_dict(self, *args, **kwargs) -> dict[str, Any]:
index 566fa7c2e1cad734dac57d413b3ec1de468c3c3e..21e6d4559b2bc2b0e9b85b5b3c0e58da2cec4243 100644 (file)
@@ -302,6 +302,13 @@ class AlbumsController(MediaControllerBase[Album]):
         # store (serializable items) in cache
         if prov.is_streaming_provider:
             self.mass.create_task(self.mass.cache.set(cache_key, [x.to_dict() for x in items]))
+        for item in items:
+            # if this is a complete track object, pre-cache it as
+            # that will save us an (expensive) lookup later
+            if item.image and item.artist_str and item.album and prov.domain != "builtin":
+                await self.mass.cache.set(
+                    f"provider_item.track.{prov.lookup_key}.{item_id}", item.to_dict()
+                )
         return items
 
     async def _get_provider_dynamic_tracks(
index 20ae7a08ded1feb21c7b64bfe88bd5c2744e1901..a039618290790956c522e7c3c4808859157775a4 100644 (file)
@@ -326,7 +326,7 @@ class ArtistsController(MediaControllerBase[Artist]):
         if isinstance(update, ItemMapping):
             # NOTE that artist is the only mediatype where its accepted we
             # receive an itemmapping from streaming providers
-            update = Artist.from_item_mapping(update)
+            update = self._artist_from_item_mapping(update)
             metadata = cur_item.metadata
         else:
             metadata = update.metadata if overwrite else cur_item.metadata.update(update.metadata)
index c2920b767ce464cdd78938e44516566f610cc48e..c9ce75cf43c5a8139053ea0df5e2179f3975eeb8 100644 (file)
@@ -62,6 +62,7 @@ SORT_KEYS = {
     "position": "position ASC",
     "position_desc": "position DESC",
     "random": "RANDOM()",
+    "random_play_count": "RANDOM(), play_count",
 }
 
 
index d6b455de97d4675b18874aa321611e5a51d750db..b2c9ba4658b2bf55ee6a9476bd5548982c04b3e0 100644 (file)
@@ -348,12 +348,13 @@ class MusicController(CoreController):
             )
             if not prov:
                 return PagedItems(items=prepend_items, limit=limit, offset=offset)
-        else:
+        elif offset == 0:
             back_path = f"{provider_instance}://" + "/".join(sub_path.split("/")[:-1])
             prepend_items.append(
                 BrowseFolder(item_id="back", provider=provider_instance, path=back_path, name="..")
             )
-        prov_items = await prov.browse(path, offset, limit)
+        # limit -1 to account for the prepended items
+        prov_items = await prov.browse(path, offset=offset, limit=limit)
         prov_items.items = prepend_items + prov_items.items
         return prov_items
 
@@ -376,7 +377,8 @@ class MusicController(CoreController):
                 continue
             with suppress(MediaNotFoundError, ProviderUnavailableError):
                 media_type = MediaType(db_row["media_type"])
-                item = await self.get_item(media_type, db_row["item_id"], db_row["provider"])
+                ctrl = self.get_controller(media_type)
+                item = await ctrl.get_provider_item(db_row["item_id"], db_row["provider"])
                 result.append(item)
         return result
 
index e99ec7b0ba2bf52eee879df56f9d8f09c97b8ce0..ede8f9d7392b657674d97b4bc30cca4adf55dc14 100644 (file)
@@ -292,7 +292,7 @@ class MusicProvider(Provider):
             # we may NOT use the default implementation if the provider does not support browse
             raise NotImplementedError
         items: list[MediaItemType] = []
-        index = 0
+        index = -1
         subpath = path.split("://", 1)[1]
         # this reference implementation can be overridden with a provider specific approach
         generator: AsyncGenerator[MediaItemType, None] | None = None
@@ -314,10 +314,10 @@ class MusicProvider(Provider):
         if generator:
             # grab items from library generator
             async for item in generator:
+                index += 1
                 if index < offset:
                     continue
                 items.append(item)
-                index += 1
                 if len(items) >= limit:
                     break
         else:
index 3735afdb1de19908a784b6a66e1b08b185c1f076..13f719dd5eb3e596a5db5813dcd66a3d79bcda67 100644 (file)
@@ -509,7 +509,7 @@ class BuiltinProvider(MusicProvider):
         result: list[Track] = []
         if builtin_playlist_id == ALL_FAVORITE_TRACKS:
             res = await self.mass.music.tracks.library_items(
-                favorite=True, limit=2500, order_by="RANDOM(), play_count"
+                favorite=True, limit=2500, order_by="random_play_count"
             )
             for idx, item in enumerate(res.items, 1):
                 item.position = idx
@@ -517,7 +517,7 @@ class BuiltinProvider(MusicProvider):
             return result
         if builtin_playlist_id == RANDOM_TRACKS:
             res = await self.mass.music.tracks.library_items(
-                limit=500, order_by="RANDOM(), play_count"
+                limit=500, order_by="random_play_count"
             )
             for idx, item in enumerate(res.items, 1):
                 item.position = idx
@@ -525,7 +525,7 @@ class BuiltinProvider(MusicProvider):
             return result
         if builtin_playlist_id == RANDOM_ALBUM:
             for random_album in (
-                await self.mass.music.albums.library_items(limit=1, order_by="RANDOM()")
+                await self.mass.music.albums.library_items(limit=1, order_by="random")
             ).items:
                 # use the function specified in the queue controller as that
                 # already handles unwrapping an album by user preference
@@ -538,7 +538,7 @@ class BuiltinProvider(MusicProvider):
                 return result
         if builtin_playlist_id == RANDOM_ARTIST:
             for random_artist in (
-                await self.mass.music.artists.library_items(limit=1, order_by="RANDOM()")
+                await self.mass.music.artists.library_items(limit=1, order_by="random")
             ).items:
                 # use the function specified in the queue controller as that
                 # already handles unwrapping an artist by user preference
index 3e66ec88be72399cd171053a54ec3e56e07f45b4..ad6932bbde16170cd4e0f2164b86382d6f6202aa 100644 (file)
@@ -310,7 +310,7 @@ class JellyfinProvider(MusicProvider):
                     type=ImageType.THUMB,\r
                     path=thumb,\r
                     provider=self.instance_id,\r
-                    remotely_accessible=True,\r
+                    remotely_accessible=False,\r
                 )\r
             ]\r
         if ITEM_KEY_OVERVIEW in current_jellyfin_album:\r
@@ -374,7 +374,7 @@ class JellyfinProvider(MusicProvider):
                     type=ImageType.THUMB,\r
                     path=thumb,\r
                     provider=self.instance_id,\r
-                    remotely_accessible=True,\r
+                    remotely_accessible=False,\r
                 )\r
             ]\r
         return artist\r
@@ -421,7 +421,7 @@ class JellyfinProvider(MusicProvider):
                     type=ImageType.THUMB,\r
                     path=thumb,\r
                     provider=self.instance_id,\r
-                    remotely_accessible=True,\r
+                    remotely_accessible=False,\r
                 )\r
             ]\r
         if len(current_jellyfin_track[ITEM_KEY_ARTIST_ITEMS]) >= 1:\r
@@ -452,13 +452,6 @@ class JellyfinProvider(MusicProvider):
                     parent_album[ITEM_KEY_ALBUM_ARTIST],\r
                 )\r
             )\r
-            track.artists.append(\r
-                self._get_item_mapping(\r
-                    MediaType.ARTIST,\r
-                    parent_album[ITEM_KEY_PARENT_ID],\r
-                    parent_album[ITEM_KEY_ALBUM_ARTIST],\r
-                )\r
-            )\r
         else:\r
             track.artists.append(await self._parse_artist(name=VARIOUS_ARTISTS_NAME))\r
         if (\r
@@ -513,7 +506,7 @@ class JellyfinProvider(MusicProvider):
                     type=ImageType.THUMB,\r
                     path=thumb,\r
                     provider=self.instance_id,\r
-                    remotely_accessible=True,\r
+                    remotely_accessible=False,\r
                 )\r
             ]\r
         playlist.is_editable = False\r
@@ -681,6 +674,7 @@ class JellyfinProvider(MusicProvider):
     ) -> list[Track]:\r
         """Get playlist tracks."""\r
         result: list[Track] = []\r
+        # TODO: Does Jellyfin support paging here?\r
         jellyfin_playlist = API.get_item(self._jellyfin_server.jellyfin, prov_playlist_id)\r
         playlist_items = await self._get_children(\r
             self._jellyfin_server, jellyfin_playlist[ITEM_KEY_ID], ITEM_TYPE_AUDIO\r
@@ -688,10 +682,15 @@ class JellyfinProvider(MusicProvider):
         if not playlist_items:\r
             return result\r
         for index, jellyfin_track in enumerate(playlist_items):\r
-            if track := await self._parse_track(jellyfin_track):\r
-                if not track.position:\r
-                    track.position = index\r
-                result.append(track)\r
+            try:\r
+                if track := await self._parse_track(jellyfin_track):\r
+                    if not track.position:\r
+                        track.position = index\r
+                    result.append(track)\r
+            except (KeyError, ValueError) as err:\r
+                self.logger.error(\r
+                    "Skipping track %s: %s", jellyfin_track.get(ITEM_KEY_NAME, index), str(err)\r
+                )\r
         return result\r
 \r
     async def get_artist_albums(self, prov_artist_id) -> list[Album]:\r
index 711b33136c62da075d121b72b87af8fd2b4be0bf..a4f5d9dad9f95ff40bf907533712ae8771ce886b 100644 (file)
@@ -102,7 +102,7 @@ class RadioBrowserProvider(MusicProvider):
 
         return result
 
-    async def browse(self, path: str, limit: int, offset: int) -> PagedItems[MediaItemType]:
+    async def browse(self, path: str, offset: int, limit: int) -> PagedItems[MediaItemType]:
         """Browse this provider's items.
 
         :param path: The path to browse, (e.g. provid://artists).