1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 08:56:17 +00:00

Port supysonic.scanner

This commit is contained in:
Alban Féron 2022-12-10 18:04:09 +01:00
parent e589247458
commit 83ba85aaf1
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
3 changed files with 50 additions and 77 deletions

View File

@ -156,14 +156,14 @@ class Folder(PathMixin, db.Model):
@classmethod @classmethod
def prune(cls): def prune(cls):
query = cls.select( query = cls.delete().where(
lambda self: not exists(t for t in Track if t.folder == self) ~cls.root,
and not exists(f for f in Folder if f.parent == self) cls.id.not_in(Track.select(Track.folder)),
and not self.root cls.id.not_in(cls.select(cls.parent)),
) )
total = 0 total = 0
while True: while True:
count = query.delete() count = query.execute()
total += count total += count
if not count: if not count:
return total return total
@ -191,7 +191,7 @@ class Artist(db.Model):
@classmethod @classmethod
def prune(cls): def prune(cls):
cls.delete().where( return cls.delete().where(
cls.id.not_in(Album.select(Album.artist)), cls.id.not_in(Album.select(Album.artist)),
cls.id.not_in(Track.select(Track.artist)), cls.id.not_in(Track.select(Track.artist)),
).execute() ).execute()
@ -254,7 +254,7 @@ class Album(db.Model):
@classmethod @classmethod
def prune(cls): def prune(cls):
cls.delete().where(cls.id.not_in(Track.select(Track.album))).execute() return cls.delete().where(cls.id.not_in(Track.select(Track.album))).execute()
class Track(PathMixin, db.Model): class Track(PathMixin, db.Model):

View File

