Fix inconsistencies in name+version parsing that affects mapping
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 20 Dec 2025 20:40:12 +0000 (21:40 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 20 Dec 2025 20:40:12 +0000 (21:40 +0100)
music_assistant/providers/apple_music/__init__.py
music_assistant/providers/deezer/__init__.py
music_assistant/providers/jellyfin/parsers.py
music_assistant/providers/nugs/__init__.py
music_assistant/providers/opensubsonic/parsers.py
music_assistant/providers/tidal/parsers.py
music_assistant/providers/ytmusic/__init__.py

index ac352a930661830c9037cd27220e5bc5e864af00..caf038a6cd30da3ef6e96124432764c913f87f00 100644 (file)
@@ -67,7 +67,7 @@ from music_assistant.helpers.auth import AuthenticationHelper
 from music_assistant.helpers.json import json_loads
 from music_assistant.helpers.playlists import fetch_playlist
 from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries
-from music_assistant.helpers.util import infer_album_type
+from music_assistant.helpers.util import infer_album_type, parse_title_and_version
 from music_assistant.models.music_provider import MusicProvider
 
 if TYPE_CHECKING:
@@ -762,10 +762,12 @@ class AppleMusicProvider(MusicProvider):
                 attributes.get("name"),
             )
             return None
+        name, version = parse_title_and_version(attributes["name"])
         album = Album(
             item_id=album_id,
             provider=self.domain,
-            name=attributes.get("name"),
+            name=name,
+            version=version,
             provider_mappings={
                 ProviderMapping(
                     item_id=album_id,
@@ -848,10 +850,12 @@ class AppleMusicProvider(MusicProvider):
         else:
             track_id = track_obj["id"]
             attributes = {}
+        name, version = parse_title_and_version(attributes.get("name", ""))
         track = Track(
             item_id=track_id,
             provider=self.domain,
-            name=attributes.get("name"),
+            name=name,
+            version=version,
             duration=attributes.get("durationInMillis", 0) / 1000,
             provider_mappings={
                 ProviderMapping(
index f3831cf1ddd8c38a6d70c088697d9c59275d1d5c..05dbead2c71319b3465bc1f9c1fc4faf805109c5 100644 (file)
@@ -47,7 +47,7 @@ from music_assistant.controllers.cache import use_cache
 from music_assistant.helpers.app_vars import app_var  # type: ignore[attr-defined]
 from music_assistant.helpers.auth import AuthenticationHelper
 from music_assistant.helpers.datetime import utc_timestamp
-from music_assistant.helpers.util import infer_album_type
+from music_assistant.helpers.util import infer_album_type, parse_title_and_version
 from music_assistant.models import ProviderInstanceType
 from music_assistant.models.music_provider import MusicProvider
 
@@ -615,11 +615,13 @@ class DeezerProvider(MusicProvider):
 
     def parse_album(self, album: deezer.Album) -> Album:
         """Parse the deezer-python album to a Music Assistant album."""
+        name, version = parse_title_and_version(album.title)
         return Album(
             album_type=self.get_album_type(album),
             item_id=str(album.id),
             provider=self.instance_id,
-            name=album.title,
+            name=name,
+            version=version,
             artists=UniqueList(
                 [
                     ItemMapping(
@@ -703,10 +705,12 @@ class DeezerProvider(MusicProvider):
         else:
             album = None
 
+        name, version = parse_title_and_version(track.title)
         item = Track(
             item_id=str(track.id),
             provider=self.instance_id,
-            name=track.title,
+            name=name,
+            version=version,
             sort_name=self.get_short_title(track),
             duration=track.duration,
             artists=UniqueList([artist]) if artist else UniqueList(),
index bac9aaf8c173309596fe3070fdade7a5c3e12602..e7c508252f809dcd09e7670db4a54f766798dbce 100644 (file)
@@ -21,6 +21,8 @@ from music_assistant_models.media_items import (
     UniqueList,
 )
 
+from music_assistant.helpers.util import parse_title_and_version
+
 from .const import (
     DOMAIN,
     ITEM_KEY_ALBUM,
@@ -65,10 +67,12 @@ def parse_album(
 ) -> Album:
     """Parse a Jellyfin Album response to an Album model object."""
     album_id = jellyfin_album[ITEM_KEY_ID]
+    name, version = parse_title_and_version(jellyfin_album[ITEM_KEY_NAME])
     album = Album(
         item_id=album_id,
         provider=DOMAIN,
-        name=jellyfin_album[ITEM_KEY_NAME],
+        name=name,
+        version=version,
         provider_mappings={
             ProviderMapping(
                 item_id=str(album_id),
@@ -195,12 +199,13 @@ def parse_track(
     logger: Logger, instance_id: str, client: Connection, jellyfin_track: JellyTrack
 ) -> Track:
     """Parse a Jellyfin Track response to a Track model object."""
-    available = False
     available = jellyfin_track[ITEM_KEY_CAN_DOWNLOAD]
+    name, version = parse_title_and_version(jellyfin_track[ITEM_KEY_NAME])
     track = Track(
         item_id=jellyfin_track[ITEM_KEY_ID],
         provider=instance_id,
-        name=jellyfin_track[ITEM_KEY_NAME],
+        name=name,
+        version=version,
         provider_mappings={
             ProviderMapping(
                 item_id=jellyfin_track[ITEM_KEY_ID],
index 380a8d34089cebadd64ac048a244d70ada9543f0..a30c6214465848d0fa3bcff9822322587939cb13 100644 (file)
@@ -41,7 +41,7 @@ from music_assistant_models.streamdetails import StreamDetails
 from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME
 from music_assistant.controllers.cache import use_cache
 from music_assistant.helpers.json import json_loads
-from music_assistant.helpers.util import infer_album_type
+from music_assistant.helpers.util import infer_album_type, parse_title_and_version
 from music_assistant.models.music_provider import MusicProvider
 
 if TYPE_CHECKING:
@@ -281,11 +281,12 @@ class NugsProvider(MusicProvider):
         """Parse nugs release/show/album object to generic album layout."""
         item_id = album_obj.get("releaseId") or album_obj.get("id") or album_obj.get("containerID")
         title = album_obj.get("title") or album_obj.get("containerInfo")
+        name, version = parse_title_and_version(str(title))
         album = Album(
             item_id=str(item_id),
             provider=self.instance_id,
-            name=str(title),
-            # version=album_obj["type"],
+            name=name,
+            version=version,
             provider_mappings={
                 ProviderMapping(
                     item_id=str(item_id),
@@ -327,7 +328,7 @@ class NugsProvider(MusicProvider):
             album.year = int(year)
 
         # No album type info in this provider so try and infer it
-        album.album_type = infer_album_type(album.name, "")
+        album.album_type = infer_album_type(album.name, album.version)
 
         return album
 
@@ -371,11 +372,13 @@ class NugsProvider(MusicProvider):
             track_obj.get("trackId") or track_obj.get("trackID") or track_obj.get("trackLabel")
         )
         track_name = track_obj.get("name") or track_obj.get("songTitle")
+        name, version = parse_title_and_version(str(track_name))
 
         track = Track(
             item_id=str(track_id),
             provider=self.instance_id,
-            name=str(track_name),
+            name=name,
+            version=version,
             provider_mappings={
                 ProviderMapping(
                     item_id=str(track_id),
index 406812b53f6e9c6af8e2308a99502537f8dbf48e..501234064bc36bd92f5f2c86d34526208a1024cf 100644 (file)
@@ -23,6 +23,7 @@ from music_assistant_models.media_items import (
 )
 
 from music_assistant.constants import UNKNOWN_ARTIST
+from music_assistant.helpers.util import parse_title_and_version
 
 if TYPE_CHECKING:
     from libopensonic.media import AlbumID3 as SonicAlbum
@@ -108,10 +109,12 @@ def parse_track(
         for c in sonic_song.contributors:
             metadata.performers.add(c.artist.name)
 
+    name, version = parse_title_and_version(sonic_song.title)
     track = Track(
         item_id=sonic_song.id,
         provider=instance_id,
-        name=sonic_song.title,
+        name=name,
+        version=version,
         album=album,
         duration=sonic_song.duration or 0,
         disc_number=sonic_song.disc_number or 0,
@@ -313,11 +316,13 @@ def parse_album(
     if sonic_album.moods:
         metadata.mood = sonic_album.moods[0]
 
+    name, version = parse_title_and_version(sonic_album.name)
     album = Album(
         item_id=sonic_album.id,
         provider=SUBSONIC_DOMAIN,
         metadata=metadata,
-        name=sonic_album.name,
+        name=name,
+        version=version,
         favorite=bool(sonic_album.starred),
         provider_mappings={
             ProviderMapping(
index 5911ae83b1ab84b7cb61db630c522e35b9353edb..30ea0847b07df5e5a4e9412dc6895a151e6aece6 100644 (file)
@@ -24,7 +24,7 @@ from music_assistant_models.media_items import (
     UniqueList,
 )
 
-from music_assistant.helpers.util import infer_album_type
+from music_assistant.helpers.util import infer_album_type, parse_title_and_version
 
 from .constants import BROWSE_URL, RESOURCES_URL
 
@@ -70,8 +70,10 @@ def parse_artist(provider: TidalProvider, artist_obj: dict[str, Any]) -> Artist:
 
 def parse_album(provider: TidalProvider, album_obj: dict[str, Any]) -> Album:
     """Parse tidal album object to generic layout."""
-    name = album_obj.get("title", "Unknown Album")
-    version = album_obj.get("version", "") or ""
+    name, version = parse_title_and_version(
+        album_obj.get("title", "Unknown Album"),
+        album_obj.get("version") or None,
+    )
     album_id = str(album_obj.get("id", ""))
 
     album = Album(
@@ -162,7 +164,10 @@ def parse_track(
     lyrics: dict[str, str] | None = None,
 ) -> Track:
     """Parse tidal track object to generic layout."""
-    version = track_obj.get("version", "") or ""
+    name, version = parse_title_and_version(
+        track_obj.get("title", "Unknown"),
+        track_obj.get("version") or None,
+    )
     track_id = str(track_obj.get("id", 0))
     media_metadata = track_obj.get("mediaMetadata") or {}
     tags = media_metadata.get("tags", [])
@@ -170,7 +175,7 @@ def parse_track(
     track = Track(
         item_id=track_id,
         provider=provider.instance_id,
-        name=track_obj.get("title", "Unknown"),
+        name=name,
         version=version,
         duration=track_obj.get("duration", 0),
         provider_mappings={
index 9cc5981af73dc9da7747faffda629cd75de1a099..592c7da403fbb93865eb22a857c13a48df93b33a 100644 (file)
@@ -54,7 +54,7 @@ from ytmusicapi.helpers import get_authorization, sapisid_from_cookie
 
 from music_assistant.constants import CONF_USERNAME, VERBOSE_LOG_LEVEL
 from music_assistant.controllers.cache import use_cache
-from music_assistant.helpers.util import infer_album_type, install_package
+from music_assistant.helpers.util import infer_album_type, install_package, parse_title_and_version
 from music_assistant.models.music_provider import MusicProvider
 
 from .helpers import (
@@ -740,12 +740,15 @@ class YoutubeMusicProvider(MusicProvider):
             raise InvalidDataError("Album ID is required but not found")
 
         if "title" in album_obj:
-            name = album_obj["title"]
+            name, version = parse_title_and_version(album_obj["title"])
         elif "name" in album_obj:
-            name = album_obj["name"]
+            name, version = parse_title_and_version(album_obj["name"])
+        else:
+            name, version = "", ""
         album = Album(
             item_id=album_id,
             name=name,
+            version=version,
             provider=self.instance_id,
             provider_mappings={
                 ProviderMapping(
@@ -787,7 +790,7 @@ class YoutubeMusicProvider(MusicProvider):
             album.album_type = album_type
 
         # Try inference - override if it finds something more specific
-        inferred_type = infer_album_type(name, "")  # YouTube doesn't seem to have version field
+        inferred_type = infer_album_type(name, version)
         if inferred_type in (AlbumType.SOUNDTRACK, AlbumType.LIVE):
             album.album_type = inferred_type
 
@@ -873,10 +876,12 @@ class YoutubeMusicProvider(MusicProvider):
             msg = "Track is missing videoId"
             raise InvalidDataError(msg)
         track_id = str(track_obj["videoId"])
+        name, version = parse_title_and_version(track_obj["title"])
         track = Track(
             item_id=track_id,
             provider=self.instance_id,
-            name=track_obj["title"],
+            name=name,
+            version=version,
             provider_mappings={
                 ProviderMapping(
                     item_id=track_id,