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

Scanner, CLI and watcher are on a pony

This commit is contained in:
spl0k 2017-12-17 23:25:34 +01:00
parent a4b9a97271
commit 2428ffeb57
8 changed files with 262 additions and 241 deletions

View File

@ -12,9 +12,11 @@ import sys
from supysonic.cli import SupysonicCLI
from supysonic.config import IniConfig
from supysonic.db import get_database, release_database
if __name__ == "__main__":
config = IniConfig.from_common_locations()
db = get_database(config.BASE['database_uri'])
cli = SupysonicCLI(config)
if len(sys.argv) > 1:
@ -22,3 +24,5 @@ if __name__ == "__main__":
else:
cli.cmdloop()
release_database(db)

View File

@ -25,7 +25,9 @@ import getpass
import sys
import time
from .db import get_store, Folder, User
from pony.orm import db_session
from .db import Folder, User
from .managers.folder import FolderManager
from .managers.user import UserManager
from .scanner import Scanner
@ -105,8 +107,6 @@ class SupysonicCLI(cmd.Cmd):
for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems():
setattr(self, 'help_{} {}'.format(command, action), subparser.print_help)
self.__store = get_store(config.BASE['database_uri'])
def write_line(self, line = ''):
self.stdout.write(line + '\n')
@ -148,44 +148,49 @@ class SupysonicCLI(cmd.Cmd):
folder_scan_parser.add_argument('folders', metavar = 'folder', nargs = '*', help = 'Folder(s) to be scanned. If ommitted, all folders are scanned')
folder_scan_parser.add_argument('-f', '--force', action = 'store_true', help = "Force scan of already know files even if they haven't changed")
@db_session
def folder_list(self):
self.write_line('Name\t\tPath\n----\t\t----')
self.write_line('\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in self.__store.find(Folder, Folder.root == True)))
self.write_line('\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in Folder.select(lambda f: f.root)))
def folder_add(self, name, path):
ret = FolderManager.add(self.__store, name, path)
ret = FolderManager.add(name, path)
if ret != FolderManager.SUCCESS:
self.write_error_line(FolderManager.error_str(ret))
else:
self.write_line("Folder '{}' added".format(name))
def folder_delete(self, name):
ret = FolderManager.delete_by_name(self.__store, name)
ret = FolderManager.delete_by_name(name)
if ret != FolderManager.SUCCESS:
self.write_error_line(FolderManager.error_str(ret))
else:
self.write_line("Deleted folder '{}'".format(name))
@db_session
def folder_scan(self, folders, force):
extensions = self.__config.BASE['scanner_extensions']
if extensions:
extensions = extensions.split(' ')
scanner = Scanner(self.__store, force = force, extensions = extensions)
scanner = Scanner(force = force, extensions = extensions)
if folders:
folders = map(lambda n: self.__store.find(Folder, Folder.name == n, Folder.root == True).one() or n, folders)
if any(map(lambda f: isinstance(f, basestring), folders)):
self.write_line("No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring)))
for folder in filter(lambda f: isinstance(f, Folder), folders):
fstrs = folders
folders = Folder.select(lambda f: f.root and f.name in fstrs)[:]
notfound = set(fstrs) - set(map(lambda f: f.name, folders))
if notfound:
self.write_line("No such folder(s): " + ' '.join(notfound))
for folder in folders:
scanner.scan(folder, TimedProgressDisplay(folder.name, self.stdout))
self.write_line()
else:
for folder in self.__store.find(Folder, Folder.root == True):
for folder in Folder.select(lambda f: f.root):
scanner.scan(folder, TimedProgressDisplay(folder.name, self.stdout))
self.write_line()
scanner.finish()
added, deleted = scanner.stats()
self.__store.commit()
self.write_line("Scanning done")
self.write_line('Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2]))
@ -208,9 +213,10 @@ class SupysonicCLI(cmd.Cmd):
user_pass_parser.add_argument('name', help = 'Name/login of the user to which change the password')
user_pass_parser.add_argument('password', nargs = '?', help = 'New password')
@db_session
def user_list(self):
self.write_line('Name\t\tAdmin\tEmail\n----\t\t-----\t-----')
self.write_line('\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in self.__store.find(User)))
self.write_line('\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in User.select()))
def user_add(self, name, admin, password, email):
if not password:
@ -219,24 +225,24 @@ class SupysonicCLI(cmd.Cmd):
if password != confirm:
self.write_error_line("Passwords don't match")
return
status = UserManager.add(self.__store, name, password, email, admin)
status = UserManager.add(name, password, email, admin)
if status != UserManager.SUCCESS:
self.write_error_line(UserManager.error_str(status))
def user_delete(self, name):
ret = UserManager.delete_by_name(self.__store, name)
ret = UserManager.delete_by_name(name)
if ret != UserManager.SUCCESS:
self.write_error_line(UserManager.error_str(ret))
else:
self.write_line("Deleted user '{}'".format(name))
@db_session
def user_setadmin(self, name, off):
user = self.__store.find(User, User.name == name).one()
if not user:
user = User.get(name = name)
if user is None:
self.write_error_line('No such user')
else:
user.admin = not off
self.__store.commit()
self.write_line("{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name))
def user_changepass(self, name, password):
@ -246,7 +252,7 @@ class SupysonicCLI(cmd.Cmd):
if password != confirm:
self.write_error_line("Passwords don't match")
return
status = UserManager.change_password2(self.__store, name, password)
status = UserManager.change_password2(name, password)
if status != UserManager.SUCCESS:
self.write_error_line(UserManager.error_str(status))
else:

View File

@ -153,7 +153,7 @@ class Track(db.Entity):
number = Required(int)
title = Required(str)
year = Optional(int)
genre = Optional(str)
genre = Optional(str, nullable = True)
duration = Required(int)
album = Required(Album, column = 'album_id')

View File

@ -23,6 +23,8 @@ import mimetypes
import mutagen
import time
from pony.orm import db_session
from .db import Folder, Artist, Album, Track, User
from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from .db import RatingFolder, RatingTrack
@ -67,8 +69,9 @@ class Scanner:
progress_callback(current, total)
# Remove files that have been deleted
for track in [ t for t in self.__store.find(Track, Track.root_folder_id == folder.id) if not self.__is_valid_path(t.path) ]:
self.remove_file(track.path)
for track in Track.select(lambda t: t.root_folder == folder):
if not self.__is_valid_path(track.path):
self.remove_file(track.path)
# Update cover art info
folders = [ folder ]
@ -79,25 +82,32 @@ class Scanner:
folder.last_scan = int(time.time())
@db_session
def finish(self):
for album in [ a for a in self.__albums_to_check if not a.tracks.count() ]:
self.__artists_to_check.add(album.artist)
for album in Album.select(lambda a: a.id in self.__albums_to_check):
if not album.tracks.is_empty():
continue
self.__artists_to_check.add(album.artist.id)
self.__deleted_albums += 1
album.delete()
self.__albums_to_check.clear()
for artist in [ a for a in self.__artists_to_check if not a.albums.count() and not a.tracks.count() ]:
for artist in Artist.select(lambda a: a.id in self.__artists_to_check):
if not artist.albums.is_empty() or not artist.tracks.is_empty():
continue
self.__deleted_artists += 1
artist.delete()
self.__artists_to_check.clear()
while self.__folders_to_check:
folder = self.__folders_to_check.pop()
folder = Folder[self.__folders_to_check.pop()]
if folder.root:
continue
if not folder.tracks.count() and not folder.children.count():
self.__folders_to_check.add(folder.parent)
if folder.tracks.is_empty() and folder.children.is_empty():
self.__folders_to_check.add(folder.parent.id)
folder.delete()
def __is_valid_path(self, path):
@ -107,13 +117,13 @@ class Scanner:
return True
return os.path.splitext(path)[1][1:].lower() in self.__extensions
@db_session
def scan_file(self, path):
if not isinstance(path, basestring):
raise TypeError('Expecting string, got ' + str(type(path)))
tr = self.__store.find(Track, Track.path == path).one()
add = False
if tr:
tr = Track.get(path = path)
if tr is not None:
if not self.__force and not int(os.path.getmtime(path)) > tr.last_modification:
return
@ -121,55 +131,55 @@ class Scanner:
if not tag:
self.remove_file(path)
return
trdict = {}
else:
tag = self.__try_load_tag(path)
if not tag:
return
tr = Track()
tr.path = path
add = True
trdict = { 'path': path }
artist = self.__try_read_tag(tag, 'artist', '')
album = self.__try_read_tag(tag, 'album', '')
artist = self.__try_read_tag(tag, 'artist')
if not artist:
return
album = self.__try_read_tag(tag, 'album', '[non-album tracks]')
albumartist = self.__try_read_tag(tag, 'albumartist', artist)
tr.disc = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0]))
tr.number = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0]))
tr.title = self.__try_read_tag(tag, 'title', '')
tr.year = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
tr.genre = self.__try_read_tag(tag, 'genre')
tr.duration = int(tag.info.length)
trdict['disc'] = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0]))
trdict['number'] = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0]))
trdict['title'] = self.__try_read_tag(tag, 'title', '')
trdict['year'] = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
trdict['genre'] = self.__try_read_tag(tag, 'genre')
trdict['duration'] = int(tag.info.length)
tr.bitrate = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000
tr.content_type = mimetypes.guess_type(path, False)[0] or 'application/octet-stream'
tr.last_modification = os.path.getmtime(path)
trdict['bitrate'] = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000
trdict['content_type'] = mimetypes.guess_type(path, False)[0] or 'application/octet-stream'
trdict['last_modification'] = int(os.path.getmtime(path))
tralbum = self.__find_album(albumartist, album)
trartist = self.__find_artist(artist)
if add:
trroot = self.__find_root_folder(path)
trfolder = self.__find_folder(path)
if tr is None:
trdict['root_folder'] = self.__find_root_folder(path)
trdict['folder'] = self.__find_folder(path)
trdict['album'] = tralbum
trdict['artist'] = trartist
# Set the references at the very last as searching for them will cause the added track to be flushed, even if
# it is incomplete, causing not null constraints errors.
tr.album = tralbum
tr.artist = trartist
tr.folder = trfolder
tr.root_folder = trroot
self.__store.add(tr)
Track(**trdict)
self.__added_tracks += 1
else:
if tr.album.id != tralbum.id:
self.__albums_to_check.add(tr.album)
tr.album = tralbum
self.__albums_to_check.add(tr.album.id)
trdict['album'] = tralbum
if tr.artist.id != trartist.id:
self.__artists_to_check.add(tr.artist)
tr.artist = trartist
self.__artists_to_check.add(tr.artist.id)
trdict['artist'] = trartist
tr.set(**trdict)
@db_session
def remove_file(self, path):
if not isinstance(path, basestring):
raise TypeError('Expecting string, got ' + str(type(path)))
@ -178,12 +188,13 @@ class Scanner:
if not tr:
return
self.__folders_to_check.add(tr.folder)
self.__albums_to_check.add(tr.album)
self.__artists_to_check.add(tr.artist)
self.__folders_to_check.add(tr.folder.id)
self.__albums_to_check.add(tr.album.id)
self.__artists_to_check.add(tr.artist.id)
self.__deleted_tracks += 1
tr.delete()
@db_session
def move_file(self, src_path, dst_path):
if not isinstance(src_path, basestring):
raise TypeError('Expecting string, got ' + str(type(src_path)))
@ -193,16 +204,18 @@ class Scanner:
if src_path == dst_path:
return
tr = self.__store.find(Track, Track.path == src_path).one()
if not tr:
tr = Track.get(path = src_path)
if tr is None:
return
self.__folders_to_check.add(tr.folder)
tr_dst = self.__store.find(Track, Track.path == dst_path).one()
if tr_dst:
tr.root_folder = tr_dst.root_folder
tr.folder = tr_dst.folder
self.__folders_to_check.add(tr.folder.id)
tr_dst = Track.get(path = dst_path)
if tr_dst is not None:
root = tr_dst.root_folder
folder = tr_dst.folder
self.remove_file(dst_path)
tr.root_folder = root
tr.folder = folder
else:
root = self.__find_root_folder(dst_path)
folder = self.__find_folder(dst_path)
@ -212,70 +225,48 @@ class Scanner:
def __find_album(self, artist, album):
ar = self.__find_artist(artist)
al = ar.albums.find(name = album).one()
al = ar.albums.select(lambda a: a.name == album).first()
if al:
return al
al = Album()
al.name = album
al.artist = ar
self.__store.add(al)
al = Album(name = album, artist = ar)
self.__added_albums += 1
return al
def __find_artist(self, artist):
ar = self.__store.find(Artist, Artist.name == artist).one()
ar = Artist.get(name = artist)
if ar:
return ar
ar = Artist()
ar.name = artist
self.__store.add(ar)
ar = Artist(name = artist)
self.__added_artists += 1
return ar
def __find_root_folder(self, path):
path = os.path.dirname(path)
db = self.__store.get_database().__module__[len('storm.databases.'):]
folders = self.__store.find(Folder, Like(path, Concat(Folder.path, u'%', db)), Folder.root == True)
count = folders.count()
if count > 1:
raise Exception("Found multiple root folders for '{}'.".format(path))
elif count == 0:
raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder".format(path))
return folders.one()
for folder in Folder.select(lambda f: f.root):
if path.startswith(folder.path):
return folder
raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder".format(path))
def __find_folder(self, path):
children = []
drive, _ = os.path.splitdrive(path)
path = os.path.dirname(path)
folders = self.__store.find(Folder, Folder.path == path)
count = folders.count()
if count > 1:
raise Exception("Found multiple folders for '{}'.".format(path))
elif count == 1:
return folders.one()
while path != drive and path != '/':
folder = Folder.get(path = path)
if folder is not None:
break
db = self.__store.get_database().__module__[len('storm.databases.'):]
folder = self.__store.find(Folder, Like(path, Concat(Folder.path, os.sep + u'%', db))).order_by(Folder.path).last()
children.append(dict(root = False, name = os.path.basename(path), path = path))
path = os.path.dirname(path)
full_path = folder.path
path = path[len(folder.path) + 1:]
for name in path.split(os.sep):
full_path = os.path.join(full_path, name)
fold = Folder()
fold.root = False
fold.name = name
fold.path = full_path
fold.parent = folder
self.__store.add(fold)
folder = fold
assert folder is not None
while children:
folder = Folder(parent = folder, **children.pop())
return folder