@ -101,10 +101,10 @@ class Scanner(Thread):
except QueueEmpty: except QueueEmpty:
break break
with db_session: try:
folder = Folder.get(name=folder_name, root=True) folder = Folder.get(name=folder_name, root=True)
if folder is None: except Folder.DoesNotExist:
continue continue
self.__scan_folder(folder) self.__scan_folder(folder)
@ -144,32 +144,27 @@ class Scanner(Thread):
# 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 above
if not self.__stopped.is_set(): if not self.__stopped.is_set():
with db_session: for track in Track.select().where(Track.root_folder == folder):
for track in Track.select(lambda t: t.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( track.path
track.path ):
): self.remove_file(track.path)
self.remove_file(track.path)
# Remove deleted/moved folders and update cover art info # Remove deleted/moved folders and 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()
with db_session: if not f.root and not os.path.isdir(f.path):
# f has been fetched from another session, refetch or Pony will complain f.delete_instance(recursive=True)
f = Folder[f.id] continue
if not f.root and not os.path.isdir(f.path): self.find_cover(f.path)
f.delete() # Pony will cascade folders += f.children[:]
continue
self.find_cover(f.path)
folders += f.children
if not self.__stopped.is_set(): if not self.__stopped.is_set():
with db_session: folder.last_scan = int(time.time())
Folder[folder.id].last_scan = int(time.time()) folder.save()
if self.__on_folder_end is not None: if self.__on_folder_end is not None:
self.__on_folder_end(folder) self.__on_folder_end(folder)
@ -178,10 +173,9 @@ class Scanner(Thread):
if self.__stopped.is_set(): if self.__stopped.is_set():
return return
with db_session: 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):
if not self.__extensions: if not self.__extensions:
@ -210,7 +204,7 @@ class Scanner(Thread):
mtime = int(stat.st_mtime) mtime = int(stat.st_mtime)
tr = Track.get(path=path) tr = Track.get_or_none(path=path)
if tr is not None: if tr is not None:
if not self.__force and not mtime > tr.last_modification: if not self.__force and not mtime > tr.last_modification:
return return
@ -253,7 +247,7 @@ class Scanner(Thread):
trdict["created"] = datetime.fromtimestamp(mtime) trdict["created"] = datetime.fromtimestamp(mtime)
try: try:
Track(**trdict) Track.create(**trdict)
self.__stats.added.tracks += 1 self.__stats.added.tracks += 1
except ValueError: except ValueError:
# Field validation error # Field validation error
@ -266,7 +260,9 @@ class Scanner(Thread):
trdict["artist"] = trartist trdict["artist"] = trartist
try: try:
tr.set(**trdict) for attr, value in trdict.items():
setattr(tr, attr, value)
tr.save()
except ValueError: except ValueError:
# Field validation error # Field validation error
self.__stats.errors.append(path) self.__stats.errors.append(path)
@ -275,12 +271,11 @@ class Scanner(Thread):
if not isinstance(path, str): if not isinstance(path, str):
raise TypeError("Expecting string, got " + str(type(path))) raise TypeError("Expecting string, got " + str(type(path)))
tr = Track.get(path=path) try:
if not tr: Track.get(path=path).delete_instance()
return self.__stats.deleted.tracks += 1
except Track.DoesNotExist:
self.__stats.deleted.tracks += 1 pass
tr.delete()
def move_file(self, src_path, dst_path): def move_file(self, src_path, dst_path):
if not isinstance(src_path, str): if not isinstance(src_path, str):
@ -291,8 +286,9 @@ class Scanner(Thread):
if src_path == dst_path: if src_path == dst_path:
return return
tr = Track.get(path=src_path) try:
if tr is None: tr = Track.get(path=src_path)
except Track.DoesNotExist:
return return
tr_dst = Track.get(path=dst_path) tr_dst = Track.get(path=dst_path)
@ -352,28 +348,23 @@ class Scanner(Thread):
def __find_album(self, artist, album): def __find_album(self, artist, album):
ar = self.__find_artist(artist) ar = self.__find_artist(artist)
al = ar.albums.select(lambda a: a.name == album).first() al = ar.albums.where(Album.name == album).first()
if al: if al:
return al return al
al = Album(name=album, artist=ar)
self.__stats.added.albums += 1 self.__stats.added.albums += 1
return Album.create(name=album, artist=ar)
return al
def __find_artist(self, artist): def __find_artist(self, artist):
ar = Artist.get(name=artist) try:
if ar: return Artist.get(name=artist)
return ar except Artist.DoesNotExist:
self.__stats.added.artists += 1
ar = Artist(name=artist) return Artist.create(name=artist)
self.__stats.added.artists += 1
return ar
def __find_root_folder(self, path): def __find_root_folder(self, path):
path = os.path.dirname(path) path = os.path.dirname(path)
for folder in Folder.select(lambda f: f.root): for folder in Folder.select().where(Folder.root):
if path.startswith(folder.path): if path.startswith(folder.path):
return folder return folder

View File

