Chore: Subsonic: Clean up last of the typing errors and add tests (#2225)
authorEric Munson <eric@munsonfam.org>
Tue, 10 Jun 2025 21:16:03 +0000 (17:16 -0400)
committerGitHub <noreply@github.com>
Tue, 10 Jun 2025 21:16:03 +0000 (23:16 +0200)
Take the time to clean up the last of the strict type checker errors.
While in here, lets add test cases that excercise some of the work
aroudns we have for various subsonic servers.

Signed-off-by: Eric B Munson <eric@munsonfam.org>
14 files changed:
music_assistant/providers/opensubsonic/parsers.py
music_assistant/providers/opensubsonic/sonic_provider.py
tests/providers/opensubsonic/__snapshots__/test_parsers.ambr
tests/providers/opensubsonic/fixtures/albums/no-artist.album.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/albums/no-artist.info.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/artists/no-sort-name.artist.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/artists/no-sort-name.info.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/episodes/no-duration.episode.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/episodes/no-duration.podcast.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/podcasts/no-episodes.podcast.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/tracks/missing-items.album.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/tracks/missing-items.track.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/tracks/navi-various.album.json [new file with mode: 0644]
tests/providers/opensubsonic/fixtures/tracks/navi-various.track.json [new file with mode: 0644]

index 1f7d260991121c99f3156186ef0f07d22c25dfcb..95a1f8b0cd4e5836161678a8785204644755c0d4 100644 (file)
@@ -7,7 +7,7 @@ from datetime import datetime
 from typing import TYPE_CHECKING
 
 from music_assistant_models.enums import ContentType, ImageType, MediaType
-from music_assistant_models.errors import MediaNotFoundError
+from music_assistant_models.errors import InvalidDataError, MediaNotFoundError
 from music_assistant_models.media_items import (
     Album,
     Artist,
@@ -125,14 +125,14 @@ def parse_track(
                 available=True,
                 audio_format=AudioFormat(
                     content_type=ContentType.try_parse(sonic_song.content_type or "?"),
-                    sample_rate=sonic_song.sampling_rate if sonic_song.sampling_rate else 44100,
-                    bit_depth=sonic_song.bit_depth if sonic_song.bit_depth else 16,
-                    channels=sonic_song.channel_count if sonic_song.channel_count else 2,
-                    bit_rate=sonic_song.bit_rate if sonic_song.bit_rate else None,
+                    sample_rate=sonic_song.sampling_rate or 44100,
+                    bit_depth=sonic_song.bit_depth or 16,
+                    channels=sonic_song.channel_count or 2,
+                    bit_rate=sonic_song.bit_rate,
                 ),
             )
         },
-        track_number=sonic_song.track if sonic_song.track else 0,
+        track_number=sonic_song.track or 0,
     )
 
     if sonic_song.music_brainz_id:
@@ -151,17 +151,18 @@ def parse_track(
                 instance_id,
                 MediaType.ARTIST,
                 sonic_song.artist_id,
-                sonic_song.artist if sonic_song.artist else UNKNOWN_ARTIST,
+                sonic_song.artist or UNKNOWN_ARTIST,
             )
         )
 
-    for entry in sonic_song.artists:
-        if entry.id == sonic_song.artist_id:
-            continue
-        if entry.id is not None and entry.name is not None:
-            track.artists.append(
-                get_item_mapping(instance_id, MediaType.ARTIST, entry.id, entry.name)
-            )
+    if sonic_song.artists:
+        for entry in sonic_song.artists:
+            if entry.id == sonic_song.artist_id:
+                continue
+            if entry.id is not None and entry.name is not None:
+                track.artists.append(
+                    get_item_mapping(instance_id, MediaType.ARTIST, entry.id, entry.name)
+                )
 
     if not track.artists:
         if sonic_song.artist and not sonic_song.artist_id:
@@ -257,7 +258,7 @@ def parse_artist(
                 provider_instance=instance_id,
             )
         },
-        sort_name=sonic_artist.sort_name if sonic_artist.sort_name else None,
+        sort_name=sonic_artist.sort_name,
     )
 
     if sonic_artist.music_brainz_id:
@@ -340,7 +341,7 @@ def parse_album(
                 media_type=MediaType.ARTIST,
                 item_id=sonic_album.artist_id,
                 provider=instance_id,
-                name=sonic_album.artist if sonic_album.artist else UNKNOWN_ARTIST,
+                name=sonic_album.artist or UNKNOWN_ARTIST,
             )
         )
     elif not sonic_album.artists:
@@ -408,12 +409,16 @@ def parse_playlist(instance_id: str, sonic_playlist: SonicPlaylist) -> Playlist:
 
 def parse_podcast(instance_id: str, sonic_podcast: SonicPodcast) -> Podcast:
     """Parse Subsonic PodcastChannel into MA Podcast."""
+    if not sonic_podcast.title:
+        raise InvalidDataError(
+            f"Subsonic Podcast ({sonic_podcast.id})is missing required name field."
+        )
     podcast = Podcast(
         item_id=sonic_podcast.id,
         provider=SUBSONIC_DOMAIN,
         name=sonic_podcast.title,
         uri=sonic_podcast.url,
-        total_episodes=len(sonic_podcast.episode),
+        total_episodes=len(sonic_podcast.episode) if sonic_podcast.episode else 0,
         provider_mappings={
             ProviderMapping(
                 item_id=sonic_podcast.id,
@@ -465,7 +470,7 @@ def parse_epsiode(
                 provider_instance=instance_id,
             )
         },
-        duration=sonic_episode.duration,
+        duration=sonic_episode.duration or 0,
     )
 
     if sonic_episode.publish_date:
index 54de11b688fe255db5cd5cc0b5c0952d113917e4..bdff0adbb04b6bf2ab71774d1c6f719b46169a13 100644 (file)
@@ -89,18 +89,17 @@ class OpenSonicProvider(MusicProvider):
     async def handle_async_init(self) -> None:
         """Set up the music provider and test the connection."""
         port = self.config.get_value(CONF_PORT)
-        if port is None:
-            port = 443
+        port = int(str(port)) or 443
         path = self.config.get_value(CONF_PATH)
         if path is None:
             path = ""
         self.conn = SonicConnection(
-            self.config.get_value(CONF_BASE_URL),
-            username=self.config.get_value(CONF_USERNAME),
-            password=self.config.get_value(CONF_PASSWORD),
-            legacy_auth=self.config.get_value(CONF_ENABLE_LEGACY_AUTH),
+            str(self.config.get_value(CONF_BASE_URL)),
+            username=str(self.config.get_value(CONF_USERNAME)),
+            password=str(self.config.get_value(CONF_PASSWORD)),
+            legacy_auth=bool(self.config.get_value(CONF_ENABLE_LEGACY_AUTH)),
             port=port,
-            server_path=path,
+            server_path=str(path),
             app_name="Music Assistant",
         )
         try:
@@ -308,7 +307,7 @@ class OpenSonicProvider(MusicProvider):
             album: Album | None = None
             for entry in results.song:
                 aid = entry.album_id if entry.album_id else entry.parent
-                if album is None or album.item_id != aid:
+                if aid is not None and (album is None or album.item_id != aid):
                     album = await self.get_album(prov_album_id=aid)
                 yield parse_track(self.logger, self.instance_id, entry, album=album)
             offset += count
@@ -447,6 +446,9 @@ class OpenSonicProvider(MusicProvider):
             self.conn.get_podcasts, inc_episodes=True, pid=prov_podcast_id
         )
         channel = channels[0]
+        if not channel.episode:
+            return
+
         for episode in channel.episode:
             yield parse_epsiode(self.instance_id, episode, channel)
 
@@ -492,7 +494,7 @@ class OpenSonicProvider(MusicProvider):
             aid = sonic_song.album_id if sonic_song.album_id else sonic_song.parent
             if not aid:
                 self.logger.warning("Unable to find album for track %s", sonic_song.id)