View File

@ -22,12 +22,13 @@ import logging
import time
from logging.handlers import TimedRotatingFileHandler
from pony.orm import db_session
from signal import signal, SIGTERM, SIGINT
from threading import Thread, Condition, Timer
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
from . import db
from .db import get_database, release_database, Folder
from .scanner import Scanner
OP_SCAN = 1
@ -109,12 +110,11 @@ class Event(object):
return self.__src
class ScannerProcessingQueue(Thread):
def __init__(self, database_uri, delay, logger):
def __init__(self, delay, logger):
super(ScannerProcessingQueue, self).__init__()
self.__logger = logger
self.__timeout = delay
self.__database_uri = database_uri
self.__cond = Condition()
self.__timer = None
self.__queue = {}
@ -138,8 +138,7 @@ class ScannerProcessingQueue(Thread):
continue
self.__logger.debug("Instantiating scanner")
store = db.get_store(self.__database_uri)
scanner = Scanner(store)
scanner = Scanner()
item = self.__next_item()
while item:
@ -155,8 +154,6 @@ class ScannerProcessingQueue(Thread):
item = self.__next_item()
scanner.finish()
store.commit()
store.close()
self.__logger.debug("Freeing scanner")
del scanner
@ -208,6 +205,7 @@ class SupysonicWatcher(object):
def __init__(self, config):
self.__config = config
self.__running = True
self.__db = get_database(config.BASE['database_uri'])
def run(self):
logger = logging.getLogger(__name__)
@ -227,22 +225,22 @@ class SupysonicWatcher(object):
}
logger.setLevel(mapping.get(self.__config.DAEMON['log_level'].upper(), logging.NOTSET))
store = db.get_store(self.__config.BASE['database_uri'])
folders = store.find(db.Folder, db.Folder.root == True)
if not folders.count():
with db_session:
folders = Folder.select(lambda f: f.root)
shouldrun = folders.exists()
if not shouldrun:
logger.info("No folder set. Exiting.")
store.close()
release_database(self.__db)
return
queue = ScannerProcessingQueue(self.__config.BASE['database_uri'], self.__config.DAEMON['wait_delay'], logger)
queue = ScannerProcessingQueue(self.__config.DAEMON['wait_delay'], logger)
handler = SupysonicWatcherEventHandler(self.__config.BASE['scanner_extensions'], queue, logger)
observer = Observer()
for folder in folders:
logger.info("Starting watcher for %s", folder.path)
observer.schedule(handler, folder.path, recursive = True)
store.close()
with db_session:
for folder in folders:
logger.info("Starting watcher for %s", folder.path)
observer.schedule(handler, folder.path, recursive = True)
try:
signal(SIGTERM, self.__terminate)
@ -260,6 +258,7 @@ class SupysonicWatcher(object):
observer.join()
queue.stop()
queue.join()
release_database(self.__db)
def stop(self):
self.__running = False

