Jellyfin: Add test scaffolding.
authorJohn Carr <john.carr@unrouted.co.uk>
Fri, 21 Jun 2024 15:14:01 +0000 (16:14 +0100)
committerJohn Carr <john.carr@unrouted.co.uk>
Sat, 22 Jun 2024 23:37:32 +0000 (00:37 +0100)
music_assistant/server/providers/jellyfin/manifest.json
requirements_all.txt
tests/common.py [new file with mode: 0644]
tests/conftest.py
tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr
tests/server/providers/jellyfin/fixtures/albums/infest.json [new file with mode: 0644]
tests/server/providers/jellyfin/test_init.py [new file with mode: 0644]

index a21bed18360726bfce49aeb1028569b4d8eac0fe..74714c7a16baecd95eccb9816b4e7a60874e6cb8 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.7.0"],
+  "requirements": ["aiojellyfin==0.8.3"],
   "documentation": "https://music-assistant.io/music-providers/jellyfin/",
   "multi_instance": true
 }
index f89879642287fe4773db210a94c38d3a42903de3..12c0948ff16027a1aae364f2bc2a3e4d46b4d281 100644 (file)
@@ -4,7 +4,7 @@ Brotli>=1.0.9
 aiodns>=3.0.0
 aiofiles==23.2.1
 aiohttp==3.9.5
-aiojellyfin==0.7.0
+aiojellyfin==0.8.3
 aiorun==2024.5.1
 aioslimproto==3.0.1
 aiosqlite==0.20.0
diff --git a/tests/common.py b/tests/common.py
new file mode 100644 (file)
index 0000000..394540b
--- /dev/null
@@ -0,0 +1,47 @@
+"""Common test helpers for Music Assistant tests."""
+
+import asyncio
+import contextlib
+import pathlib
+from collections.abc import AsyncGenerator
+
+import aiofiles.os
+
+from music_assistant.common.models.enums import EventType
+from music_assistant.common.models.event import MassEvent
+from music_assistant.server.server import MusicAssistant
+
+
+def _get_fixture_folder(provider: str | None = None) -> pathlib.Path:
+    tests_base = pathlib.Path(__file__).parent
+    if provider:
+        return tests_base / "server" / "providers" / provider / "fixtures"
+    return tests_base / "fixtures"
+
+
+async def get_fixtures_dir(
+    subdir: str, provider: str | None = None
+) -> AsyncGenerator[tuple[str, bytes], None]:
+    """Yield the contents of every fixture in a fixtures folder."""
+    dir_path = _get_fixture_folder(provider) / subdir
+    for file in await aiofiles.os.listdir(dir_path):
+        async with aiofiles.open(dir_path / file, "rb") as fp:
+            yield (file, await fp.read())
+
+
+@contextlib.asynccontextmanager
+async def wait_for_sync_completion(mass: MusicAssistant) -> AsyncGenerator[None, None]:
+    """Wait for a sync to finish."""
+    flag = asyncio.Event()
+
+    def _event(event: MassEvent) -> None:
+        if not event.data:
+            flag.set()
+
+    release_cb = mass.subscribe(_event, EventType.SYNC_TASKS_UPDATED)
+
+    try:
+        yield
+    finally:
+        await flag.wait()
+        release_cb()
index 6ebde314ab41c33b9239fafcd0945b73f2ad7129..fc25b0969544f372fe4037928e252bf10f8a0e6e 100644 (file)
@@ -7,6 +7,7 @@ from collections.abc import AsyncGenerator
 import pytest
 
 from music_assistant.server.server import MusicAssistant
+from tests.common import wait_for_sync_completion
 
 
 @pytest.fixture(name="caplog")
@@ -22,8 +23,13 @@ async def mass(tmp_path: pathlib.Path) -> AsyncGenerator[MusicAssistant, None]:
     storage_path = tmp_path / "root"
     storage_path.mkdir(parents=True)
 
-    mass = MusicAssistant(storage_path)
-    await mass.start()
+    logging.getLogger("aiosqlite").level = logging.INFO
+
+    mass = MusicAssistant(str(storage_path))
+
+    async with wait_for_sync_completion(mass):
+        await mass.start()
+
     try:
         yield mass
     finally:
index c9f2020b7da2b22d3b33b033eb4a33f41e7ab89f..6de9723a21b9420f1df21a5e7f298dcddabfb823 100644 (file)
@@ -1,4 +1,84 @@
 # serializer version: 1