@ -1,7 +1,7 @@
# 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-2020 Alban 'spl0k' Féron # Copyright (C) 2017-2022 Alban 'spl0k' Féron
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
@ -12,7 +12,6 @@ import tempfile
import unittest import unittest
from contextlib import contextmanager from contextlib import contextmanager
from pony.orm import db_session, commit
from supysonic import db from supysonic import db
from supysonic.managers.folder import FolderManager from supysonic.managers.folder import FolderManager
@ -23,9 +22,8 @@ class ScannerTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
db.init_database("sqlite:") db.init_database("sqlite:")
with db_session: folder = FolderManager.add("folder", os.path.abspath("tests/assets/folder"))
folder = FolderManager.add("folder", os.path.abspath("tests/assets/folder")) self.assertIsNotNone(folder)
self.assertIsNotNone(folder)
self.folderid = folder.id self.folderid = folder.id
self.__scan() self.__scan()
@ -50,9 +48,7 @@ class ScannerTestCase(unittest.TestCase):
self.scanner = Scanner(force=force) self.scanner = Scanner(force=force)
self.scanner.queue_folder("folder") self.scanner.queue_folder("folder")
self.scanner.run() self.scanner.run()
commit()
@db_session
def test_scan(self): def test_scan(self):
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@ -61,40 +57,32 @@ class ScannerTestCase(unittest.TestCase):
TypeError, self.scanner.queue_folder, db.Folder[self.folderid] TypeError, self.scanner.queue_folder, db.Folder[self.folderid]
) )
@db_session
def test_rescan(self): def test_rescan(self):
self.__scan() self.__scan()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_force_rescan(self): def test_force_rescan(self):
self.__scan(True) self.__scan(True)
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_scan_file(self): def test_scan_file(self):
self.scanner.scan_file("/some/inexistent/path") self.scanner.scan_file("/some/inexistent/path")
commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_remove_file(self): def test_remove_file(self):
track = db.Track.select().first() track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.remove_file, None) self.assertRaises(TypeError, self.scanner.remove_file, None)
self.assertRaises(TypeError, self.scanner.remove_file, track) self.assertRaises(TypeError, self.scanner.remove_file, track)
self.scanner.remove_file("/some/inexistent/path") self.scanner.remove_file("/some/inexistent/path")
commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
self.scanner.remove_file(track.path) self.scanner.remove_file(track.path)
self.scanner.prune() self.scanner.prune()
commit()
self.assertEqual(db.Track.select().count(), 0) self.assertEqual(db.Track.select().count(), 0)
self.assertEqual(db.Album.select().count(), 0) self.assertEqual(db.Album.select().count(), 0)
self.assertEqual(db.Artist.select().count(), 0) self.assertEqual(db.Artist.select().count(), 0)
@db_session
def test_move_file(self): def test_move_file(self):
track = db.Track.select().first() track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.move_file, None, "string") self.assertRaises(TypeError, self.scanner.move_file, None, "string")
@ -103,11 +91,9 @@ class ScannerTestCase(unittest.TestCase):
self.assertRaises(TypeError, self.scanner.move_file, "string", track) self.assertRaises(TypeError, self.scanner.move_file, "string", track)
self.scanner.move_file("/some/inexistent/path", track.path) self.scanner.move_file("/some/inexistent/path", track.path)
commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
self.scanner.move_file(track.path, track.path) self.scanner.move_file(track.path, track.path)
commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
self.assertRaises( self.assertRaises(
@ -118,17 +104,14 @@ class ScannerTestCase(unittest.TestCase):
self.__scan() self.__scan()
self.assertEqual(db.Track.select().count(), 2) self.assertEqual(db.Track.select().count(), 2)
self.scanner.move_file(tf, track.path) self.scanner.move_file(tf, track.path)
commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
track = db.Track.select().first() track = db.Track.select().first()
new_path = track.path.replace("silence", "silence_moved") new_path = track.path.replace("silence", "silence_moved")
self.scanner.move_file(track.path, new_path) self.scanner.move_file(track.path, new_path)
commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
self.assertEqual(track.path, new_path) self.assertEqual(track.path, new_path)
@db_session
def test_rescan_corrupt_file(self): def test_rescan_corrupt_file(self):
with self.__temporary_track_copy() as tf: with self.__temporary_track_copy() as tf:
self.__scan() self.__scan()
@ -142,7 +125,6 @@ class ScannerTestCase(unittest.TestCase):
self.__scan(True) self.__scan(True)
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_rescan_removed_file(self): def test_rescan_removed_file(self):
with self.__temporary_track_copy(): with self.__temporary_track_copy():
self.__scan() self.__scan()
@ -151,7 +133,6 @@ class ScannerTestCase(unittest.TestCase):
self.__scan() self.__scan()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_scan_tag_change(self): def test_scan_tag_change(self):
with self.__temporary_track_copy() as tf: with self.__temporary_track_copy() as tf:
self.__scan() self.__scan()
@ -165,6 +146,7 @@ class ScannerTestCase(unittest.TestCase):
tags.save() tags.save()
self.__scan(True) self.__scan(True)
copy = db.Track.get(path=tf)
self.assertEqual(copy.artist.name, "Renamed artist") self.assertEqual(copy.artist.name, "Renamed artist")
self.assertEqual(copy.album.name, "Crappy album") self.assertEqual(copy.album.name, "Crappy album")
self.assertIsNotNone(db.Artist.get(name="Some artist")) self.assertIsNotNone(db.Artist.get(name="Some artist"))