View File

@ -15,27 +15,22 @@ import tempfile
import unittest
from contextlib import contextmanager
from pony.orm import db_session
from StringIO import StringIO
from supysonic.db import Folder, User, get_store
from supysonic.db import Folder, User, get_database, release_database
from supysonic.cli import SupysonicCLI
from ..testbase import TestConfig
class CLITestCase(unittest.TestCase):
""" Really basic tests. Some even don't check anything but are juste there for coverage """
""" Really basic tests. Some even don't check anything but are just there for coverage """
def setUp(self):
conf = TestConfig(False, False)
self.__dbfile = tempfile.mkstemp()[1]
conf.BASE['database_uri'] = 'sqlite:///' + self.__dbfile
self.__store = get_store(conf.BASE['database_uri'])
with io.open('schema/sqlite.sql', 'r') as sql:
schema = sql.read()
for statement in schema.split(';'):
self.__store.execute(statement)
self.__store.commit()
self.__store = get_database(conf.BASE['database_uri'], True)
self.__stdout = StringIO()
self.__stderr = StringIO()
@ -44,7 +39,7 @@ class CLITestCase(unittest.TestCase):
def tearDown(self):
self.__stdout.close()
self.__stderr.close()
self.__store.close()
release_database(self.__store)
os.unlink(self.__dbfile)
@contextmanager
@ -59,9 +54,10 @@ class CLITestCase(unittest.TestCase):
with self._tempdir() as d:
self.__cli.onecmd('folder add tmpfolder ' + d)
f = self.__store.find(Folder).one()
self.assertIsNotNone(f)
self.assertEqual(f.path, d)
with db_session:
f = Folder.select().first()
self.assertIsNotNone(f)
self.assertEqual(f.path, d)
def test_folder_add_errors(self):
with self._tempdir() as d:
@ -71,14 +67,17 @@ class CLITestCase(unittest.TestCase):
self.__cli.onecmd('folder add f1 ' + d)
self.__cli.onecmd('folder add f3 /invalid/path')
self.assertEqual(self.__store.find(Folder).count(), 1)
with db_session:
self.assertEqual(Folder.select().count(), 1)
def test_folder_delete(self):
with self._tempdir() as d:
self.__cli.onecmd('folder add tmpfolder ' + d)
self.__cli.onecmd('folder delete randomfolder')
self.__cli.onecmd('folder delete tmpfolder')
self.assertEqual(self.__store.find(Folder).count(), 0)
with db_session:
self.assertEqual(Folder.select().count(), 0)
def test_folder_list(self):
with self._tempdir() as d:
@ -97,13 +96,17 @@ class CLITestCase(unittest.TestCase):
def test_user_add(self):
self.__cli.onecmd('user add -p Alic3 alice')
self.__cli.onecmd('user add -p alice alice')
self.assertEqual(self.__store.find(User).count(), 1)
with db_session:
self.assertEqual(User.select().count(), 1)
def test_user_delete(self):
self.__cli.onecmd('user add -p Alic3 alice')
self.__cli.onecmd('user delete alice')
self.__cli.onecmd('user delete bob')
self.assertEqual(self.__store.find(User).count(), 0)
with db_session:
self.assertEqual(User.select().count(), 0)
def test_user_list(self):
self.__cli.onecmd('user add -p Alic3 alice')
@ -114,7 +117,8 @@ class CLITestCase(unittest.TestCase):
self.__cli.onecmd('user add -p Alic3 alice')
self.__cli.onecmd('user setadmin alice')
self.__cli.onecmd('user setadmin bob')
self.assertTrue(self.__store.find(User, User.name == 'alice').one().admin)
with db_session:
self.assertTrue(User.get(name = 'alice').admin)
def test_user_changepass(self):
self.__cli.onecmd('user add -p Alic3 alice')

