mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 17:06:17 +00:00
Port supysonic.scanner
This commit is contained in:
parent
e589247458
commit
83ba85aaf1
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
|
Loading…
Reference in New Issue
Block a user