From 2f14b6e6c4cc2ee205a61c64e36b838dabf9fda2 Mon Sep 17 00:00:00 2001 From: cwilvx Date: Mon, 16 Jun 2025 20:29:47 +0200 Subject: [PATCH 1/4] fix: KeyError on missing artists --- swingmusic/lib/recipes/because.py | 3 +++ swingmusic/plugins/mixes.py | 25 ++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/swingmusic/lib/recipes/because.py b/swingmusic/lib/recipes/because.py index 453d2489..e8f241d3 100644 --- a/swingmusic/lib/recipes/because.py +++ b/swingmusic/lib/recipes/because.py @@ -29,6 +29,9 @@ class BecauseYouListened(HomepageRoutine): MixesPlugin().get_because_items(list(entry.values())) ) + if not because_you_listened_to_artist or not artists_you_might_like: + continue + HomepageStore.entries[self.store_keys[0]].items[ user.id ] = because_you_listened_to_artist diff --git a/swingmusic/plugins/mixes.py b/swingmusic/plugins/mixes.py index dbcc6122..a5bb1799 100644 --- a/swingmusic/plugins/mixes.py +++ b/swingmusic/plugins/mixes.py @@ -531,7 +531,23 @@ class MixesPlugin(Plugin): artists: dict[str, list[dict[str, str | int]]] = {} albums: dict[str, list[dict[str, str | int]]] = {} - for mix in mixes: + pivot_artist = None + pivot_artist_index = None + + # Get pivot artist + for index, mix in enumerate(mixes): + artist = ArtistStore.artistmap.get(mix.extra["artisthash"]) + if not artist: + continue + + pivot_artist = artist.artist + pivot_artist_index = index + break + + if not pivot_artist: + return None, None + + for mix in mixes[pivot_artist_index:]: mix_artisthash = mix.extra["artisthash"] artists.setdefault(mix_artisthash, []) albums.setdefault(mix_artisthash, []) @@ -582,11 +598,10 @@ class MixesPlugin(Plugin): reverse=True, ) - artisthash = mixes[0].extra["artisthash"] because_you_listened_to_artist = { "title": "Because you listened to " - + ArtistStore.artistmap[artisthash].artist.name, - "items": albums[artisthash][:15], + + pivot_artist.name, + "items": albums[pivot_artist.artisthash][:15], } # Flatten list of artists and remove duplicates by artisthash @@ -601,7 +616,7 @@ class MixesPlugin(Plugin): artists_you_might_like = { "title": "Artists you might like", - "items": artists[artisthash][:15], + "items": artists[pivot_artist.artisthash][:15], } return because_you_listened_to_artist, artists_you_might_like From 39fe3598646437953860192a6add987b8138217f Mon Sep 17 00:00:00 2001 From: cwilvx Date: Tue, 17 Jun 2025 09:47:16 +0200 Subject: [PATCH 2/4] switch to forking processes on Unix --- swingmusic/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swingmusic/__main__.py b/swingmusic/__main__.py index d0236a39..a4acb5fe 100644 --- a/swingmusic/__main__.py +++ b/swingmusic/__main__.py @@ -110,7 +110,7 @@ def run(*args, **kwargs): def main(): multiprocessing.freeze_support() - multiprocessing.set_start_method("spawn") + multiprocessing.set_start_method("fork") run() if __name__ == "__main__": From 0934e4ed7af0fcd923b68f5e3a003b4faeacba75 Mon Sep 17 00:00:00 2001 From: cwilvx Date: Tue, 17 Jun 2025 09:47:41 +0200 Subject: [PATCH 3/4] fix: integrity errors when restoring backups --- swingmusic/api/backup_and_restore.py | 33 +++++++++++++++++++++++----- swingmusic/db/userdata.py | 14 +++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/swingmusic/api/backup_and_restore.py b/swingmusic/api/backup_and_restore.py index 771544c1..63ac5aaa 100644 --- a/swingmusic/api/backup_and_restore.py +++ b/swingmusic/api/backup_and_restore.py @@ -7,6 +7,7 @@ import shutil from time import time from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint +import sqlalchemy.exc from swingmusic.api.auth import admin_required from swingmusic.db.userdata import FavoritesTable, PlaylistTable, ScrobbleTable @@ -96,6 +97,9 @@ def backup(): class RestoreBackup: + # TODO: BACKUP AND RESTORE COLLECTIONS & MIXES! + # TODO: IMPROVE UX WHEN WAITING FOR RESTORE TO COMPLETE! + def __init__(self, backup_dir: Path): self.backup_dir = backup_dir self.backup_file = backup_dir / "data.json" @@ -114,8 +118,12 @@ class RestoreBackup: existing_hashes = set(fav.hash for fav in existing_favorites) new_favorites = [fav for fav in favorites if fav["hash"] not in existing_hashes] - if new_favorites: - FavoritesTable.insert_many(new_favorites) + for fav in new_favorites: + try: + FavoritesTable.insert_item(fav) + except sqlalchemy.exc.IntegrityError: + print("Integrity error, skipping favorite") + print(fav) def restore_playlists(self, playlists: list[dict]): existing_playlists = PlaylistTable.get_all() @@ -124,8 +132,16 @@ class RestoreBackup: playlist for playlist in playlists if playlist["name"] not in existing_names ] - if new_playlists: - PlaylistTable.insert_many(new_playlists) + for playlist in new_playlists: + try: + if playlist.get("_score") is not None: + print("Removing _score from playlist") + del playlist["_score"] + + PlaylistTable.add_one(playlist) + except sqlalchemy.exc.IntegrityError: + print("Integrity error, skipping playlist") + print(playlist) def restore_scrobbles(self, scrobbles: list[dict]): existing_scrobbles = ScrobbleTable.get_all(0) @@ -139,8 +155,13 @@ class RestoreBackup: if f"{scrobble['trackhash']}.{scrobble['timestamp']}" not in existing_hashes ] - if new_scrobbles: - ScrobbleTable.insert_many(new_scrobbles) + for scrobble in new_scrobbles: + try: + ScrobbleTable.add(scrobble) + except sqlalchemy.exc.IntegrityError: + print("Integrity error, skipping scrobble") + print(scrobble) + class RestoreBackupBody(BaseModel): diff --git a/swingmusic/db/userdata.py b/swingmusic/db/userdata.py index 525d7a33..1f33d2d5 100644 --- a/swingmusic/db/userdata.py +++ b/swingmusic/db/userdata.py @@ -219,8 +219,13 @@ class FavoritesTable(Base): # guard against hash collisions for different item types item["hash"] = f"{item['type']}_{item['hash']}" - item["timestamp"] = int(datetime.datetime.now().timestamp()) - item["userid"] = get_current_userid() + if item.get("timestamp") is None: + print("No timestamp found, using current timestamp") + item["timestamp"] = int(datetime.datetime.now().timestamp()) + + if item.get("userid") is None: + print("No userid found, using current userid") + item["userid"] = get_current_userid() return next(cls.execute(insert(cls).values(item), commit=True)) @@ -337,7 +342,10 @@ class ScrobbleTable(Base): @classmethod def add(cls, item: dict[str, Any]): - item["userid"] = get_current_userid() + if item.get("userid") is None: + print("No userid found, using current userid") + item["userid"] = get_current_userid() + return cls.insert_one(item) @classmethod From d6df57ba5fb26d8052d7cccdcabc95e3bfef0588 Mon Sep 17 00:00:00 2001 From: cwilvx Date: Tue, 17 Jun 2025 09:59:55 +0200 Subject: [PATCH 4/4] remove prints --- swingmusic/api/backup_and_restore.py | 5 ++--- swingmusic/db/userdata.py | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/swingmusic/api/backup_and_restore.py b/swingmusic/api/backup_and_restore.py index 63ac5aaa..3fe3fea8 100644 --- a/swingmusic/api/backup_and_restore.py +++ b/swingmusic/api/backup_and_restore.py @@ -135,12 +135,11 @@ class RestoreBackup: for playlist in new_playlists: try: if playlist.get("_score") is not None: - print("Removing _score from playlist") del playlist["_score"] PlaylistTable.add_one(playlist) except sqlalchemy.exc.IntegrityError: - print("Integrity error, skipping playlist") + print("Integrity error, skipping playlist:") print(playlist) def restore_scrobbles(self, scrobbles: list[dict]): @@ -159,7 +158,7 @@ class RestoreBackup: try: ScrobbleTable.add(scrobble) except sqlalchemy.exc.IntegrityError: - print("Integrity error, skipping scrobble") + print("Integrity error, skipping scrobble:") print(scrobble) diff --git a/swingmusic/db/userdata.py b/swingmusic/db/userdata.py index 1f33d2d5..1ae4db45 100644 --- a/swingmusic/db/userdata.py +++ b/swingmusic/db/userdata.py @@ -220,11 +220,9 @@ class FavoritesTable(Base): item["hash"] = f"{item['type']}_{item['hash']}" if item.get("timestamp") is None: - print("No timestamp found, using current timestamp") item["timestamp"] = int(datetime.datetime.now().timestamp()) if item.get("userid") is None: - print("No userid found, using current userid") item["userid"] = get_current_userid() return next(cls.execute(insert(cls).values(item), commit=True)) @@ -343,7 +341,6 @@ class ScrobbleTable(Base): @classmethod def add(cls, item: dict[str, Any]): if item.get("userid") is None: - print("No userid found, using current userid") item["userid"] = get_current_userid() return cls.insert_one(item)