View File

@ -16,6 +16,7 @@ import tempfile
import unittest
from contextlib import contextmanager
from pony.orm import db_session, commit
from supysonic import db
from supysonic.managers.folder import FolderManager
@ -23,133 +24,158 @@ from supysonic.scanner import Scanner
class ScannerTestCase(unittest.TestCase):
def setUp(self):
self.store = db.get_store('sqlite:')
with io.open('schema/sqlite.sql', 'r') as f:
for statement in f.read().split(';'):
self.store.execute(statement)
self.store = db.get_database('sqlite:', True)
FolderManager.add(self.store, 'folder', os.path.abspath('tests/assets'))
self.folder = self.store.find(db.Folder).one()
self.assertIsNotNone(self.folder)
FolderManager.add('folder', os.path.abspath('tests/assets'))
with db_session:
folder = db.Folder.select().first()
self.assertIsNotNone(folder)
self.folderid = folder.id
self.scanner = Scanner(self.store)
self.scanner.scan(self.folder)
self.scanner = Scanner()
self.scanner.scan(folder)
def tearDown(self):
self.scanner.finish()
self.store.close()
db.release_database(self.store)
@contextmanager
def __temporary_track_copy(self):
track = self.store.find(db.Track).one()
track = db.Track.select().first()
with tempfile.NamedTemporaryFile(dir = os.path.dirname(track.path)) as tf:
with io.open(track.path, 'rb') as f:
tf.write(f.read())
yield tf
@db_session
def test_scan(self):
self.assertEqual(self.store.find(db.Track).count(), 1)
self.assertEqual(db.Track.select().count(), 1)
self.assertRaises(TypeError, self.scanner.scan, None)
self.assertRaises(TypeError, self.scanner.scan, 'string')
@db_session
def test_progress(self):
def progress(processed, total):
self.assertIsInstance(processed, int)
self.assertIsInstance(total, int)
self.assertLessEqual(processed, total)
self.scanner.scan(self.folder, progress)
self.scanner.scan(db.Folder[self.folderid], progress)
@db_session
def test_rescan(self):
self.scanner.scan(self.folder)
self.assertEqual(self.store.find(db.Track).count(), 1)
self.scanner.scan(db.Folder[self.folderid])
commit()
self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_force_rescan(self):
self.scanner = Scanner(self.store, True)
self.scanner.scan(self.folder)
self.assertEqual(self.store.find(db.Track).count(), 1)
self.scanner = Scanner(True)
self.scanner.scan(db.Folder[self.folderid])
commit()
self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_scan_file(self):
track = self.store.find(db.Track).one()
track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.scan_file, None)
self.assertRaises(TypeError, self.scanner.scan_file, track)
self.scanner.scan_file('/some/inexistent/path')
self.assertEqual(self.store.find(db.Track).count(), 1)
commit()
self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_remove_file(self):
track = self.store.find(db.Track).one()
track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.remove_file, None)
self.assertRaises(TypeError, self.scanner.remove_file, track)
self.scanner.remove_file('/some/inexistent/path')
self.assertEqual(self.store.find(db.Track).count(), 1)
commit()
self.assertEqual(db.Track.select().count(), 1)
self.scanner.remove_file(track.path)
self.scanner.finish()
self.assertEqual(self.store.find(db.Track).count(), 0)
self.assertEqual(self.store.find(db.Album).count(), 0)
self.assertEqual(self.store.find(db.Artist).count(), 0)
commit()
self.assertEqual(db.Track.select().count(), 0)
self.assertEqual(db.Album.select().count(), 0)
self.assertEqual(db.Artist.select().count(), 0)
@db_session
def test_move_file(self):
track = self.store.find(db.Track).one()
track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.move_file, None, 'string')
self.assertRaises(TypeError, self.scanner.move_file, track, 'string')
self.assertRaises(TypeError, self.scanner.move_file, 'string', None)
self.assertRaises(TypeError, self.scanner.move_file, 'string', track)
self.scanner.move_file('/some/inexistent/path', track.path)
self.assertEqual(self.store.find(db.Track).count(), 1)
commit()
self.assertEqual(db.Track.select().count(), 1)
self.scanner.move_file(track.path, track.path)
self.assertEqual(self.store.find(db.Track).count(), 1)
commit()
self.assertEqual(db.Track.select().count(), 1)
self.assertRaises(Exception, self.scanner.move_file, track.path, '/some/inexistent/path')
with self.__temporary_track_copy() as tf:
self.scanner.scan(self.folder)
self.assertEqual(self.store.find(db.Track).count(), 2)
self.scanner.scan(db.Folder[self.folderid])
commit()
self.assertEqual(db.Track.select().count(), 2)
self.scanner.move_file(tf.name, track.path)
self.assertEqual(self.store.find(db.Track).count(), 1)
commit()
self.assertEqual(db.Track.select().count(), 1)
track = self.store.find(db.Track).one()
track = db.Track.select().first()
new_path = os.path.abspath(os.path.join(os.path.dirname(track.path), '..', 'silence.mp3'))
self.scanner.move_file(track.path, new_path)
self.assertEqual(self.store.find(db.Track).count(), 1)
commit()
self.assertEqual(db.Track.select().count(), 1)
self.assertEqual(track.path, new_path)
@db_session
def test_rescan_corrupt_file(self):
track = self.store.find(db.Track).one()
self.scanner = Scanner(self.store, True)
track = db.Track.select().first()
self.scanner = Scanner(True)
with self.__temporary_track_copy() as tf:
self.scanner.scan(self.folder)
self.assertEqual(self.store.find(db.Track).count(), 2)
self.scanner.scan(db.Folder[self.folderid])
commit()
self.assertEqual(db.Track.select().count(), 2)
tf.seek(0, 0)
tf.write('\x00' * 4096)
tf.truncate()
self.scanner.scan(self.folder)
self.assertEqual(self.store.find(db.Track).count(), 1)
self.scanner.scan(db.Folder[self.folderid])
commit()
self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_rescan_removed_file(self):
track = self.store.find(db.Track).one()
track = db.Track.select().first()
with self.__temporary_track_copy() as tf:
self.scanner.scan(self.folder)
self.assertEqual(self.store.find(db.Track).count(), 2)
self.scanner.scan(db.Folder[self.folderid])
commit()
self.assertEqual(db.Track.select().count(), 2)
self.scanner.scan(self.folder)
self.assertEqual(self.store.find(db.Track).count(), 1)
self.scanner.scan(db.Folder[self.folderid])
commit()
self.assertEqual(db.Track.select().count(), 1)
@db_session
def test_scan_tag_change(self):
self.scanner = Scanner(self.store, True)
self.scanner = Scanner(True)
folder = db.Folder[self.folderid]
with self.__temporary_track_copy() as tf:
self.scanner.scan(self.folder)
copy = self.store.find(db.Track, db.Track.path == tf.name).one()
self.scanner.scan(folder)
commit()
copy = db.Track.get(path = tf.name)
self.assertEqual(copy.artist.name, 'Some artist')
self.assertEqual(copy.album.name, 'Awesome album')
@ -158,12 +184,12 @@ class ScannerTestCase(unittest.TestCase):
tags['album'] = 'Crappy album'
tags.save()
self.scanner.scan(self.folder)
self.scanner.scan(folder)
self.scanner.finish()
self.assertEqual(copy.artist.name, 'Renamed artist')
self.assertEqual(copy.album.name, 'Crappy album')
self.assertIsNotNone(self.store.find(db.Artist, db.Artist.name == 'Some artist').one())
self.assertIsNotNone(self.store.find(db.Album, db.Album.name == 'Awesome album').one())
self.assertIsNotNone(db.Artist.get(name = 'Some artist'))
self.assertIsNotNone(db.Album.get(name = 'Awesome album'))
def test_stats(self):
self.assertEqual(self.scanner.stats(), ((1,1,1),(0,0,0)))

