Some fixes for the database migration and matching logic (#958)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 8 Dec 2023 18:02:43 +0000 (19:02 +0100)
committerGitHub <noreply@github.com>
Fri, 8 Dec 2023 18:02:43 +0000 (19:02 +0100)
music_assistant/server/controllers/media/albums.py
music_assistant/server/controllers/media/artists.py
music_assistant/server/controllers/media/tracks.py
music_assistant/server/controllers/music.py
music_assistant/server/helpers/compare.py
music_assistant/server/providers/sonos/__init__.py

index e93384ec5b5a2dd6ec5402f921414ffe27638a94..10d9c75c6a7a00c42769ba2809dfcc5201ec1394 100644 (file)
@@ -394,7 +394,7 @@ class AlbumsController(MediaControllerBase[Album]):
                 for search_result_item in search_result:
                     if not search_result_item.available:
                         continue
-                    if not compare_album(search_result_item, db_album):
+                    if not compare_album(db_album, search_result_item):
                         continue
                     # we must fetch the full album version, search results are simplified objects
                     prov_album = await self.get_provider_item(
@@ -402,7 +402,7 @@ class AlbumsController(MediaControllerBase[Album]):
                         search_result_item.provider,
                         fallback=search_result_item,
                     )
-                    if compare_album(prov_album, db_album):
+                    if compare_album(db_album, prov_album):
                         # 100% match, we update the db with the additional provider mapping(s)
                         match_found = True
                         for provider_mapping in search_result_item.provider_mappings:
index 6ba375ed4a757e292f1411e5ee64046eec01d4d0..6e8cbd151fd41cead7edcedb8edb26f6afcc4872 100644 (file)
@@ -491,7 +491,9 @@ class ArtistsController(MediaControllerBase[Artist]):
                     if search_result_item.sort_name != ref_album.sort_name:
                         continue
                     # artist must match 100%
-                    if not compare_artist(search_result_item.artists[0], db_artist):
+                    if not compare_artist(
+                        db_artist, search_result_item.artists[0], allow_name_match=True
+                    ):
                         continue
                     # 100% match
                     # get full artist details so we have all metadata
index 81c1cc7eab2e4e02fcf0f30ce840f1936eddcf91..4f3de56ae28b1c3d6629df4eada9d7be4b9dc594 100644 (file)
@@ -327,7 +327,7 @@ class TracksController(MediaControllerBase[Track]):
                     if not search_result_item.available:
                         continue
                     # do a basic compare first
-                    if not compare_track(search_result_item, db_track, strict=False):
+                    if not compare_track(db_track, search_result_item, strict=False):
                         continue
                     # we must fetch the full version, search results are simplified objects
                     prov_track = await self.get_provider_item(
@@ -335,7 +335,7 @@ class TracksController(MediaControllerBase[Track]):
                         search_result_item.provider,
                         fallback=search_result_item,
                     )
-                    if compare_track(prov_track, db_track, strict=True, track_albums=track_albums):
+                    if compare_track(db_track, prov_track, strict=True, track_albums=track_albums):
                         # 100% match, we update the db with the additional provider mapping(s)
                         match_found = True
                         for provider_mapping in search_result_item.provider_mappings:
index 972167d521ca51a89d0aa79b5713de6dec5ce052..96b00cadefabfca0258aca843d3a623b4c387671 100755 (executable)
@@ -4,7 +4,6 @@ from __future__ import annotations
 import asyncio
 import os
 import shutil
-import sqlite3
 import statistics
 from collections.abc import AsyncGenerator
 from contextlib import suppress
@@ -668,6 +667,8 @@ class MusicController(CoreController):
                         "ADD COLUMN external_ids "
                         "json NOT NULL DEFAULT '[]'"
                     )
+                    if table in (DB_TABLE_PLAYLISTS, DB_TABLE_RADIOS):
+                        continue
                     # migrate existing ids into the new external_ids column
                     async for item in self.database.iter_items(table):
                         external_ids: set[tuple[str, str]] = set()
@@ -696,24 +697,6 @@ class MusicController(CoreController):
                     "Database migration to version %s completed",
                     DB_SCHEMA_VERSION,
                 )
-            elif prev_version == 26:
-                self.logger.info(
-                    "Performing database migration from %s to %s",
-                    prev_version,
-                    DB_SCHEMA_VERSION,
-                )
-                # migrate playlists and radios tables which we forgot to migrate in schema 26
-                for table in (
-                    DB_TABLE_PLAYLISTS,
-                    DB_TABLE_RADIOS,
-                ):
-                    # create new external_ids column
-                    with suppress(sqlite3.OperationalError):
-                        await self.database.execute(
-                            f"ALTER TABLE {table} "
-                            "ADD COLUMN external_ids "
-                            "json NOT NULL DEFAULT '[]'"
-                        )
             # handle all other schema versions
             else:
                 # we keep it simple and just recreate the tables
