1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-09-19 19:01:03 +00:00

Fix failing deletions from the scanner

This commit is contained in:
Alban Féron 2023-01-17 22:59:43 +01:00
parent 36efefcda6
commit 0957fef148
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
5 changed files with 126 additions and 101 deletions

View File

@ -167,18 +167,52 @@ class Folder(PathMixin, _Model):
@classmethod @classmethod
def prune(cls): def prune(cls):
query = cls.delete().where( alias = cls.alias()
query = cls.select(cls.id).where(
~cls.root, ~cls.root,
cls.id.not_in(Track.select(Track.folder)), Track.select(fn.count("*")).where(Track.folder == cls.id) == 0,
cls.id.not_in(cls.select(cls.parent)), alias.select(fn.count("*")).where(alias.parent == cls.id) == 0,
) )
total = 0 total = 0
while True: while True:
count = query.execute() clone = query.clone() # peewee caches the results, clone to force a refetch
total += count for f in clone:
if not count: f.delete_instance(recursive=True)
total += 1
if not len(clone):
return total return total
def delete_hierarchy(self):
if self.root:
cond = Track.root_folder == self
else:
cond = Track.path.startswith(self.path)
return self.__delete_hierarchy(cond)
def __delete_hierarchy(self, cond):
users = User.select(User.id).join(Track).where(cond)
User.update(last_play=None).where(User.id.in_(users)).execute()
tracks = Track.select(Track.id).where(cond)
RatingTrack.delete().where(RatingTrack.rated.in_(tracks)).execute()
StarredTrack.delete().where(StarredTrack.starred.in_(tracks)).execute()
path_cond = Folder.path.startswith(self.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()
deleted_tracks = Track.delete().where(cond).execute()
query = Folder.delete().where(path_cond)
if isinstance(db.obj, MySQLDatabase):
# MySQL can't propery resolve deletion order when it has several to handle
query = query.order_by(Folder.path.desc())
query.execute()
return deleted_tracks
class Artist(_Model): class Artist(_Model):
id = PrimaryKeyField() id = PrimaryKeyField()

View File

@ -1,27 +1,15 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # Supysonic is a Python implementation of the Subsonic server API.
# #
# Copyright (C) 2013-2022 Alban 'spl0k' Féron # Copyright (C) 2013-2023 Alban 'spl0k' Féron
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
import os.path import os.path
from peewee import IntegrityError
from ..daemon.client import DaemonClient from ..daemon.client import DaemonClient
from ..daemon.exceptions import DaemonUnavailableError from ..daemon.exceptions import DaemonUnavailableError
from ..db import ( from ..db import Folder, Artist, Album
Folder,
Track,
Artist,
Album,
User,
RatingFolder,
RatingTrack,
StarredFolder,
StarredTrack,
)
class FolderManager: class FolderManager:
@ -79,29 +67,9 @@ class FolderManager:
except DaemonUnavailableError: except DaemonUnavailableError:
pass pass
root_cond = Track.root_folder == folder folder.delete_hierarchy()
users = User.select(User.id).join(Track).where(root_cond)
User.update(last_play=None).where(User.id.in_(users)).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()
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() Album.prune()
Artist.prune() Artist.prune()
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 @staticmethod
def delete_by_name(name): def delete_by_name(name):

View File

@ -141,8 +141,19 @@ class Scanner(Thread):
self.__report_progress(folder.name, scanned) self.__report_progress(folder.name, scanned)
# Remove deleted/moved folders
folders = [folder]
while not self.__stopped.is_set() and folders:
f = folders.pop()
if not f.root and not os.path.isdir(f.path):
self.__stats.deleted.tracks += f.delete_hierarchy()
continue
folders += f.children[:]
# Remove files that have been deleted # Remove files that have been deleted
# Could be more efficient if done above # Could be more efficient if done when walking on the files
if not self.__stopped.is_set(): if not self.__stopped.is_set():
for track in Track.select().where(Track.root_folder == folder): for track in Track.select().where(Track.root_folder == folder):
if not os.path.exists(track.path) or not self.__check_extension( if not os.path.exists(track.path) or not self.__check_extension(
@ -150,15 +161,10 @@ class Scanner(Thread):
): ):
self.remove_file(track.path) self.remove_file(track.path)
# Remove deleted/moved folders and update cover art info # Update cover art info
folders = [folder] folders = [folder]
while not self.__stopped.is_set() and folders: while not self.__stopped.is_set() and folders:
f = folders.pop() f = folders.pop()
if not f.root and not os.path.isdir(f.path):
f.delete_instance(recursive=True)
continue
self.find_cover(f.path) self.find_cover(f.path)
folders += f.children[:] folders += f.children[:]
@ -173,8 +179,8 @@ class Scanner(Thread):
if self.__stopped.is_set(): if self.__stopped.is_set():
return return
self.__stats.deleted.albums = Album.prune() self.__stats.deleted.albums += Album.prune()
self.__stats.deleted.artists = Artist.prune() self.__stats.deleted.artists += Artist.prune()
Folder.prune() Folder.prune()
def __check_extension(self, path): def __check_extension(self, path):
@ -272,7 +278,7 @@ class Scanner(Thread):
raise TypeError("Expecting string, got " + str(type(path))) raise TypeError("Expecting string, got " + str(type(path)))
try: try:
Track.get(path=path).delete_instance() Track.get(path=path).delete_instance(recursive=True)
self.__stats.deleted.tracks += 1 self.__stats.deleted.tracks += 1
except Track.DoesNotExist: except Track.DoesNotExist:
pass pass

View File

@ -1,13 +1,14 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
import mutagen import mutagen
import os import os
import os.path import os.path
import shutil
import tempfile import tempfile
import unittest import unittest
@ -164,5 +165,70 @@ class ScannerTestCase(unittest.TestCase):
self.assertEqual(stats.deleted.tracks, 0) self.assertEqual(stats.deleted.tracks, 0)
class ScannerDeletionsTestCase(unittest.TestCase):
def setUp(self):
self.__dir = tempfile.mkdtemp()
db.init_database("sqlite:")
FolderManager.add("folder", self.__dir)
# Create folder hierarchy
self._firstsubdir = tempfile.mkdtemp(dir=self.__dir)
subdir = self._firstsubdir
for _ in range(4):
subdir = tempfile.mkdtemp(dir=subdir)
# Put a file in the deepest folder
self._trackpath = os.path.join(subdir, "silence.mp3")
shutil.copyfile("tests/assets/folder/silence.mp3", self._trackpath)
self._scan()
# Create annotation data
track = db.Track.get()
firstdir = db.Folder.get(path=self._firstsubdir)
user = db.User.create(
name="user", password="password", salt="salt", last_play=track
)
db.StarredFolder.create(user=user, starred=track.folder_id)
db.StarredFolder.create(user=user, starred=firstdir)
db.StarredArtist.create(user=user, starred=track.artist_id)
db.StarredAlbum.create(user=user, starred=track.album_id)
db.StarredTrack.create(user=user, starred=track)
db.RatingFolder.create(user=user, rated=track.folder_id, rating=2)
db.RatingFolder.create(user=user, rated=firstdir, rating=2)
db.RatingTrack.create(user=user, rated=track, rating=2)
def tearDown(self):
db.release_database()
shutil.rmtree(self.__dir)
def _scan(self):
scanner = Scanner()
scanner.queue_folder("folder")
scanner.run()
return scanner.stats()
def _check_assertions(self, stats):
self.assertEqual(stats.deleted.artists, 1)
self.assertEqual(stats.deleted.albums, 1)
self.assertEqual(stats.deleted.tracks, 1)
self.assertEqual(db.Track.select().count(), 0)
self.assertEqual(db.Album.select().count(), 0)
self.assertEqual(db.Artist.select().count(), 0)
self.assertEqual(db.User.select().count(), 1)
self.assertEqual(db.Folder.select().count(), 1)
def test_parent_folder(self):
shutil.rmtree(self._firstsubdir)
stats = self._scan()
self._check_assertions(stats)
def test_track(self):
os.remove(self._trackpath)
stats = self._scan()
self._check_assertions(stats)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -1,49 +0,0 @@
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
# Copyright (C) 2018-2022 Alban 'spl0k' Féron
#
# Distributed under terms of the GNU AGPLv3 license.
import os.path
import shutil
import tempfile
import unittest
from supysonic.db import init_database, release_database
from supysonic.managers.folder import FolderManager
from supysonic.scanner import Scanner
class Issue101TestCase(unittest.TestCase):
def setUp(self):
self.__dir = tempfile.mkdtemp()
init_database("sqlite:")
FolderManager.add("folder", self.__dir)
def tearDown(self):
release_database()
shutil.rmtree(self.__dir)
def test_issue(self):
firstsubdir = tempfile.mkdtemp(dir=self.__dir)
subdir = firstsubdir
for _ in range(4):
subdir = tempfile.mkdtemp(dir=subdir)
shutil.copyfile(
"tests/assets/folder/silence.mp3", os.path.join(subdir, "silence.mp3")
)
scanner = Scanner()
scanner.queue_folder("folder")
scanner.run()
shutil.rmtree(firstsubdir)
scanner = Scanner()
scanner.queue_folder("folder")
scanner.run()
if __name__ == "__main__":
unittest.main()