+# name: test_parse_albums[infest]
+  dict({
+    'album_type': 'unknown',
+    'artists': list([
+      dict({
+        'available': True,
+        'external_ids': list([
+        ]),
+        'image': None,
+        'item_id': 'e439648e08ade14e27d5de48fa97c88e',
+        'media_type': 'artist',
+        'name': 'Papa Roach',
+        'provider': 'xx-instance-id-xx',
+        'sort_name': 'papa roach',
+        'uri': 'xx-instance-id-xx://artist/e439648e08ade14e27d5de48fa97c88e',
+        'version': '',
+      }),
+    ]),
+    'external_ids': list([
+      list([
+        'musicbrainz',
+        '0193355a-cdfb-3936-afd2-44d651eb006d',
+      ]),
+    ]),
+    'favorite': False,
+    'item_id': '70b7288088b42d318f75dbcc41fd0091',
+    'media_type': 'album',
+    'metadata': dict({
+      'cache_checksum': None,
+      'chapters': None,
+      'copyright': None,
+      'description': None,
+      'explicit': None,
+      'genres': None,
+      'images': list([
+        dict({
+          'path': 'http://localhost:1234/Items/70b7288088b42d318f75dbcc41fd0091/Images/Primary?api_key=ACCESS_TOKEN',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': False,
+          'type': 'thumb',
+        }),
+      ]),
+      'label': None,
+      'last_refresh': None,
+      'links': None,
+      'lyrics': None,
+      'mood': None,
+      'performers': None,
+      'popularity': None,
+      'preview': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': 'Infest',
+    'position': None,
+    'provider': 'jellyfin',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 320,
+          'channels': 2,
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': '70b7288088b42d318f75dbcc41fd0091',
+        'provider_domain': 'jellyfin',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'infest',
+    'uri': 'jellyfin://album/70b7288088b42d318f75dbcc41fd0091',
+    'version': '',
+    'year': 2000,
+  })
+# ---
 # name: test_parse_albums[this_is_christmas]
   dict({
     'album_type': 'unknown',
diff --git a/tests/server/providers/jellyfin/fixtures/albums/infest.json b/tests/server/providers/jellyfin/fixtures/albums/infest.json
new file mode 100644 (file)
index 0000000..a7cbd80
--- /dev/null
@@ -0,0 +1,105 @@
+{
+    "Name": "Infest",
+    "ServerId": "58f180d8d2b34927bcfd73eee400ffad",
+    "Id": "70b7288088b42d318f75dbcc41fd0091",
+    "Etag": "ecf97edd78eb2b76a30ae2adba6b66e5",
+    "DateCreated": "2023-12-11T12:10:40.7607527Z",
+    "CanDelete": false,
+    "CanDownload": false,
+    "SortName": "infest",
+    "PremiereDate": "2000-04-25T00:00:00.0000000Z",
+    "ExternalUrls": [
+        {
+            "Name": "MusicBrainz",
+            "Url": "https://musicbrainz.org/release/bf25b030-0cbb-495a-8d79-6c7fee20a089"
+        },
+        {
+            "Name": "MusicBrainz",
+            "Url": "https://musicbrainz.org/release-group/0193355a-cdfb-3936-afd2-44d651eb006d"
+        }
+    ],
+    "Path": "/media/music/Papa Roach/Infest",
+    "EnableMediaSourceDisplay": true,
+    "ChannelId": null,
+    "Taglines": [],
+    "Genres": [
+        "Alt Metal"
+    ],
+    "CumulativeRunTimeTicks": 27614273019,
+    "RunTimeTicks": 27614273019,
+    "PlayAccess": "Full",
+    "ProductionYear": 2000,
+    "RemoteTrailers": [],
+    "ProviderIds": {
+        "MusicBrainzAlbum": "bf25b030-0cbb-495a-8d79-6c7fee20a089",
+        "MusicBrainzReleaseGroup": "0193355a-cdfb-3936-afd2-44d651eb006d",
+        "MusicBrainzAlbumArtist": "c5eb9407-caeb-4303-b383-6929aa94021c"
+    },
+    "IsFolder": true,
+    "ParentId": "e439648e08ade14e27d5de48fa97c88e",
+    "Type": "MusicAlbum",
+    "People": [],
+    "Studios": [],
+    "GenreItems": [
+        {
+            "Name": "Alt Metal",
+            "Id": "7fae6ce8290515d5dfedc4e1894c1522"
+        }
+    ],
+    "ParentLogoItemId": "e439648e08ade14e27d5de48fa97c88e",
+    "ParentBackdropItemId": "e439648e08ade14e27d5de48fa97c88e",
+    "ParentBackdropImageTags": [
+        "c3d584db117d4c2bba5a975f391a965e"
+    ],
+    "LocalTrailerCount": 0,
+    "UserData": {
+        "PlaybackPositionTicks": 0,
+        "PlayCount": 0,
+        "IsFavorite": false,
+        "Played": false,
+        "Key": "MusicAlbum-MusicBrainzReleaseGroup-0193355a-cdfb-3936-afd2-44d651eb006d"
+    },
+    "RecursiveItemCount": 11,
+    "ChildCount": 11,
+    "SpecialFeatureCount": 0,
+    "DisplayPreferencesId": "f13d7f51d4f1f8b6fcd620855eb88c1e",
+    "Tags": [],
+    "PrimaryImageAspectRatio": 1,
+    "Artists": [
+        "Papa Roach"
+    ],
+    "ArtistItems": [
+        {
+            "Name": "Papa Roach",
+            "Id": "e439648e08ade14e27d5de48fa97c88e"
+        }
+    ],
+    "AlbumArtist": "Papa Roach",
+    "AlbumArtists": [
+        {
+            "Name": "Papa Roach",
+            "Id": "e439648e08ade14e27d5de48fa97c88e"
+        }
+    ],
+    "ImageTags": {
+        "Primary": "bcbe1ac159b0522743c9a0fe5401f948"
+    },
+    "BackdropImageTags": [],
+    "ParentLogoImageTag": "d58ea3bfadfb34e66033f55b8b2198c4",
+    "ImageBlurHashes": {
+        "Primary": {
+            "bcbe1ac159b0522743c9a0fe5401f948": "ecQb^8vf.S_2xY*0%hxDV[kXyYx^IUNGxt=ZsSNGV@njxuxuaKayS2"
+        },
+        "Logo": {
+            "d58ea3bfadfb34e66033f55b8b2198c4": "OQBftnWXD%WBNHoft7xaWBaej[fkoLay0Lax-:ofxZazRj"
+        },
+        "Backdrop": {
+            "c3d584db117d4c2bba5a975f391a965e": "W%F~5FodtRNGkCt6~Woet8Rkazs:-;j@ofoLWBkCxuWBays:axof"
+        }
+    },
+    "LocationType": "FileSystem",
+    "MediaType": "Unknown",
+    "LockedFields": [],
+    "LockData": false,
+    "NormalizationGain": -11
+}
\ No newline at end of file
diff --git a/tests/server/providers/jellyfin/test_init.py b/tests/server/providers/jellyfin/test_init.py
new file mode 100644 (file)
index 0000000..6d8418e
--- /dev/null
@@ -0,0 +1,55 @@
+"""Tests for the Jellyfin provider."""
+
+from collections.abc import AsyncGenerator
+from unittest import mock
+
+import pytest
+from aiojellyfin.testing import FixtureBuilder
+
+from music_assistant.common.models.config_entries import ProviderConfig
+from music_assistant.server.server import MusicAssistant
+from tests.common import get_fixtures_dir, wait_for_sync_completion
+
+
+@pytest.fixture
+async def jellyfin_provider(mass: MusicAssistant) -> AsyncGenerator[ProviderConfig, None]:
+    """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)
+
+    async for _, album in get_fixtures_dir("albums", "jellyfin"):
+        f.add_album_bytes(album)
+
+    async for _, track in get_fixtures_dir("tracks", "jellyfin"):
+        f.add_track_bytes(track)
+
+    authenticate_by_name = f.to_authenticate_by_name()
+
+    with mock.patch(
+        "music_assistant.server.providers.jellyfin.authenticate_by_name", authenticate_by_name
+    ):
+        async with wait_for_sync_completion(mass):
+            config = await mass.config.save_provider_config(
+                "jellyfin",
+                {
+                    "url": "http://localhost",
+                    "username": "username",
+                    "password": "password",
+                },
+            )
+
+        yield config
+
+
+@pytest.mark.usefixtures("jellyfin_provider")
+async def test_initial_sync(mass: MusicAssistant) -> None:
+    """Test that initial sync worked."""
+    artists = await mass.music.artists.library_items(search="Ash")
+    assert artists[0].name == "Ash"
+
+    albums = await mass.music.albums.library_items(search="christmas")
+    assert albums[0].name == "This Is Christmas"
+
+    tracks = await mass.music.tracks.library_items(search="where the bands are")
+    assert tracks[0].name == "Where the Bands Are (2018 Version)"