index 52fae88e8ef03c1dbbdf21bf5b1e04fac841c637..5919708e025fc88bbf0e4b362a34cc18b66a76d6 100644 (file)
@@ -74,7 +74,7 @@ def compare_album(
     if (
         hasattr(base_item, "metadata")
         and hasattr(compare_item, "metadata")
-        and not compare_explicit(base_item.metadata, compare_item.metadata)
+        and compare_explicit(base_item.metadata, compare_item.metadata) is False
     ):
         return False
     # compare album artist
@@ -105,6 +105,7 @@ def compare_track(
     external_id_match = compare_external_ids(base_item.external_ids, compare_item.external_ids)
     if external_id_match is not None:
         return external_id_match
+
     ## fallback to comparing on attributes
     # compare name
     if not compare_strings(base_item.name, compare_item.name, strict=True):
@@ -120,9 +121,9 @@ def compare_track(
         base_item.metadata.explicit = base_item.album.metadata.explicit
     if compare_item.metadata.explicit is None and isinstance(compare_item.album, Album):
         compare_item.metadata.explicit = compare_item.album.metadata.explicit
-    if strict and not compare_explicit(base_item.metadata, compare_item.metadata):
+    if strict and compare_explicit(base_item.metadata, compare_item.metadata) is False:
         return False
-    if not strict and not track_albums:
+    if not strict and not (base_item.album or track_albums):
         # in non-strict mode, the album does not have to match
         return abs(base_item.duration - compare_item.duration) <= 3
     # exact albumtrack match = 100% match
@@ -138,14 +139,14 @@ def compare_track(
         base_item.album is not None
         and compare_item.album is not None
         and compare_album(base_item.album, compare_item.album)
-        and abs(base_item.duration - compare_item.duration) <= 5
+        and abs(base_item.duration - compare_item.duration) <= 3
     ):
         return True
     # fallback: additional compare albums provided for base track
     if (
         compare_item.album is not None
         and track_albums
-        and abs(base_item.duration - compare_item.duration) <= 5
+        and abs(base_item.duration - compare_item.duration) <= 3
     ):
         for track_album in track_albums:
             if compare_album(track_album, compare_item.album):
@@ -154,7 +155,7 @@ def compare_track(
     if (
         base_item.album is None
         and compare_item.album is None
-        and abs(base_item.duration - compare_item.duration) <= 3
+        and abs(base_item.duration - compare_item.duration) <= 1
     ):
         return True
 
@@ -243,10 +244,17 @@ def compare_external_ids(
             # handle upc stored as EAN-13 barcode
             if external_id_base[0] == ExternalID.BARCODE and len(external_id_base[1]) == 12:
                 external_id_base[1] = f"0{external_id_base}"
-            if external_id_compare[0] == ExternalID.BARCODE and len(external_id_compare[1]) == 12:
+            if external_id_compare[1] == ExternalID.BARCODE and len(external_id_compare[1]) == 12:
                 external_id_compare[1] = f"0{external_id_compare}"
-            # external id is exact match. either it is a match or it isn't
-            return external_id_compare[0] == external_id_base[0]
+            if external_id_base[0] in (ExternalID.ISRC, ExternalID.BARCODE):
+                if external_id_compare[1] == external_id_base[1]:
+                    # barcode and isrc can be multiple per media item
+                    # so we only return early on match as there might be
+                    # another entry for this ExternalID type.
+                    return True
+                continue
+            # other ExternalID types: external id must be exact match.
+            return external_id_compare[1] == external_id_base[1]
     # return None to define we did not find the same external id type in both sets
     return None
 
index 207629164feafb4a056ffa4aad73154a30852239..007795d33761bd3d1c1f9838f56f07bb67ff5205 100644 (file)
@@ -176,7 +176,11 @@ class SonosPlayer:
             self.player.elapsed_time_last_updated = self.track_info_updated
 
         # zone topology (syncing/grouping) details
-        if self.group_info and self.group_info.coordinator.uid == self.player_id:
+        if (
+            self.group_info
+            and self.group_info.coordinator
+            and self.group_info.coordinator.uid == self.player_id
+        ):
             # this player is the sync leader
             self.player.synced_to = None
             group_members = {x.uid for x in self.group_info.members if x.is_visible}
@@ -375,6 +379,10 @@ class SonosPlayerProvider(PlayerProvider):
         """
         sonos_player = self.sonosplayers[player_id]
         await asyncio.to_thread(sonos_player.soco.join, self.sonosplayers[target_player].soco)
+        await asyncio.to_thread(
+            sonos_player.update_info,
+            update_group_info=True,
+        )
 
     async def cmd_unsync(self, player_id: str) -> None:
         """Handle UNSYNC command for given player.
@@ -385,6 +393,10 @@ class SonosPlayerProvider(PlayerProvider):
         """
         sonos_player = self.sonosplayers[player_id]
         await asyncio.to_thread(sonos_player.soco.unjoin)
+        await asyncio.to_thread(
+            sonos_player.update_info,
+            update_group_info=True,
+        )
 
     async def poll_player(self, player_id: str) -> None:
         """Poll player for state updates.
@@ -431,7 +443,10 @@ class SonosPlayerProvider(PlayerProvider):
             for device in discovered_devices:
                 if device.uid not in added_devices:
                     continue
-                await self._device_discovered(device)
+                try:
+                    await self._device_discovered(device)
+                except Exception as err:
+                    self.logger.exception(str(err), exc_info=err)
 
         finally:
             self._discovery_running = False