From 09e3966f46264e2692bf5ebe93083e779cc32fcf Mon Sep 17 00:00:00 2001 From: John Carr Date: Fri, 21 Jun 2024 16:14:01 +0100 Subject: [PATCH] Jellyfin: Add test scaffolding. --- .../server/providers/jellyfin/manifest.json | 2 +- requirements_all.txt | 2 +- tests/common.py | 47 ++++++++ tests/conftest.py | 10 +- .../jellyfin/__snapshots__/test_parsers.ambr | 80 +++++++++++++ .../jellyfin/fixtures/albums/infest.json | 105 ++++++++++++++++++ tests/server/providers/jellyfin/test_init.py | 55 +++++++++ 7 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 tests/common.py create mode 100644 tests/server/providers/jellyfin/fixtures/albums/infest.json create mode 100644 tests/server/providers/jellyfin/test_init.py diff --git a/music_assistant/server/providers/jellyfin/manifest.json b/music_assistant/server/providers/jellyfin/manifest.json index a21bed18..74714c7a 100644 --- a/music_assistant/server/providers/jellyfin/manifest.json +++ b/music_assistant/server/providers/jellyfin/manifest.json @@ -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 } diff --git a/requirements_all.txt b/requirements_all.txt index f8987964..12c0948f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 index 00000000..394540b2 --- /dev/null +++ b/tests/common.py @@ -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() diff --git a/tests/conftest.py b/tests/conftest.py index 6ebde314..fc25b096 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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: diff --git a/tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr b/tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr index c9f2020b..6de9723a 100644 --- a/tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr +++ b/tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr @@ -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 index 00000000..a7cbd805 --- /dev/null +++ b/tests/server/providers/jellyfin/fixtures/albums/infest.json @@ -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 index 00000000..6d8418ea --- /dev/null +++ b/tests/server/providers/jellyfin/test_init.py @@ -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)" -- 2.34.1