diff --git a/supysonic/db.py b/supysonic/db.py index 53ac7dc..931b7b1 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -202,11 +202,19 @@ class Artist(_Model): @classmethod def prune(cls): + album_artists = Album.select(Album.artist) + track_artists = Track.select(Track.artist) + + StarredArtist.delete().where( + StarredArtist.starred.not_in(album_artists), + StarredArtist.starred.not_in(track_artists), + ).execute() + return ( cls.delete() .where( - cls.id.not_in(Album.select(Album.artist)), - cls.id.not_in(Track.select(Track.artist)), + cls.id.not_in(album_artists), + cls.id.not_in(track_artists), ) .execute() ) @@ -269,7 +277,9 @@ class Album(_Model): @classmethod def prune(cls): - return cls.delete().where(cls.id.not_in(Track.select(Track.album))).execute() + albums = Track.select(Track.album) + StarredAlbum.delete().where(StarredAlbum.starred.not_in(albums)).execute() + return cls.delete().where(cls.id.not_in(albums)).execute() class Track(PathMixin, _Model): diff --git a/supysonic/managers/folder.py b/supysonic/managers/folder.py index 943149a..27cd9e7 100644 --- a/supysonic/managers/folder.py +++ b/supysonic/managers/folder.py @@ -7,9 +7,21 @@ import os.path +from peewee import IntegrityError + from ..daemon.client import DaemonClient from ..daemon.exceptions import DaemonUnavailableError -from ..db import Folder, Track, Artist, Album, User, RatingTrack, StarredTrack +from ..db import ( + Folder, + Track, + Artist, + Album, + User, + RatingFolder, + RatingTrack, + StarredFolder, + StarredTrack, +) class FolderManager: @@ -67,21 +79,29 @@ class FolderManager: except DaemonUnavailableError: pass - users = User.select(User.id).join(Track).where(Track.root_folder == folder) + root_cond = Track.root_folder == folder + users = User.select(User.id).join(Track).where(root_cond) User.update(last_play=None).where(User.id.in_(users)).execute() - deleted_tracks_query = Track.select(Track.id).where(Track.root_folder == folder) - RatingTrack.delete().where( - RatingTrack.rated.in_(deleted_tracks_query) - ).execute() - StarredTrack.delete().where( - StarredTrack.starred.in_(deleted_tracks_query) - ).execute() + tracks = Track.select(Track.id).where(root_cond) + RatingTrack.delete().where(RatingTrack.rated.in_(tracks)).execute() + StarredTrack.delete().where(StarredTrack.starred.in_(tracks)).execute() - Track.delete().where(Track.root_folder == folder).execute() + path_cond = Folder.path.startswith(folder.path) + folders = Folder.select(Folder.id).where(path_cond) + RatingFolder.delete().where(RatingFolder.rated.in_(folders)).execute() + StarredFolder.delete().where(StarredFolder.starred.in_(folders)).execute() + + Track.delete().where(root_cond).execute() Album.prune() Artist.prune() - Folder.delete().where(Folder.path.startswith(folder.path)).execute() + query = Folder.delete().where(path_cond) + try: + query.execute() + except IntegrityError: + # Integrity error most likely due to MySQL poor handling of delete order + query = query.order_by(Folder.path.desc()) + query.execute() @staticmethod def delete_by_name(name): diff --git a/tests/managers/test_manager_folder.py b/tests/managers/test_manager_folder.py index cccb47b..221404e 100644 --- a/tests/managers/test_manager_folder.py +++ b/tests/managers/test_manager_folder.py @@ -1,12 +1,26 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2017-2022 Alban 'spl0k' Féron +# Copyright (C) 2017-2023 Alban 'spl0k' Féron # 2017 Óscar García Amor # # Distributed under terms of the GNU AGPLv3 license. -from supysonic.db import Folder, Album, Artist, Track, init_database, release_database +from supysonic.db import ( + Folder, + Album, + Artist, + RatingFolder, + RatingTrack, + StarredAlbum, + StarredArtist, + StarredFolder, + StarredTrack, + Track, + User, + init_database, + release_database, +) from supysonic.managers.folder import FolderManager import os @@ -31,31 +45,48 @@ class FolderManagerTestCase(unittest.TestCase): def create_folders(self): # Add test folders - self.assertIsNotNone(FolderManager.add("media", self.media_dir)) - self.assertIsNotNone(FolderManager.add("music", self.music_dir)) + media = FolderManager.add("media", self.media_dir) + music = FolderManager.add("music", self.music_dir) + self.assertIsNotNone(media) + self.assertIsNotNone(music) Folder.create( - root=False, name="non-root", path=os.path.join(self.music_dir, "subfolder") + root=False, + parent=music, + name="non-root", + path=os.path.join(self.music_dir, "subfolder"), ) artist = Artist.create(name="Artist") album = Album.create(name="Album", artist=artist) - root = Folder.get(name="media") - Track( + Track.create( title="Track", artist=artist, album=album, disc=1, number=1, path=os.path.join(self.media_dir, "somefile"), - folder=root, - root_folder=root, + folder=media, + root_folder=media, duration=2, bitrate=320, last_modification=0, ) + def create_annotations(self): + track = Track.select().first() + user = User.create(name="user", password="secret", salt="ABC+", last_play=track) + folder = Folder.get(name="media") + + RatingFolder.create(user=user, rated=folder, rating=3) + RatingTrack.create(user=user, rated=track, rating=3) + + StarredFolder.create(user=user, starred=folder) + StarredArtist.create(user=user, starred=track.artist_id) + StarredAlbum.create(user=user, starred=track.album_id) + StarredTrack.create(user=user, starred=track) + def test_get_folder(self): self.create_folders() @@ -116,6 +147,9 @@ class FolderManagerTestCase(unittest.TestCase): self.assertRaises(Folder.DoesNotExist, FolderManager.delete, folder.id) self.assertEqual(Folder.select().count(), 3) + # Create some annotation to ensure foreign keys are properly handled + self.create_annotations() + # Delete existing folders for name in ["media", "music"]: folder = Folder.get(name=name, root=True) @@ -132,6 +166,9 @@ class FolderManagerTestCase(unittest.TestCase): self.assertRaises(Folder.DoesNotExist, FolderManager.delete_by_name, "null") self.assertEqual(Folder.select().count(), 3) + # Create some annotation to ensure foreign keys are properly handled + self.create_annotations() + # Delete existing folders for name in ["media", "music"]: FolderManager.delete_by_name(name)