diff --git a/bin/supysonic-cli b/bin/supysonic-cli index 0cce62b..cd1a33e 100755 --- a/bin/supysonic-cli +++ b/bin/supysonic-cli @@ -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) + diff --git a/supysonic/cli.py b/supysonic/cli.py index 116e184..251b981 100755 --- a/supysonic/cli.py +++ b/supysonic/cli.py @@ -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: diff --git a/supysonic/db.py b/supysonic/db.py index 49ba5f3..ee61870 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -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') diff --git a/supysonic/scanner.py b/supysonic/scanner.py index aedccd1..5ec590c 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -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 diff --git a/supysonic/watcher.py b/supysonic/watcher.py index d50a45b..e36d8cb 100644 --- a/supysonic/watcher.py +++ b/supysonic/watcher.py @@ -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 diff --git a/tests/base/test_cli.py b/tests/base/test_cli.py index ea49e7e..22bafb9 100644 --- a/tests/base/test_cli.py +++ b/tests/base/test_cli.py @@ -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') diff --git a/tests/base/test_scanner.py b/tests/base/test_scanner.py index d8953bd..719cb09 100644 --- a/tests/base/test_scanner.py +++ b/tests/base/test_scanner.py @@ -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))) diff --git a/tests/base/test_watcher.py b/tests/base/test_watcher.py index 983b265..63a82a0 100644 --- a/tests/base/test_watcher.py +++ b/tests/base/test_watcher.py @@ -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)