View File

@ -18,9 +18,10 @@ import time
import unittest
from contextlib import contextmanager
from pony.orm import db_session
from threading import Thread
from supysonic.db import get_store, Track, Artist
from supysonic.db import get_database, release_database, Track, Artist
from supysonic.managers.folder import FolderManager
from supysonic.watcher import SupysonicWatcher
@ -38,29 +39,13 @@ class WatcherTestConfig(TestConfig):
self.BASE['database_uri'] = db_uri
class WatcherTestBase(unittest.TestCase):
@contextmanager
def _get_store(self):
store = None
try:
store = get_store('sqlite:///' + self.__dbfile)
yield store
store.commit()
store.close()
except:
store.rollback()
store.close()
raise
def setUp(self):
self.__dbfile = tempfile.mkstemp()[1]
conf = WatcherTestConfig('sqlite:///' + self.__dbfile)
self.__sleep_time = conf.DAEMON['wait_delay'] + 1
dburi = 'sqlite:///' + self.__dbfile
release_database(get_database(dburi, True))
with self._get_store() as store:
with io.open('schema/sqlite.sql', 'r') as sql:
schema = sql.read()
for statement in schema.split(';'):
store.execute(statement)
conf = WatcherTestConfig(dburi)
self.__sleep_time = conf.DAEMON['wait_delay'] + 1
self.__watcher = SupysonicWatcher(conf)
self.__thread = Thread(target = self.__watcher.run)
@ -82,6 +67,12 @@ class WatcherTestBase(unittest.TestCase):
def _sleep(self):
time.sleep(self.__sleep_time)
@contextmanager
def _tempdbrebind(self):
db = get_database('sqlite:///' + self.__dbfile)
try: yield
finally: release_database(db)
class NothingToWatchTestCase(WatcherTestBase):
def test_spawn_useless_watcher(self):
self._start()
@ -93,8 +84,7 @@ class WatcherTestCase(WatcherTestBase):
def setUp(self):
super(WatcherTestCase, self).setUp()
self.__dir = tempfile.mkdtemp()
with self._get_store() as store:
FolderManager.add(store, 'Folder', self.__dir)
FolderManager.add('Folder', self.__dir)
self._start()
def tearDown(self):
@ -115,9 +105,9 @@ class WatcherTestCase(WatcherTestBase):
shutil.copyfile('tests/assets/folder/silence.mp3', path)
return path
@db_session
def assertTrackCountEqual(self, expected):
with self._get_store() as store:
self.assertEqual(store.find(Track).count(), expected)
self.assertEqual(Track.select().count(), expected)
def test_add(self):
self._addfile()
@ -128,7 +118,8 @@ class WatcherTestCase(WatcherTestBase):
def test_add_nowait_stop(self):
self._addfile()
self._stop()
self.assertTrackCountEqual(1)
with self._tempdbrebind():
self.assertTrackCountEqual(1)
def test_add_multiple(self):
self._addfile()
@ -136,46 +127,46 @@ class WatcherTestCase(WatcherTestBase):
self._addfile()
self.assertTrackCountEqual(0)
self._sleep()
with self._get_store() as store:
self.assertEqual(store.find(Track).count(), 3)
self.assertEqual(store.find(Artist).count(), 1)
with db_session:
self.assertEqual(Track.select().count(), 3)
self.assertEqual(Artist.select().count(), 1)
def test_change(self):
path = self._addfile()
self._sleep()
trackid = None
with self._get_store() as store:
self.assertEqual(store.find(Track).count(), 1)
self.assertEqual(store.find(Artist, Artist.name == 'Some artist').count(), 1)
trackid = store.find(Track).one().id
with db_session:
self.assertEqual(Track.select().count(), 1)
self.assertEqual(Artist.select(lambda a: a.name == 'Some artist').count(), 1)
trackid = Track.select().first().id
tags = mutagen.File(path, easy = True)
tags['artist'] = 'Renamed'
tags.save()
self._sleep()
with self._get_store() as store:
self.assertEqual(store.find(Track).count(), 1)
self.assertEqual(store.find(Artist, Artist.name == 'Some artist').count(), 0)
self.assertEqual(store.find(Artist, Artist.name == 'Renamed').count(), 1)
self.assertEqual(store.find(Track).one().id, trackid)
with db_session:
self.assertEqual(Track.select().count(), 1)
self.assertEqual(Artist.select(lambda a: a.name == 'Some artist').count(), 0)
self.assertEqual(Artist.select(lambda a: a.name == 'Renamed').count(), 1)
self.assertEqual(Track.select().first().id, trackid)
def test_rename(self):
path = self._addfile()
self._sleep()
trackid = None
with self._get_store() as store:
self.assertEqual(store.find(Track).count(), 1)
trackid = store.find(Track).one().id
with db_session:
self.assertEqual(Track.select().count(), 1)
trackid = Track.select().first().id
newpath = self._temppath()
shutil.move(path, newpath)
self._sleep()
with self._get_store() as store:
track = store.find(Track).one()
with db_session:
track = Track.select().first()
self.assertIsNotNone(track)
self.assertNotEqual(track.path, path)
self.assertEqual(track.path, newpath)