-            if not album or album.item_id != aid:
+            if aid is not None and (not album or album.item_id != aid):
                 album = await self.get_album(prov_album_id=aid)
             track = parse_track(self.logger, self.instance_id, sonic_song, album=album)
             track.position = index
index 30ba1af169941e55cc65ebcc2d169c2757c94426..4bb485d0c12e7e08fd35bbd99673d949f6807e2a 100644 (file)
@@ -1,4 +1,270 @@
 # serializer version: 1
+# name: test_parse_albums[no-artist.album]
+  dict({
+    'album_type': 'unknown',
+    'artists': list([
+      dict({
+        'external_ids': list([
+        ]),
+        'favorite': False,
+        'is_playable': True,
+        'item_id': 'fake_artist_unknown',
+        'media_type': 'artist',
+        'metadata': dict({
+          'chapters': None,
+          'copyright': None,
+          'description': None,
+          'explicit': None,
+          'genres': None,
+          'images': None,
+          'label': None,
+          'languages': None,
+          'last_refresh': None,
+          'links': None,
+          'lrc_lyrics': None,
+          'lyrics': None,
+          'mood': None,
+          'performers': None,
+          'popularity': None,
+          'preview': None,
+          'release_date': None,
+          'review': None,
+          'style': None,
+        }),
+        'name': '[unknown]',
+        'position': None,
+        'provider': 'xx-instance-id-xx',
+        'provider_mappings': list([
+          dict({
+            'audio_format': dict({
+              'bit_depth': 16,
+              'bit_rate': 0,
+              'channels': 2,
+              'codec_type': '?',
+              'content_type': '?',
+              'output_format_str': '?',
+              'sample_rate': 44100,
+            }),
+            'available': True,
+            'details': None,
+            'item_id': 'fake_artist_unknown',
+            'provider_domain': 'opensubsonic',
+            'provider_instance': 'xx-instance-id-xx',
+            'url': None,
+          }),
+        ]),
+        'sort_name': 'unknown]',
+        'translation_key': None,
+        'uri': 'xx-instance-id-xx://artist/fake_artist_unknown',
+        'version': '',
+      }),
+    ]),
+    'external_ids': list([
+      list([
+        'musicbrainz_albumid',
+        '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      ]),
+    ]),
+    'favorite': True,
+    'is_playable': True,
+    'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35',
+    'media_type': 'album',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': None,
+      'explicit': None,
+      'genres': list([
+        'East coast',
+        'Hip-Hop',
+      ]),
+      'images': list([
+        dict({
+          'path': 'al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': False,
+          'type': 'thumb',
+        }),
+      ]),
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': 'slow',
+      'performers': None,
+      'popularity': None,
+      'preview': None,
+      'release_date': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': '8-bit lagerfeuer',
+    'position': None,
+    'provider': 'opensubsonic',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'lagerfeuer (8-bit)',
+    'translation_key': None,
+    'uri': 'opensubsonic://album/ad0f112b6dcf83de5e9cae85d07f0d35',
+    'version': '',
+    'year': 2007,
+  })
+# ---
+# name: test_parse_albums[no-artist.album].1
+  dict({
+    'album_type': 'unknown',
+    'artists': list([
+      dict({
+        'external_ids': list([
+        ]),
+        'favorite': False,
+        'is_playable': True,
+        'item_id': 'fake_artist_unknown',
+        'media_type': 'artist',
+        'metadata': dict({
+          'chapters': None,
+          'copyright': None,
+          'description': None,
+          'explicit': None,
+          'genres': None,
+          'images': None,
+          'label': None,
+          'languages': None,
+          'last_refresh': None,
+          'links': None,
+          'lrc_lyrics': None,
+          'lyrics': None,
+          'mood': None,
+          'performers': None,
+          'popularity': None,
+          'preview': None,
+          'release_date': None,
+          'review': None,
+          'style': None,
+        }),
+        'name': '[unknown]',
+        'position': None,
+        'provider': 'xx-instance-id-xx',
+        'provider_mappings': list([
+          dict({
+            'audio_format': dict({
+              'bit_depth': 16,
+              'bit_rate': 0,
+              'channels': 2,
+              'codec_type': '?',
+              'content_type': '?',
+              'output_format_str': '?',
+              'sample_rate': 44100,
+            }),
+            'available': True,
+            'details': None,
+            'item_id': 'fake_artist_unknown',
+            'provider_domain': 'opensubsonic',
+            'provider_instance': 'xx-instance-id-xx',
+            'url': None,
+          }),
+        ]),
+        'sort_name': 'unknown]',
+        'translation_key': None,
+        'uri': 'xx-instance-id-xx://artist/fake_artist_unknown',
+        'version': '',
+      }),
+    ]),
+    'external_ids': list([
+      list([
+        'musicbrainz_albumid',
+        '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      ]),
+    ]),
+    'favorite': True,
+    'is_playable': True,
+    'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35',
+    'media_type': 'album',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': 'Download the full release here (creative commons). These cripsy beats are ripe with thumping funk and techno influences, sample wizardry and daring shuffles. Composed with the help of unique sound plugins which were especially programmed to measure Comfort Fit’s needs and wishes, we think the chances aren’t bad that you’ll fall for the unique sound signature, bounce and elegance of this unusual Hip Hop production. Ltj bukem / Good looking Rec., UK: "Really love this music." Velanche / XLR8R, UK: "Awesome job he\'s done... overall production is dope." Kwesi / BBE Music, UK: "Wooooooowwwww... WHAT THE FUCK! THIS IS WHAT',
+      'explicit': None,
+      'genres': list([
+        'East coast',
+        'Hip-Hop',
+      ]),
+      'images': list([
+        dict({
+          'path': 'al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': False,
+          'type': 'thumb',
+        }),
+        dict({
+          'path': 'http://localhost:8989/play/art/0f8c3cbd6b0b22c3b5402141351ac812/album/21/thumb34.jpg',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': True,
+          'type': 'thumb',
+        }),
+      ]),
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': 'slow',
+      'performers': None,
+      'popularity': None,
+      'preview': None,
+      'release_date': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': '8-bit lagerfeuer',
+    'position': None,
+    'provider': 'opensubsonic',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'lagerfeuer (8-bit)',
+    'translation_key': None,
+    'uri': 'opensubsonic://album/ad0f112b6dcf83de5e9cae85d07f0d35',
+    'version': '',
+    'year': 2007,
+  })
+# ---
 # name: test_parse_albums[spec.album]
   dict({
     'album_type': 'unknown',
     'year': 2007,
   })
 # ---
-# name: test_parse_artists[spec-artistid3.artist]
+# name: test_parse_artists[no-sort-name.artist]
   dict({
     'external_ids': list([
       list([
         'url': None,
       }),
     ]),
-    'sort_name': 'Mello (2)',
+    'sort_name': '2 mello',
     'translation_key': None,
     'uri': 'opensubsonic://artist/37ec820ca7193e17040c98f7da7c4b51',
     'version': '',
   })
 # ---
-# name: test_parse_artists[spec-artistid3.artist].1
+# name: test_parse_artists[no-sort-name.artist].1
   dict({
     'external_ids': list([
       list([
         'url': None,
       }),
     ]),
-    'sort_name': 'Mello (2)',
+    'sort_name': '2 mello',
     'translation_key': None,
     'uri': 'opensubsonic://artist/37ec820ca7193e17040c98f7da7c4b51',
     'version': '',
   })
 # ---
-# name: test_parse_artists[spec-sample.artist]
+# name: test_parse_artists[spec-artistid3.artist]
   dict({
     'external_ids': list([
+      list([
+        'musicbrainz_artistid',
+        '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      ]),
     ]),
     'favorite': True,
     'is_playable': True,
-    'item_id': '100000002',
+    'item_id': '37ec820ca7193e17040c98f7da7c4b51',
     'media_type': 'artist',
     'metadata': dict({
       'chapters': None,
       'genres': None,
       'images': list([
         dict({
-          'path': 'ar-100000002',
+          'path': 'https://demo.org/image.jpg',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': True,
+          'type': 'thumb',
+        }),
+        dict({
+          'path': 'ar-37ec820ca7193e17040c98f7da7c4b51_0',
           'provider': 'xx-instance-id-xx',
           'remotely_accessible': False,
           'type': 'thumb',
       'review': None,
       'style': None,
     }),
-    'name': 'Synthetic',
+    'name': '2 Mello',
     'position': None,
     'provider': 'opensubsonic',
     'provider_mappings': list([
         }),
         'available': True,
         'details': None,
-        'item_id': '100000002',
+        'item_id': '37ec820ca7193e17040c98f7da7c4b51',
         'provider_domain': 'opensubsonic',
         'provider_instance': 'xx-instance-id-xx',
         'url': None,
       }),
     ]),
-    'sort_name': 'synthetic',
+    'sort_name': 'Mello (2)',
     'translation_key': None,
-    'uri': 'opensubsonic://artist/100000002',
+    'uri': 'opensubsonic://artist/37ec820ca7193e17040c98f7da7c4b51',
     'version': '',
   })
 # ---
-# name: test_parse_artists[spec-sample.artist].1
+# name: test_parse_artists[spec-artistid3.artist].1
   dict({
     'external_ids': list([
+      list([
+        'musicbrainz_artistid',
+        '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      ]),
     ]),
     'favorite': True,
     'is_playable': True,
-    'item_id': '100000002',
+    'item_id': '37ec820ca7193e17040c98f7da7c4b51',
     'media_type': 'artist',
     'metadata': dict({
       'chapters': None,
       'genres': None,
       'images': list([
         dict({
-          'path': 'ar-100000002',
+          'path': 'https://demo.org/image.jpg',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': True,
+          'type': 'thumb',
+        }),
+        dict({
+          'path': 'ar-37ec820ca7193e17040c98f7da7c4b51_0',
           'provider': 'xx-instance-id-xx',
           'remotely_accessible': False,
           'type': 'thumb',
       'review': None,
       'style': None,
     }),
-    'name': 'Synthetic',
+    'name': '2 Mello',
     'position': None,
     'provider': 'opensubsonic',
     'provider_mappings': list([
         }),
         'available': True,
         'details': None,
-        'item_id': '100000002',
+        'item_id': '37ec820ca7193e17040c98f7da7c4b51',
         'provider_domain': 'opensubsonic',
         'provider_instance': 'xx-instance-id-xx',
         'url': None,
       }),
     ]),
-    'sort_name': 'synthetic',
+    'sort_name': 'Mello (2)',
     'translation_key': None,
-    'uri': 'opensubsonic://artist/100000002',
+    'uri': 'opensubsonic://artist/37ec820ca7193e17040c98f7da7c4b51',
     'version': '',
   })
 # ---
-# name: test_parse_episode[gonic-sample.episode]
+# name: test_parse_artists[spec-sample.artist]
   dict({
-    'duration': 1878,
     'external_ids': list([
     ]),
-    'favorite': False,
-    'fully_played': None,
+    'favorite': True,
     'is_playable': True,
-    'item_id': 'pd-5$!$pe-1860',
-    'media_type': 'podcast_episode',
+    'item_id': '100000002',
+    'media_type': 'artist',
     'metadata': dict({
       'chapters': None,
       'copyright': None,
-      'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.',
+      'description': None,
       'explicit': None,
       'genres': None,
-      'images': None,
+      'images': list([
+        dict({
+          'path': 'ar-100000002',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': False,
+          'type': 'thumb',
+        }),
+      ]),
       'label': None,
       'languages': None,
       'last_refresh': None,
       'performers': None,
       'popularity': None,
       'preview': None,
-      'release_date': '2012-05-06T18:18:38+00:00',
+      'release_date': None,
       'review': None,
       'style': None,
     }),
-    'name': '179- The End',
+    'name': 'Synthetic',
+    'position': None,
+    'provider': 'opensubsonic',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': '100000002',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'synthetic',
+    'translation_key': None,
+    'uri': 'opensubsonic://artist/100000002',
+    'version': '',
+  })
+# ---
+# name: test_parse_artists[spec-sample.artist].1
+  dict({
+    'external_ids': list([
+    ]),
+    'favorite': True,
+    'is_playable': True,
+    'item_id': '100000002',
+    'media_type': 'artist',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': 'Empty biography',
+      'explicit': None,
+      'genres': None,
+      'images': list([
+        dict({
+          'path': 'ar-100000002',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': False,
+          'type': 'thumb',
+        }),
+        dict({
+          'path': 'http://localhost:8989/play/art/f20070e8e11611cc53542a38801d60fa/artist/2/thumb34.jpg',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': True,
+          'type': 'thumb',
+        }),
+      ]),
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': None,
+      'performers': None,
+      'popularity': None,
+      'preview': None,
+      'release_date': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': 'Synthetic',
+    'position': None,
+    'provider': 'opensubsonic',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': '100000002',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'synthetic',
+    'translation_key': None,
+    'uri': 'opensubsonic://artist/100000002',
+    'version': '',
+  })
+# ---
+# name: test_parse_episode[gonic-sample.episode]
+  dict({
+    'duration': 1878,
+    'external_ids': list([
+    ]),
+    'favorite': False,
+    'fully_played': None,
+    'is_playable': True,
+    'item_id': 'pd-5$!$pe-1860',
+    'media_type': 'podcast_episode',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.',
+      'explicit': None,
+      'genres': None,
+      'images': None,
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': None,
+      'performers': None,
+      'popularity': None,
+      'preview': None,
+      'release_date': '2012-05-06T18:18:38+00:00',
+      'review': None,
+      'style': None,
+    }),
+    'name': '179- The End',
+    'podcast': dict({
+      'external_ids': list([
+      ]),
+      'favorite': False,
+      'is_playable': True,
+      'item_id': 'pd-5',
+      'media_type': 'podcast',
+      'metadata': dict({
+        'chapters': None,
+        'copyright': None,
+        'description': 'A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!',
+        'explicit': None,
+        'genres': None,
+        'images': list([
+          dict({
+            'path': 'pd-5',
+            'provider': 'xx-instance-id-xx',
+            'remotely_accessible': False,
+            'type': 'thumb',
+          }),
+        ]),
+        'label': None,
+        'languages': None,
+        'last_refresh': None,
+        'links': None,
+        'lrc_lyrics': None,
+        'lyrics': None,
+        'mood': None,
+        'performers': None,
+        'popularity': None,
+        'preview': None,
+        'release_date': None,
+        'review': None,
+        'style': None,
+      }),
+      'name': 'The History of Rome',
+      'position': None,
+      'provider': 'opensubsonic',
+      'provider_mappings': list([
+        dict({
+          'audio_format': dict({
+            'bit_depth': 16,
+            'bit_rate': 0,
+            'channels': 2,
+            'codec_type': '?',
+            'content_type': '?',
+            'output_format_str': '?',
+            'sample_rate': 44100,
+          }),
+          'available': True,
+          'details': None,
+          'item_id': 'pd-5',
+          'provider_domain': 'opensubsonic',
+          'provider_instance': 'xx-instance-id-xx',
+          'url': None,
+        }),
+      ]),
+      'publisher': None,
+      'sort_name': 'history of rome, the',
+      'total_episodes': 5,
+      'translation_key': None,
+      'uri': 'http://feeds.feedburner.com/TheHistoryOfRome',
+      'version': '',
+    }),
+    'position': 5,
+    'provider': 'opensubsonic',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': 'pd-5$!$pe-1860',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'resume_position_ms': None,
+    'sort_name': '179- the end',
+    'translation_key': None,
+    'uri': 'opensubsonic://podcast_episode/pd-5$!$pe-1860',
+    'version': '',
+  })
+# ---
+# name: test_parse_episode[no-duration.episode]
+  dict({
+    'duration': 0,
+    'external_ids': list([
+    ]),
+    'favorite': False,
+    'fully_played': None,
+    'is_playable': True,
+    'item_id': 'pd-5$!$pe-1860',
+    'media_type': 'podcast_episode',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.',
+      'explicit': None,
+      'genres': None,
+      'images': None,
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': None,
+      'performers': None,
+      'popularity': None,
+      'preview': None,
+      'release_date': '2012-05-06T18:18:38+00:00',
+      'review': None,
+      'style': None,
+    }),
+    'name': '179- The End',
     'podcast': dict({
       'external_ids': list([
       ]),
     'version': '',
   })
 # ---
+# name: test_parse_podcast[no-episodes.podcast]
+  dict({
+    'external_ids': list([
+    ]),
+    'favorite': False,
+    'is_playable': True,
+    'item_id': 'pd-5',
+    'media_type': 'podcast',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': 'A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!',
+      'explicit': None,
+      'genres': None,
+      'images': list([
+        dict({
+          'path': 'pd-5',
+          'provider': 'xx-instance-id-xx',
+          'remotely_accessible': False,
+          'type': 'thumb',
+        }),
+      ]),
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': None,
+      'performers': None,
+      'popularity': None,
+      'preview': None,
+      'release_date': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': 'The History of Rome',
+    'position': None,
+    'provider': 'opensubsonic',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': 'pd-5',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'publisher': None,
+    'sort_name': 'history of rome, the',
+    'total_episodes': 0,
+    'translation_key': None,
+    'uri': 'http://feeds.feedburner.com/TheHistoryOfRome',
+    'version': '',
+  })
+# ---
+# name: test_parse_track[missing-items.track]
+  dict({
+    'album': dict({
+      'available': True,
+      'external_ids': list([
+      ]),
+      'image': None,
+      'is_playable': True,
+      'item_id': 'e8a0685e3f3ec6f251649af2b58b8617',
+      'media_type': 'album',
+      'name': 'Live at The Casbah - 2005-04-29',
+      'provider': 'xx-instance-id-xx',
+      'sort_name': 'live at the casbah - 2005-04-29',
+      'translation_key': None,
+      'uri': 'xx-instance-id-xx://album/e8a0685e3f3ec6f251649af2b58b8617',
+      'version': '',
+    }),
+    'artists': list([
+      dict({
+        'external_ids': list([
+        ]),
+        'favorite': False,
+        'is_playable': True,
+        'item_id': 'fake_artist_unknown',
+        'media_type': 'artist',
+        'metadata': dict({
+          'chapters': None,
+          'copyright': None,
+          'description': None,
+          'explicit': None,
+          'genres': None,
+          'images': None,
+          'label': None,
+          'languages': None,
+          'last_refresh': None,
+          'links': None,
+          'lrc_lyrics': None,
+          'lyrics': None,
+          'mood': None,
+          'performers': None,
+          'popularity': None,
+          'preview': None,
+          'release_date': None,
+          'review': None,
+          'style': None,
+        }),
+        'name': '[unknown]',
+        'position': None,
+        'provider': 'xx-instance-id-xx',
+        'provider_mappings': list([
+          dict({
+            'audio_format': dict({
+              'bit_depth': 16,
+              'bit_rate': 0,
+              'channels': 2,
+              'codec_type': '?',
+              'content_type': '?',
+              'output_format_str': '?',
+              'sample_rate': 44100,
+            }),
+            'available': True,
+            'details': None,
+            'item_id': 'fake_artist_unknown',
+            'provider_domain': 'opensubsonic',
+            'provider_instance': 'xx-instance-id-xx',
+            'url': None,
+          }),
+        ]),
+        'sort_name': 'unknown]',
+        'translation_key': None,
+        'uri': 'xx-instance-id-xx://artist/fake_artist_unknown',
+        'version': '',
+      }),
+    ]),
+    'disc_number': 0,
+    'duration': 0,
+    'external_ids': list([
+      list([
+        'musicbrainz_recordingid',
+        '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      ]),
+    ]),
+    'favorite': True,
+    'is_playable': True,
+    'item_id': '082f435a363c32c57d5edb6a678a28d4',
+    'media_type': 'track',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': None,
+      'explicit': True,
+      'genres': list([
+        'East coast',
+        'Hip-Hop',
+      ]),
+      'images': None,
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': 'slow',
+      'performers': list([
+        'Artist 3',
+        'Artist 4',
+        'Artist 5',
+      ]),
+      'popularity': None,
+      'preview': None,
+      'release_date': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': '"polar expedition"',
+    'position': None,
+    'provider': 'xx-instance-id-xx',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': '082f435a363c32c57d5edb6a678a28d4',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'Polar expedition',
+    'track_number': 0,
+    'translation_key': None,
+    'uri': 'xx-instance-id-xx://track/082f435a363c32c57d5edb6a678a28d4',
+    'version': '',
+  })
+# ---
+# name: test_parse_track[missing-items.track].1
+  dict({
+    'album': dict({
+      'artist': 'pornophonique',
+      'artistId': '97e0398acf63f9fb930d7d4ce209a52b',
+      'artists': list([
+        dict({
+          'id': 'ar-1',
+          'name': 'Artist 1',
+        }),
+        dict({
+          'id': 'ar-2',
+          'name': 'Artist 2',
+        }),
+      ]),
+      'coverArt': 'al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8',
+      'created': '2023-03-10T02:19:35.784818075Z',
+      'discTitles': list([
+        dict({
+          'disc': 0,
+          'title': 'Disc 0 title',
+        }),
+        dict({
+          'disc': 2,
+          'title': 'Disc 1 title',
+        }),
+      ]),
+      'displayArtist': 'Artist 1 feat. Artist 2',
+      'duration': 1954,
+      'explicitStatus': 'explicit',
+      'genre': 'Hip-Hop',
+      'genres': list([
+        dict({
+          'name': 'Hip-Hop',
+        }),
+        dict({
+          'name': 'East coast',
+        }),
+      ]),
+      'id': 'e8a0685e3f3ec6f251649af2b58b8617',
+      'isCompilation': False,
+      'moods': list([
+        'slow',
+        'cool',
+      ]),
+      'musicBrainzId': '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      'name': 'Live at The Casbah - 2005-04-29',
+      'originalReleaseDate': dict({
+        'day': 10,
+        'month': 3,
+        'year': 2001,
+      }),
+      'playCount': 97,
+      'played': '2023-03-28T00:45:13Z',
+      'releaseDate': dict({
+        'day': 10,
+        'month': 3,
+        'year': 2001,
+      }),
+      'releaseTypes': list([
+        'Album',
+        'Remixes',
+      ]),
+      'songCount': 8,
+      'sortName': 'lagerfeuer (8-bit)',
+      'starred': '2023-03-22T01:51:06Z',
+      'userRating': 4,
+      'version': 'Deluxe Edition',
+      'year': 2007,
+    }),
+    'artists': list([
+      dict({
+        'external_ids': list([
+        ]),
+        'favorite': False,
+        'is_playable': True,
+        'item_id': 'fake_artist_unknown',
+        'media_type': 'artist',
+        'metadata': dict({
+          'chapters': None,
+          'copyright': None,
+          'description': None,
+          'explicit': None,
+          'genres': None,
+          'images': None,
+          'label': None,
+          'languages': None,
+          'last_refresh': None,
+          'links': None,
+          'lrc_lyrics': None,
+          'lyrics': None,
+          'mood': None,
+          'performers': None,
+          'popularity': None,
+          'preview': None,
+          'release_date': None,
+          'review': None,
+          'style': None,
+        }),
+        'name': '[unknown]',
+        'position': None,
+        'provider': 'xx-instance-id-xx',
+        'provider_mappings': list([
+          dict({
+            'audio_format': dict({
+              'bit_depth': 16,
+              'bit_rate': 0,
+              'channels': 2,
+              'codec_type': '?',
+              'content_type': '?',
+              'output_format_str': '?',
+              'sample_rate': 44100,
+            }),
+            'available': True,
+            'details': None,
+            'item_id': 'fake_artist_unknown',
+            'provider_domain': 'opensubsonic',
+            'provider_instance': 'xx-instance-id-xx',
+            'url': None,
+          }),
+        ]),
+        'sort_name': 'unknown]',
+        'translation_key': None,
+        'uri': 'xx-instance-id-xx://artist/fake_artist_unknown',
+        'version': '',
+      }),
+    ]),
+    'disc_number': 0,
+    'duration': 0,
+    'external_ids': list([
+      list([
+        'musicbrainz_recordingid',
+        '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      ]),
+    ]),
+    'favorite': True,
+    'is_playable': True,
+    'item_id': '082f435a363c32c57d5edb6a678a28d4',
+    'media_type': 'track',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': None,
+      'explicit': True,
+      'genres': list([
+        'East coast',
+        'Hip-Hop',
+      ]),
+      'images': None,
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': 'slow',
+      'performers': list([
+        'Artist 3',
+        'Artist 4',
+        'Artist 5',
+      ]),
+      'popularity': None,
+      'preview': None,
+      'release_date': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': '"polar expedition"',
+    'position': None,
+    'provider': 'xx-instance-id-xx',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': '082f435a363c32c57d5edb6a678a28d4',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'Polar expedition',
+    'track_number': 0,
+    'translation_key': None,
+    'uri': 'xx-instance-id-xx://track/082f435a363c32c57d5edb6a678a28d4',
+    'version': '',
+  })
+# ---
+# name: test_parse_track[navi-various.track]
+  dict({
+    'album': dict({
+      'available': True,
+      'external_ids': list([
+      ]),
+      'image': None,
+      'is_playable': True,
+      'item_id': 'e8a0685e3f3ec6f251649af2b58b8617',
+      'media_type': 'album',
+      'name': 'Live at The Casbah - 2005-04-29',
+      'provider': 'xx-instance-id-xx',
+      'sort_name': 'live at the casbah - 2005-04-29',
+      'translation_key': None,
+      'uri': 'xx-instance-id-xx://album/e8a0685e3f3ec6f251649af2b58b8617',
+      'version': '',
+    }),
+    'artists': list([
+      dict({
+        'external_ids': list([
+        ]),
+        'favorite': False,
+        'is_playable': True,
+        'item_id': 'MA-NAVIDROME-The New Deal',
+        'media_type': 'artist',
+        'metadata': dict({
+          'chapters': None,
+          'copyright': None,
+          'description': None,
+          'explicit': None,
+          'genres': None,
+          'images': None,
+          'label': None,
+          'languages': None,
+          'last_refresh': None,
+          'links': None,
+          'lrc_lyrics': None,
+          'lyrics': None,
+          'mood': None,
+          'performers': None,
+          'popularity': None,
+          'preview': None,
+          'release_date': None,
+          'review': None,
+          'style': None,
+        }),
+        'name': 'The New Deal',
+        'position': None,
+        'provider': 'opensubsonic',
+        'provider_mappings': list([
+          dict({
+            'audio_format': dict({
+              'bit_depth': 16,
+              'bit_rate': 0,
+              'channels': 2,
+              'codec_type': '?',
+              'content_type': '?',
+              'output_format_str': '?',
+              'sample_rate': 44100,
+            }),
+            'available': True,
+            'details': None,
+            'item_id': 'MA-NAVIDROME-The New Deal',
+            'provider_domain': 'opensubsonic',
+            'provider_instance': 'xx-instance-id-xx',
+            'url': None,
+          }),
+        ]),
+        'sort_name': 'new deal, the',
+        'translation_key': None,
+        'uri': 'opensubsonic://artist/MA-NAVIDROME-The New Deal',
+        'version': '',
+      }),
+    ]),
+    'disc_number': 0,
+    'duration': 0,
+    'external_ids': list([
+      list([
+        'musicbrainz_recordingid',
+        '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      ]),
+    ]),
+    'favorite': True,
+    'is_playable': True,
+    'item_id': '082f435a363c32c57d5edb6a678a28d4',
+    'media_type': 'track',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': None,
+      'explicit': True,
+      'genres': list([
+        'East coast',
+        'Hip-Hop',
+      ]),
+      'images': None,
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': 'slow',
+      'performers': list([
+        'Artist 3',
+        'Artist 4',
+        'Artist 5',
+      ]),
+      'popularity': None,
+      'preview': None,
+      'release_date': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': '"polar expedition"',
+    'position': None,
+    'provider': 'xx-instance-id-xx',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': '082f435a363c32c57d5edb6a678a28d4',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'Polar expedition',
+    'track_number': 0,
+    'translation_key': None,
+    'uri': 'xx-instance-id-xx://track/082f435a363c32c57d5edb6a678a28d4',
+    'version': '',
+  })
+# ---
+# name: test_parse_track[navi-various.track].1
+  dict({
+    'album': dict({
+      'artist': 'pornophonique',
+      'artistId': '97e0398acf63f9fb930d7d4ce209a52b',
+      'artists': list([
+        dict({
+          'id': 'ar-1',
+          'name': 'Artist 1',
+        }),
+        dict({
+          'id': 'ar-2',
+          'name': 'Artist 2',
+        }),
+      ]),
+      'coverArt': 'al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8',
+      'created': '2023-03-10T02:19:35.784818075Z',
+      'discTitles': list([
+        dict({
+          'disc': 0,
+          'title': 'Disc 0 title',
+        }),
+        dict({
+          'disc': 2,
+          'title': 'Disc 1 title',
+        }),
+      ]),
+      'displayArtist': 'Artist 1 feat. Artist 2',
+      'duration': 1954,
+      'explicitStatus': 'explicit',
+      'genre': 'Hip-Hop',
+      'genres': list([
+        dict({
+          'name': 'Hip-Hop',
+        }),
+        dict({
+          'name': 'East coast',
+        }),
+      ]),
+      'id': 'e8a0685e3f3ec6f251649af2b58b8617',
+      'isCompilation': False,
+      'moods': list([
+        'slow',
+        'cool',
+      ]),
+      'musicBrainzId': '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      'name': 'Live at The Casbah - 2005-04-29',
+      'originalReleaseDate': dict({
+        'day': 10,
+        'month': 3,
+        'year': 2001,
+      }),
+      'playCount': 97,
+      'played': '2023-03-28T00:45:13Z',
+      'releaseDate': dict({
+        'day': 10,
+        'month': 3,
+        'year': 2001,
+      }),
+      'releaseTypes': list([
+        'Album',
+        'Remixes',
+      ]),
+      'songCount': 8,
+      'sortName': 'lagerfeuer (8-bit)',
+      'starred': '2023-03-22T01:51:06Z',
+      'userRating': 4,
+      'version': 'Deluxe Edition',
+      'year': 2007,
+    }),
+    'artists': list([
+      dict({
+        'external_ids': list([
+        ]),
+        'favorite': False,
+        'is_playable': True,
+        'item_id': 'MA-NAVIDROME-The New Deal',
+        'media_type': 'artist',
+        'metadata': dict({
+          'chapters': None,
+          'copyright': None,
+          'description': None,
+          'explicit': None,
+          'genres': None,
+          'images': None,
+          'label': None,
+          'languages': None,
+          'last_refresh': None,
+          'links': None,
+          'lrc_lyrics': None,
+          'lyrics': None,
+          'mood': None,
+          'performers': None,
+          'popularity': None,
+          'preview': None,
+          'release_date': None,
+          'review': None,
+          'style': None,
+        }),
+        'name': 'The New Deal',
+        'position': None,
+        'provider': 'opensubsonic',
+        'provider_mappings': list([
+          dict({
+            'audio_format': dict({
+              'bit_depth': 16,
+              'bit_rate': 0,
+              'channels': 2,
+              'codec_type': '?',
+              'content_type': '?',
+              'output_format_str': '?',
+              'sample_rate': 44100,
+            }),
+            'available': True,
+            'details': None,
+            'item_id': 'MA-NAVIDROME-The New Deal',
+            'provider_domain': 'opensubsonic',
+            'provider_instance': 'xx-instance-id-xx',
+            'url': None,
+          }),
+        ]),
+        'sort_name': 'new deal, the',
+        'translation_key': None,
+        'uri': 'opensubsonic://artist/MA-NAVIDROME-The New Deal',
+        'version': '',
+      }),
+    ]),
+    'disc_number': 0,
+    'duration': 0,
+    'external_ids': list([
+      list([
+        'musicbrainz_recordingid',
+        '189002e7-3285-4e2e-92a3-7f6c30d407a2',
+      ]),
+    ]),
+    'favorite': True,
+    'is_playable': True,
+    'item_id': '082f435a363c32c57d5edb6a678a28d4',
+    'media_type': 'track',
+    'metadata': dict({
+      'chapters': None,
+      'copyright': None,
+      'description': None,
+      'explicit': True,
+      'genres': list([
+        'East coast',
+        'Hip-Hop',
+      ]),
+      'images': None,
+      'label': None,
+      'languages': None,
+      'last_refresh': None,
+      'links': None,
+      'lrc_lyrics': None,
+      'lyrics': None,
+      'mood': 'slow',
+      'performers': list([
+        'Artist 3',
+        'Artist 4',
+        'Artist 5',
+      ]),
+      'popularity': None,
+      'preview': None,
+      'release_date': None,
+      'review': None,
+      'style': None,
+    }),
+    'name': '"polar expedition"',
+    'position': None,
+    'provider': 'xx-instance-id-xx',
+    'provider_mappings': list([
+      dict({
+        'audio_format': dict({
+          'bit_depth': 16,
+          'bit_rate': 0,
+          'channels': 2,
+          'codec_type': '?',
+          'content_type': '?',
+          'output_format_str': '?',
+          'sample_rate': 44100,
+        }),
+        'available': True,
+        'details': None,
+        'item_id': '082f435a363c32c57d5edb6a678a28d4',
+        'provider_domain': 'opensubsonic',
+        'provider_instance': 'xx-instance-id-xx',
+        'url': None,
+      }),
+    ]),
+    'sort_name': 'Polar expedition',
+    'track_number': 0,
+    'translation_key': None,
+    'uri': 'xx-instance-id-xx://track/082f435a363c32c57d5edb6a678a28d4',
+    'version': '',
+  })
+# ---
 # name: test_parse_track[spec-child.track]
   dict({
     'album': dict({
diff --git a/tests/providers/opensubsonic/fixtures/albums/no-artist.album.json b/tests/providers/opensubsonic/fixtures/albums/no-artist.album.json
new file mode 100644 (file)
index 0000000..ee448cb
--- /dev/null
@@ -0,0 +1,61 @@
+{
+    "id": "ad0f112b6dcf83de5e9cae85d07f0d35",
+    "name": "8-bit lagerfeuer",
+    "artist": "pornophonique",
+    "year": 2007,
+    "coverArt": "al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8",
+    "starred": "2023-03-22T01:51:06Z",
+    "duration": 1954,
+    "playCount": 97,
+    "genre": "Hip-Hop",
+    "created": "2023-03-10T02:19:35.784818075Z",
+    "songCount": 8,
+    "played": "2023-03-28T00:45:13Z",
+    "userRating": 4,
+    "recordLabels": [
+        {
+            "name": "Sony"
+        }
+    ],
+    "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2",
+    "genres": [
+        {
+            "name": "Hip-Hop"
+        },
+        {
+            "name": "East coast"
+        }
+    ],
+    "displayArtist": "Artist 1 feat. Artist 2",
+    "releaseTypes": [
+        "Album",
+        "Remixes"
+    ],
+    "moods": [
+        "slow",
+        "cool"
+    ],
+    "sortName": "lagerfeuer (8-bit)",
+    "originalReleaseDate": {
+        "year": 2001,
+        "month": 3,
+        "day": 10
+    },
+    "releaseDate": {
+        "year": 2001,
+        "month": 3,
+        "day": 10
+    },
+    "isCompilation": false,
+    "explicitStatus": "explicit",
+    "discTitles": [
+        {
+            "disc": 0,
+            "title": "Disc 0 title"
+        },
+        {
+            "disc": 2,
+            "title": "Disc 1 title"
+        }
+    ]
+}
diff --git a/tests/providers/opensubsonic/fixtures/albums/no-artist.info.json b/tests/providers/opensubsonic/fixtures/albums/no-artist.info.json
new file mode 100644 (file)
index 0000000..b971e1b
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "notes": "Download the full release here (creative commons). These cripsy beats are ripe with thumping funk and techno influences, sample wizardry and daring shuffles. Composed with the help of unique sound plugins which were especially programmed to measure Comfort Fit’s needs and wishes, we think the chances aren’t bad that you’ll fall for the unique sound signature, bounce and elegance of this unusual Hip Hop production. Ltj bukem / Good looking Rec., UK: \"Really love this music.\" Velanche / XLR8R, UK: \"Awesome job he's done... overall production is dope.\" Kwesi / BBE Music, UK: \"Wooooooowwwww... WHAT THE FUCK! THIS IS WHAT",
+    "musicBrainzId": "6e1d48f7-717c-416e-af35-5d2454a13af2",
+    "smallImageUrl": "http://localhost:8989/play/art/0f8c3cbd6b0b22c3b5402141351ac812/album/21/thumb34.jpg",
+    "mediumImageUrl": "http://localhost:8989/play/art/41b16680dc1b3aaf5dfba24ddb6a1712/album/21/thumb64.jpg",
+    "largeImageUrl": "http://localhost:8989/play/art/e6fd8d4e0d35c4436e56991892bfb27b/album/21/thumb174.jpg"
+}
diff --git a/tests/providers/opensubsonic/fixtures/artists/no-sort-name.artist.json b/tests/providers/opensubsonic/fixtures/artists/no-sort-name.artist.json
new file mode 100644 (file)
index 0000000..2b151c6
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "id": "37ec820ca7193e17040c98f7da7c4b51",
+    "name": "2 Mello",
+    "coverArt": "ar-37ec820ca7193e17040c98f7da7c4b51_0",
+    "albumCount": 1,
+    "userRating": 5,
+    "artistImageUrl": "https://demo.org/image.jpg",
+    "starred": "2017-04-11T10:42:50.842Z",
+    "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2",
+    "roles": [
+        "artist",
+        "albumartist",
+        "composer"
+    ]
+}
diff --git a/tests/providers/opensubsonic/fixtures/artists/no-sort-name.info.json b/tests/providers/opensubsonic/fixtures/artists/no-sort-name.info.json
new file mode 100644 (file)
index 0000000..8e64ec6
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "biography": "Empty biography",
+    "musicBrainzId": "1",
+    "smallImageUrl": "http://localhost:8989/play/art/f20070e8e11611cc53542a38801d60fa/artist/2/thumb34.jpg",
+    "mediumImageUrl": "http://localhost:8989/play/art/2b9b6c057cd4bf21089ce7572e7792b6/artist/2/thumb64.jpg",
+    "largeImageUrl": "http://localhost:8989/play/art/e18287c23a75e263b64c31b3d64c1944/artist/2/thumb174.jpg"
+}
diff --git a/tests/providers/opensubsonic/fixtures/episodes/no-duration.episode.json b/tests/providers/opensubsonic/fixtures/episodes/no-duration.episode.json
new file mode 100644 (file)
index 0000000..d44fd3d
--- /dev/null
@@ -0,0 +1,18 @@
+{
+    "id": "pe-1860",
+    "isDir": false,
+    "title": "179- The End",
+    "parent": "",
+    "year": 2012,
+    "genre": "Podcast",
+    "coverArt": "pd-5",
+    "size": 15032655,
+    "contentType": "audio/mpeg",
+    "suffix": "mp3",
+    "path": "",
+    "channelId": "pd-5",
+    "status": "completed",
+    "streamId": "pe-1860",
+    "description": "The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.",
+    "publishDate": "2012-05-06T18:18:38Z"
+}
diff --git a/tests/providers/opensubsonic/fixtures/episodes/no-duration.podcast.json b/tests/providers/opensubsonic/fixtures/episodes/no-duration.podcast.json
new file mode 100644 (file)
index 0000000..2cac9bc
--- /dev/null
@@ -0,0 +1,101 @@
+{
+    "id": "pd-5",
+    "url": "http://feeds.feedburner.com/TheHistoryOfRome",
+    "status": "skipped",
+    "title": "The History of Rome",
+    "description": "A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!",
+    "coverArt": "pd-5",
+    "originalImageUrl": "https://static.libsyn.com/p/assets/1/2/f/c/12fc067020662e91/THoR_Logo_1500x1500.jpg",
+    "episode": [
+        {
+            "id": "pe-4805",
+            "isDir": false,
+            "title": "Ad-Free History of Rome Patreon",
+            "parent": "",
+            "year": 2024,
+            "genre": "Podcast",
+            "coverArt": "pd-5",
+            "size": 1717520,
+            "contentType": "audio/mpeg",
+            "suffix": "mp3",
+            "path": "",
+            "channelId": "pd-5",
+            "status": "completed",
+            "streamId": "pe-4805",
+            "description": "Become a patron and get the entire History of Rome backcatalog ad-fre, plus bonus content, behind the scenes peeks at the new book, plus a chat community where you can talk to me directly. Join today! Patreon: patreon.com/thehistoryofrome Merch Store: cottonbureau.com/mikeduncan",
+            "publishDate": "2024-11-05T02:35:00Z"
+        },
+        {
+            "id": "pe-1857",
+            "isDir": false,
+            "title": "The Storm Before The Storm: Chapter 1- The Beasts of Italy",
+            "parent": "",
+            "year": 2017,
+            "genre": "Podcast",
+            "coverArt": "pd-5",
+            "size": 80207374,
+            "contentType": "audio/mpeg",
+            "suffix": "mp3",
+            "path": "",
+            "channelId": "pd-5",
+            "status": "completed",
+            "streamId": "pe-1857",
+            "description": "Audio excerpt from The Storm Before the Storm: The Beginning of the End of the Roman Republic by Mike Duncan. Forthcoming Oct. 24, 2017. Pre-order a copy today! Amazon Powells Barnes & Noble Indibound Books-a-Million Or visit us at: revolutionspodcast.com thehistoryofrome.com",
+            "publishDate": "2017-07-27T11:30:00Z"
+        },
+        {
+            "id": "pe-1858",
+            "isDir": false,
+            "title": "Revolutions Launch",
+            "parent": "",
+            "year": 2013,
+            "genre": "Podcast",
+            "coverArt": "pd-5",
+            "size": 253200,
+            "contentType": "audio/mpeg",
+            "suffix": "mp3",
+            "path": "",
+            "channelId": "pd-5",
+            "status": "completed",
+            "streamId": "pe-1858",
+            "description": "Available at revolutionspodcast.com, iTunes, or anywhere else fine podcasts can be found.",
+            "publishDate": "2013-09-16T15:39:57Z"
+        },
+        {
+            "id": "pe-1859",
+            "isDir": false,
+            "title": "Update- One Year Later",
+            "parent": "",
+            "year": 2013,
+            "genre": "Podcast",
+            "coverArt": "pd-5",
+            "size": 1588998,
+            "contentType": "audio/mpeg",
+            "suffix": "mp3",
+            "path": "",
+            "channelId": "pd-5",
+            "status": "completed",
+            "streamId": "pe-1859",
+            "description": "Next show coming soon!",
+            "publishDate": "2013-05-30T15:18:57Z"
+        },
+        {
+            "id": "pe-1860",
+            "isDir": false,
+            "title": "179- The End",
+            "parent": "",
+            "year": 2012,
+            "genre": "Podcast",
+            "coverArt": "pd-5",
+            "size": 15032655,
+            "contentType": "audio/mpeg",
+            "suffix": "mp3",
+            "path": "",
+            "channelId": "pd-5",
+            "status": "completed",
+            "streamId": "pe-1860",
+            "description": "The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.",
+            "publishDate": "2012-05-06T18:18:38Z"
+        }
+    ]
+}
diff --git a/tests/providers/opensubsonic/fixtures/podcasts/no-episodes.podcast.json b/tests/providers/opensubsonic/fixtures/podcasts/no-episodes.podcast.json
new file mode 100644 (file)
index 0000000..4c3f6e5
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "id": "pd-5",
+    "url": "http://feeds.feedburner.com/TheHistoryOfRome",
+    "status": "skipped",
+    "title": "The History of Rome",
+    "description": "A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!",
+    "coverArt": "pd-5",
+    "originalImageUrl": "https://static.libsyn.com/p/assets/1/2/f/c/12fc067020662e91/THoR_Logo_1500x1500.jpg"
+}
diff --git a/tests/providers/opensubsonic/fixtures/tracks/missing-items.album.json b/tests/providers/opensubsonic/fixtures/tracks/missing-items.album.json
new file mode 100644 (file)
index 0000000..2f81a24
--- /dev/null
@@ -0,0 +1,75 @@
+
+
+{
+    "id": "e8a0685e3f3ec6f251649af2b58b8617",
+    "name": "Live at The Casbah - 2005-04-29",
+    "version": "Deluxe Edition",
+    "artist": "pornophonique",
+    "year": 2007,
+    "coverArt": "al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8",
+    "starred": "2023-03-22T01:51:06Z",
+    "duration": 1954,
+    "playCount": 97,
+    "genre": "Hip-Hop",
+    "created": "2023-03-10T02:19:35.784818075Z",
+    "artistId": "97e0398acf63f9fb930d7d4ce209a52b",
+    "songCount": 8,
+    "played": "2023-03-28T00:45:13Z",
+    "userRating": 4,
+    "recordLabels": [
+        {
+            "name": "Sony"
+        }
+    ],
+    "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2",
+    "genres": [
+        {
+            "name": "Hip-Hop"
+        },
+        {
+            "name": "East coast"
+        }
+    ],
+    "artists": [
+        {
+            "id": "ar-1",
+            "name": "Artist 1"
+        },
+        {
+            "id": "ar-2",
+            "name": "Artist 2"
+        }
+    ],
+    "displayArtist": "Artist 1 feat. Artist 2",
+    "releaseTypes": [
+        "Album",
+        "Remixes"
+    ],
+    "moods": [
+        "slow",
+        "cool"
+    ],
+    "sortName": "lagerfeuer (8-bit)",
+    "originalReleaseDate": {
+        "year": 2001,
+        "month": 3,
+        "day": 10
+    },
+    "releaseDate": {
+        "year": 2001,
+        "month": 3,
+        "day": 10
+    },
+    "isCompilation": false,
+    "explicitStatus": "explicit",
+    "discTitles": [
+        {
+            "disc": 0,
+            "title": "Disc 0 title"
+        },
+        {
+            "disc": 2,
+            "title": "Disc 1 title"
+        }
+    ]
+}
diff --git a/tests/providers/opensubsonic/fixtures/tracks/missing-items.track.json b/tests/providers/opensubsonic/fixtures/tracks/missing-items.track.json
new file mode 100644 (file)
index 0000000..6c0d789
--- /dev/null
@@ -0,0 +1,78 @@
+{
+  "id": "082f435a363c32c57d5edb6a678a28d4",
+  "parent": "e8a0685e3f3ec6f251649af2b58b8617",
+  "isDir": false,
+  "title": "\"polar expedition\"",
+  "album": "Live at The Casbah - 2005-04-29",
+  "year": 2005,
+  "coverArt": "mf-082f435a363c32c57d5edb6a678a28d4_6410b3ce",
+  "size": 19866778,
+  "suffix": "flac",
+  "starred": "2023-03-27T09:45:27Z",
+  "path": "The New Deal/Live at The Casbah - 2005-04-29/04 - \"polar expedition\".flac",
+  "playCount": 8,
+  "played": "2023-03-26T22:27:46Z",
+  "created": "2023-03-14T17:51:22.112827504Z",
+  "albumId": "e8a0685e3f3ec6f251649af2b58b8617",
+  "type": "music",
+  "mediaType": "song",
+  "isVideo": false,
+  "bpm": 134,
+  "comment": "This is a song comment",
+  "sortName": "Polar expedition",
+  "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2",
+  "genres": [
+    {
+      "name": "Hip-Hop"
+    },
+    {
+      "name": "East coast"
+    }
+  ],
+  "displayArtist": "Artist 1 feat. Artist 2",
+  "displayAlbumArtist": "Artist 6 & Artist 7",
+  "contributors": [
+    {
+      "role": "composer",
+      "artist": {
+        "id": "ar-3",
+        "name": "Artist 3"
+      }
+    },
+    {
+      "role": "composer",
+      "artist": {
+        "id": "ar-4",
+        "name": "Artist 4"
+      }
+    },
+    {
+      "role": "lyricist",
+      "artist": {
+        "id": "ar-5",
+        "name": "Artist 5"
+      }
+    },
+    {
+      "role": "performer",
+      "subRole": "Bass",
+      "artist": {
+        "id": "ar-5",
+        "name": "Artist 5"
+      }
+    }
+  ],
+  "displayComposer": "Artist 3, Artist 4",
+  "moods": [
+    "slow",
+    "cool"
+  ],
+  "explicitStatus": "explicit",
+  "replayGain": {
+    "trackGain": 0.1,
+    "albumGain": 1.1,
+    "trackPeak": 9.2,
+    "albumPeak": 9,
+    "baseGain": 0
+  }
+}
diff --git a/tests/providers/opensubsonic/fixtures/tracks/navi-various.album.json b/tests/providers/opensubsonic/fixtures/tracks/navi-various.album.json
new file mode 100644 (file)
index 0000000..2f81a24
--- /dev/null
@@ -0,0 +1,75 @@
+
+
+{
+    "id": "e8a0685e3f3ec6f251649af2b58b8617",
+    "name": "Live at The Casbah - 2005-04-29",
+    "version": "Deluxe Edition",
+    "artist": "pornophonique",
+    "year": 2007,
+    "coverArt": "al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8",
+    "starred": "2023-03-22T01:51:06Z",
+    "duration": 1954,
+    "playCount": 97,
+    "genre": "Hip-Hop",
+    "created": "2023-03-10T02:19:35.784818075Z",
+    "artistId": "97e0398acf63f9fb930d7d4ce209a52b",
+    "songCount": 8,
+    "played": "2023-03-28T00:45:13Z",
+    "userRating": 4,
+    "recordLabels": [
+        {
+            "name": "Sony"
+        }
+    ],
+    "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2",
+    "genres": [
+        {
+            "name": "Hip-Hop"
+        },
+        {
+            "name": "East coast"
+        }
+    ],
+    "artists": [
+        {
+            "id": "ar-1",
+            "name": "Artist 1"
+        },
+        {
+            "id": "ar-2",
+            "name": "Artist 2"
+        }
+    ],
+    "displayArtist": "Artist 1 feat. Artist 2",
+    "releaseTypes": [
+        "Album",
+        "Remixes"
+    ],
+    "moods": [
+        "slow",
+        "cool"
+    ],
+    "sortName": "lagerfeuer (8-bit)",
+    "originalReleaseDate": {
+        "year": 2001,
+        "month": 3,
+        "day": 10
+    },
+    "releaseDate": {
+        "year": 2001,
+        "month": 3,
+        "day": 10
+    },
+    "isCompilation": false,
+    "explicitStatus": "explicit",
+    "discTitles": [
+        {
+            "disc": 0,
+            "title": "Disc 0 title"
+        },
+        {
+            "disc": 2,
+            "title": "Disc 1 title"
+        }
+    ]
+}
diff --git a/tests/providers/opensubsonic/fixtures/tracks/navi-various.track.json b/tests/providers/opensubsonic/fixtures/tracks/navi-various.track.json
new file mode 100644 (file)
index 0000000..38c4ebb
--- /dev/null
@@ -0,0 +1,79 @@
+{
+  "id": "082f435a363c32c57d5edb6a678a28d4",
+  "parent": "e8a0685e3f3ec6f251649af2b58b8617",
+  "isDir": false,
+  "title": "\"polar expedition\"",
+  "album": "Live at The Casbah - 2005-04-29",
+  "artist": "The New Deal",
+  "year": 2005,
+  "coverArt": "mf-082f435a363c32c57d5edb6a678a28d4_6410b3ce",
+  "size": 19866778,
+  "suffix": "flac",
+  "starred": "2023-03-27T09:45:27Z",
+  "path": "The New Deal/Live at The Casbah - 2005-04-29/04 - \"polar expedition\".flac",
+  "playCount": 8,
+  "played": "2023-03-26T22:27:46Z",
+  "created": "2023-03-14T17:51:22.112827504Z",
+  "albumId": "e8a0685e3f3ec6f251649af2b58b8617",
+  "type": "music",
+  "mediaType": "song",
+  "isVideo": false,
+  "bpm": 134,
+  "comment": "This is a song comment",
+  "sortName": "Polar expedition",
+  "musicBrainzId": "189002e7-3285-4e2e-92a3-7f6c30d407a2",
+  "genres": [
+    {
+      "name": "Hip-Hop"
+    },
+    {
+      "name": "East coast"
+    }
+  ],
+  "displayArtist": "Artist 1 feat. Artist 2",
+  "displayAlbumArtist": "Artist 6 & Artist 7",
+  "contributors": [
+    {
+      "role": "composer",
+      "artist": {
+        "id": "ar-3",
+        "name": "Artist 3"
+      }
+    },
+    {
+      "role": "composer",
+      "artist": {
+        "id": "ar-4",
+        "name": "Artist 4"
+      }
+    },
+    {
+      "role": "lyricist",
+      "artist": {
+        "id": "ar-5",
+        "name": "Artist 5"
+      }
+    },
+    {
+      "role": "performer",
+      "subRole": "Bass",
+      "artist": {
+        "id": "ar-5",
+        "name": "Artist 5"
+      }
+    }
+  ],
+  "displayComposer": "Artist 3, Artist 4",
+  "moods": [
+    "slow",
+    "cool"
+  ],
+  "explicitStatus": "explicit",
+  "replayGain": {
+    "trackGain": 0.1,
+    "albumGain": 1.1,
+    "trackPeak": 9.2,
+    "albumPeak": 9,
+    "baseGain": 0
+  }
+}