Jellyfin: Use aiojellyfin 0.10.0 for stricter typing and more speed (#1521)
authorJc2k <john.carr@unrouted.co.uk>
Mon, 29 Jul 2024 10:00:23 +0000 (11:00 +0100)
committerGitHub <noreply@github.com>
Mon, 29 Jul 2024 10:00:23 +0000 (12:00 +0200)
music_assistant/server/providers/jellyfin/__init__.py
music_assistant/server/providers/jellyfin/const.py
music_assistant/server/providers/jellyfin/manifest.json
music_assistant/server/providers/jellyfin/parsers.py
requirements_all.txt
tests/server/providers/jellyfin/test_init.py

index b1c32b0fa1e61a82275ac686181319124b377711..f2699e329bfabd795c4e6c529d0ccd31d95b84b2 100644 (file)
@@ -175,11 +175,12 @@ class JellyfinProvider(MusicProvider):
         return False
 
     async def _search_track(self, search_query: str, limit: int) -> list[Track]:
-        resultset = await self._client.tracks(
-            search_term=search_query,
-            limit=limit,
-            enable_user_data=True,
-            fields=TRACK_FIELDS,
+        resultset = (
+            await self._client.tracks.search_term(search_query)
+            .limit(limit)
+            .enable_userdata()
+            .fields(*TRACK_FIELDS)
+            .request()
         )
         tracks = []
         for item in resultset["Items"]:
@@ -192,11 +193,12 @@ class JellyfinProvider(MusicProvider):
             albumname = searchterms[1]
         else:
             albumname = search_query
-        resultset = await self._client.albums(
-            search_term=albumname,
-            limit=limit,
-            enable_user_data=True,
-            fields=ALBUM_FIELDS,
+        resultset = (
+            await self._client.albums.search_term(albumname)
+            .limit(limit)
+            .enable_userdata()
+            .fields(*ALBUM_FIELDS)
+            .request()
         )
         albums = []
         for item in resultset["Items"]:
@@ -204,11 +206,12 @@ class JellyfinProvider(MusicProvider):
         return albums
 
     async def _search_artist(self, search_query: str, limit: int) -> list[Artist]:
-        resultset = await self._client.artists(
-            search_term=search_query,
-            limit=limit,
-            enable_user_data=True,
-            fields=ARTIST_FIELDS,
+        resultset = (
+            await self._client.artists.search_term(search_query)
+            .limit(limit)
+            .enable_userdata()
+            .fields(*ARTIST_FIELDS)
+            .request()
         )
         artists = []
         for item in resultset["Items"]:
@@ -216,10 +219,11 @@ class JellyfinProvider(MusicProvider):
         return artists
 
     async def _search_playlist(self, search_query: str, limit: int) -> list[Playlist]:
-        resultset = await self._client.playlists(
-            search_term=search_query,
-            limit=limit,
-            enable_user_data=True,
+        resultset = (
+            await self._client.playlists.search_term(search_query)
+            .limit(limit)
+            .enable_userdata()
+            .request()
         )
         playlists = []
         for item in resultset["Items"]:
@@ -270,77 +274,39 @@ class JellyfinProvider(MusicProvider):
         """Retrieve all library artists from Jellyfin Music."""
         jellyfin_libraries = await self._get_music_libraries()
         for jellyfin_library in jellyfin_libraries:
-            offset = 0
-            limit = 100
-
-            response = await self._client.artists(
-                jellyfin_library[ITEM_KEY_ID],
-                start_index=offset,
-                limit=limit,
-                enable_user_data=True,
-                fields=ARTIST_FIELDS,
+            stream = (
+                self._client.artists.parent(jellyfin_library[ITEM_KEY_ID])
+                .enable_userdata()
+                .fields(*ARTIST_FIELDS)
+                .stream(100)
             )
-            for artist in response["Items"]:
+            async for artist in stream:
                 yield parse_artist(self.logger, self.instance_id, self._client, artist)
 
-            while offset < response["TotalRecordCount"]:
-                response = await self._client.artists(
-                    jellyfin_library[ITEM_KEY_ID],
-                    start_index=offset,
-                    limit=limit,
-                    enable_user_data=True,
-                    fields=ARTIST_FIELDS,
-                )
-                for artist in response["Items"]:
-                    yield parse_artist(self.logger, self.instance_id, self._client, artist)
-
-                offset += limit
-
     async def get_library_albums(self) -> AsyncGenerator[Album, None]:
         """Retrieve all library albums from Jellyfin Music."""
         jellyfin_libraries = await self._get_music_libraries()
         for jellyfin_library in jellyfin_libraries:
-            offset = 0
-            limit = 100
-
-            response = await self._client.albums(
-                jellyfin_library[ITEM_KEY_ID],
-                start_index=offset,
-                limit=limit,
-                enable_user_data=True,
-                fields=ALBUM_FIELDS,
+            stream = (
+                self._client.albums.parent(jellyfin_library[ITEM_KEY_ID])
+                .enable_userdata()
+                .fields(*ALBUM_FIELDS)
+                .stream(100)
             )
-            for artist in response["Items"]:
-                yield parse_album(self.logger, self.instance_id, self._client, artist)
-
-            while offset < response["TotalRecordCount"]:
-                response = await self._client.albums(
-                    jellyfin_library[ITEM_KEY_ID],
-                    start_index=offset,
-                    limit=limit,
-                    enable_user_data=True,
-                    fields=ALBUM_FIELDS,
-                )
-                for artist in response["Items"]:
-                    yield parse_album(self.logger, self.instance_id, self._client, artist)
-
-                offset += limit
+            async for album in stream:
+                yield parse_album(self.logger, self.instance_id, self._client, album)
 
     async def get_library_tracks(self) -> AsyncGenerator[Track, None]:
         """Retrieve library tracks from Jellyfin Music."""
         jellyfin_libraries = await self._get_music_libraries()
         for jellyfin_library in jellyfin_libraries:
-            offset = 0
-            limit = 100
-
-            response = await self._client.tracks(
-                jellyfin_library[ITEM_KEY_ID],
-                start_index=offset,
-                limit=limit,
-                enable_user_data=True,
-                fields=TRACK_FIELDS,
+            stream = (
+                self._client.tracks.parent(jellyfin_library[ITEM_KEY_ID])
+                .enable_userdata()
+                .fields(*TRACK_FIELDS)
+                .stream(100)
             )
-            for track in response["Items"]:
+            async for track in stream:
                 if not len(track[ITEM_KEY_MEDIA_STREAMS]):
                     self.logger.warning(
                         "Invalid track %s: Does not have any media streams", track[ITEM_KEY_NAME]
@@ -348,31 +314,16 @@ class JellyfinProvider(MusicProvider):
                     continue
                 yield parse_track(self.logger, self.instance_id, self._client, track)
 
-            while offset < response["TotalRecordCount"]:
-                response = await self._client.tracks(
-                    jellyfin_library[ITEM_KEY_ID],
-                    start_index=offset,
-                    limit=limit,
-                    enable_user_data=True,
-                    fields=TRACK_FIELDS,
-                )
-                for track in response["Items"]:
-                    if not len(track[ITEM_KEY_MEDIA_STREAMS]):
-                        self.logger.warning(
-                            "Invalid track %s: Does not have any media streams",
-                            track[ITEM_KEY_NAME],
-                        )
-                        continue
-                    yield parse_track(self.logger, self.instance_id, self._client, track)
-
-                offset += limit
-
     async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]:
         """Retrieve all library playlists from the provider."""
         playlist_libraries = await self._get_playlists()
         for playlist_library in playlist_libraries:
-            playlists_obj = await self._client.playlists(playlist_library[ITEM_KEY_ID])
-            for playlist in playlists_obj["Items"]:
+            stream = (
+                self._client.playlists.parent(playlist_library[ITEM_KEY_ID])
+                .enable_userdata()
+                .stream(100)
+            )
+            async for playlist in stream:
                 if "MediaType" in playlist:  # Only jellyfin has this property
                     if playlist["MediaType"] == "Audio":
                         yield parse_playlist(self.instance_id, self._client, playlist)
@@ -389,8 +340,11 @@ class JellyfinProvider(MusicProvider):
 
     async def get_album_tracks(self, prov_album_id: str) -> list[Track]:
         """Get album tracks for given album id."""
-        jellyfin_album_tracks = await self._client.tracks(
-            prov_album_id, enable_user_data=True, fields=TRACK_FIELDS
+        jellyfin_album_tracks = (
+            await self._client.tracks.parent(prov_album_id)
+            .enable_userdata()
+            .fields(*TRACK_FIELDS)
+            .request()
         )
         return [
             parse_track(self.logger, self.instance_id, self._client, jellyfin_album_track)
@@ -445,8 +399,11 @@ class JellyfinProvider(MusicProvider):
             return []
         # TODO: Does Jellyfin support paging here?
         jellyfin_playlist = await self._client.get_playlist(prov_playlist_id)
-        playlist_items = await self._client.tracks(
-            jellyfin_playlist[ITEM_KEY_ID], enable_user_data=True, fields=TRACK_FIELDS
+        playlist_items = (
+            await self._client.tracks.parent(jellyfin_playlist[ITEM_KEY_ID])
+            .enable_userdata()
+            .fields(*TRACK_FIELDS)
+            .request()
         )
         for index, jellyfin_track in enumerate(playlist_items["Items"], 1):
             try:
@@ -466,8 +423,11 @@ class JellyfinProvider(MusicProvider):
         """Get a list of albums for the given artist."""
         if not prov_artist_id.startswith(FAKE_ARTIST_PREFIX):
             return []
-        albums = await self._client.albums(
-            prov_artist_id, fields=ALBUM_FIELDS, enable_user_data=True
+        albums = (
+            await self._client.albums.parent(prov_artist_id)
+            .fields(*ALBUM_FIELDS)
+            .enable_userdata()
+            .request()
         )
         return [
             parse_album(self.logger, self.instance_id, self._client, album)
index 7ea656f74f9e8c904d7f385fcb5b4bebfacc0215..2bbfc9a9af6ab9d09352eaa4b3c5f4fd44339a7a 100644 (file)
@@ -2,7 +2,8 @@
 
 from typing import Final
 
-from aiojellyfin.const import ImageType as JellyImageType
+from aiojellyfin import ImageType as JellyImageType
+from aiojellyfin import ItemFields
 
 from music_assistant.common.models.enums import ImageType, MediaType
 from music_assistant.common.models.media_items import ItemMapping
@@ -67,9 +68,23 @@ SUPPORTED_CONTAINER_FORMATS: Final = "ogg,flac,mp3,aac,mpeg,alac,wav,aiff,wma,m4
 
 PLAYABLE_ITEM_TYPES: Final = [ITEM_TYPE_AUDIO]
 
-ARTIST_FIELDS = ["Overview", "ProviderIds", "SortName"]
-ALBUM_FIELDS = ["ProductionYear", "Overview", "ProviderIds", "SortName"]
-TRACK_FIELDS = ["ProviderIds", "CanDownload", "SortName", "MediaSources", "MediaStreams"]
+ARTIST_FIELDS: Final = [
+    ItemFields.Overview,
+    ItemFields.ProviderIds,
+    ItemFields.SortName,
+]
+ALBUM_FIELDS: Final = [
+    ItemFields.Overview,
+    ItemFields.ProviderIds,
+    ItemFields.SortName,
+]
+TRACK_FIELDS: Final = [
+    ItemFields.ProviderIds,
+    ItemFields.CanDownload,
+    ItemFields.SortName,
+    ItemFields.MediaSources,
+    ItemFields.MediaStreams,
+]
 
 USER_APP_NAME: Final = "Music Assistant"
 USER_AGENT: Final = "Music-Assistant-1.0"
index be8104211b84ef264c664b6879da339700828174..e86e08a475853709a9585681406013b378b6df72 100644 (file)
@@ -4,7 +4,7 @@
   "name": "Jellyfin Media Server Library",
   "description": "Support for the Jellyfin streaming provider in Music Assistant.",
   "codeowners": ["@lokiberra", "@Jc2k"],
-  "requirements": ["aiojellyfin==0.9.2"],
+  "requirements": ["aiojellyfin==0.10.0"],
   "documentation": "https://music-assistant.io/music-providers/jellyfin/",
   "multi_instance": true
 }
index 84676e863aa1f7ca32d15a8058872449d6491d03..1d022ce62c0aa1710b57d04c23894ff1c5baddb7 100644 (file)
@@ -6,7 +6,7 @@ import logging
 from logging import Logger
 from typing import TYPE_CHECKING
 
-from aiojellyfin.const import ImageType as JellyImageType
+from aiojellyfin import ImageType as JellyImageType
 
 from music_assistant.common.models.enums import ContentType, ExternalID, ImageType, MediaType
 from music_assistant.common.models.errors import InvalidDataError
index 91f6a2f9cb7448d09d884356e8b71b248207ae95..4facbcf31d716742e887ea89752856f7df6315ea 100644 (file)
@@ -4,7 +4,7 @@ Brotli>=1.0.9
 aiodns>=3.0.0
 aiofiles==24.1.0
 aiohttp==3.9.5
-aiojellyfin==0.9.2
+aiojellyfin==0.10.0
 aiorun==2024.5.1
 aioslimproto==3.0.1
 aiosqlite==0.20.0
index 6d8418ea03d2aadfc6b140de5fce1d69510431db..1222ef58382138544d0a436bff4af3999ab33636 100644 (file)
@@ -16,13 +16,13 @@ async def jellyfin_provider(mass: MusicAssistant) -> AsyncGenerator[ProviderConf
     """Configure an aiojellyfin test fixture, and add a provider to mass that uses it."""
     f = FixtureBuilder()
     async for _, artist in get_fixtures_dir("artists", "jellyfin"):
-        f.add_artist_bytes(artist)
+        f.add_json_bytes(artist)
 
     async for _, album in get_fixtures_dir("albums", "jellyfin"):
-        f.add_album_bytes(album)
+        f.add_json_bytes(album)
 
     async for _, track in get_fixtures_dir("tracks", "jellyfin"):
-        f.add_track_bytes(track)
+        f.add_json_bytes(track)
 
     authenticate_by_name = f.to_authenticate_by_name()