diff --git a/bin/supysonic-cli b/bin/supysonic-cli index 829156f..23f97bc 100755 --- a/bin/supysonic-cli +++ b/bin/supysonic-cli @@ -141,18 +141,19 @@ class CLI(cmd.Cmd): print "Scanning '{0}': {1}% ({2}/{3})".format(self.__name, (scanned * 100) / total, scanned, total) self.__last_display = time.time() - s = Scanner(self.__store) + scanner = Scanner(self.__store) 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)): print "No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring)) for folder in filter(lambda f: isinstance(f, Folder), folders): - FolderManager.scan(self.__store, folder.id, s, TimedProgressDisplay(folder.name)) + scanner.scan(folder, TimedProgressDisplay(folder.name)) else: for folder in self.__store.find(Folder, Folder.root == True): - FolderManager.scan(self.__store, folder.id, s, TimedProgressDisplay(folder.name)) + scanner.scan(folder, TimedProgressDisplay(folder.name)) - added, deleted = s.stats() + scanner.finish() + added, deleted = scanner.stats() self.__store.commit() print "Scanning done" diff --git a/bin/supysonic-watcher b/bin/supysonic-watcher index b7e55bd..9f5f1ba 100755 --- a/bin/supysonic-watcher +++ b/bin/supysonic-watcher @@ -47,8 +47,9 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler): store = db.get_store(config.get('base', 'database_uri')) track = store.find(db.Track, db.Track.path == event.src_path).one() if track: - folder = track.root_folder - Scanner(store).prune(folder) + scanner = Scanner(store) + scanner.remove_file(track.path) + scanner.finish() store.commit() else: self.__logger.debug("Deleted file %s not in the database", event.src_path) diff --git a/supysonic/frontend/folder.py b/supysonic/frontend/folder.py index 898ddf8..5a40ed1 100644 --- a/supysonic/frontend/folder.py +++ b/supysonic/frontend/folder.py @@ -84,17 +84,19 @@ def del_folder(id): @app.route('/folder/scan') @app.route('/folder/scan/') def scan_folder(id = None): - s = Scanner(store) + scanner = Scanner(store) if id is None: for folder in store.find(Folder, Folder.root == True): - FolderManager.scan(store, folder.id, s) + scanner.scan(folder) else: - status = FolderManager.scan(store, id, s) + status, folder = FolderManager.get(store, id) if status != FolderManager.SUCCESS: flash(FolderManager.error_str(status)) return redirect(url_for('folder_index')) + scanner.scan(folder) - added, deleted = s.stats() + scanner.finish() + added, deleted = scanner.stats() store.commit() flash('Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])) diff --git a/supysonic/managers/folder.py b/supysonic/managers/folder.py index 9ac31b3..d92c288 100644 --- a/supysonic/managers/folder.py +++ b/supysonic/managers/folder.py @@ -20,6 +20,7 @@ import os.path, uuid from supysonic.db import Folder, Artist, Album, Track +from supysonic.scanner import Scanner class FolderManager: SUCCESS = 0 @@ -77,24 +78,11 @@ class FolderManager: if not folder.root: return FolderManager.NO_SUCH_FOLDER - # delete associated tracks and prune empty albums/artists - potentially_removed_albums = set() + scanner = Scanner(store) for track in store.find(Track, Track.root_folder_id == folder.id): - potentially_removed_albums.add(track.album) - store.remove(track) - potentially_removed_artists = set() - for album in filter(lambda album: album.tracks.count() == 0, potentially_removed_albums): - potentially_removed_artists.add(album.artist) - store.remove(album) - for artist in filter(lambda artist: artist.albums.count() == 0, potentially_removed_artists): - store.remove(artist) - - def cleanup_folder(folder): - for f in folder.children: - cleanup_folder(f) - store.remove(folder) - - cleanup_folder(folder) + scanner.remove_file(track.path) + scanner.finish() + store.remove(folder) store.commit() return FolderManager.SUCCESS @@ -106,17 +94,6 @@ class FolderManager: return FolderManager.NO_SUCH_FOLDER return FolderManager.delete(store, folder.id) - @staticmethod - def scan(store, uid, scanner, progress_callback = None): - status, folder = FolderManager.get(store, uid) - if status != FolderManager.SUCCESS: - return status - - scanner.scan(folder, progress_callback) - scanner.prune(folder) - scanner.check_cover_art(folder) - return FolderManager.SUCCESS - @staticmethod def error_str(err): if err == FolderManager.SUCCESS: diff --git a/supysonic/scanner.py b/supysonic/scanner.py index df983bb..006b300 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -42,7 +42,16 @@ class Scanner: extensions = config.get('base', 'scanner_extensions') self.__extensions = map(str.lower, extensions.split()) if extensions else None + self.__folders_to_check = set() + self.__artists_to_check = set() + self.__albums_to_check = set() + + def __del__(self): + if self.__folders_to_check or self.__artists_to_check or self.__albums_to_check: + raise Exception("There's still something to check. Did you run Scanner.finish()?") + def scan(self, folder, progress_callback = None): + # Scan new/updated files files = [ os.path.join(root, f) for root, _, fs in os.walk(folder.path) for f in fs if self.__is_valid_path(os.path.join(root, f)) ] total = len(files) current = 0 @@ -53,32 +62,39 @@ class Scanner: if progress_callback: 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) + + # Update cover art info + folders = [ folder ] + while folders: + f = folders.pop() + f.has_cover_art = os.path.isfile(os.path.join(f.path, 'cover.jpg')) + folders += f.children + folder.last_scan = int(time.time()) - self.__store.flush() - - def prune(self, folder): - 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.__store.remove(track) - self.__deleted_tracks += 1 - - # TODO execute the conditional part on SQL - for album in [ a for a in self.__store.find(Album) if a.tracks.count() == 0 ]: + 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) self.__store.remove(album) self.__deleted_albums += 1 + self.__albums_to_check.clear() - # TODO execute the conditional part on SQL - for artist in [ a for a in self.__store.find(Artist) if a.albums.count() == 0 ]: + for artist in [ a for a in self.__artists_to_check if not a.albums.count() ]: self.__store.remove(artist) self.__deleted_artists += 1 + self.__artists_to_check.clear() - self.__cleanup_folder(folder) - self.__store.flush() + while self.__folders_to_check: + folder = self.__folders_to_check.pop() + if folder.root: + continue - def check_cover_art(self, folder): - folder.has_cover_art = os.path.isfile(os.path.join(folder.path, 'cover.jpg')) - for f in folder.children: - self.check_cover_art(f) + if not folder.tracks.count() and not folder.children.count(): + self.__folders_to_check.add(folder.parent) + self.__store.remove(folder) def __is_valid_path(self, path): if not os.path.exists(path): @@ -96,8 +112,7 @@ class Scanner: tag = self.__try_load_tag(path) if not tag: - self.__store.remove(tr) - self.__deleted_tracks += 1 + self.remove_file(path) return else: tag = self.__try_load_tag(path) @@ -134,6 +149,16 @@ class Scanner: self.__store.add(tr) self.__added_tracks += 1 + def remove_file(self, path): + tr = self.__store.find(Track, Track.path == path).one() + if not tr: + return + + self.__folders_to_check.add(tr.folder) + self.__albums_to_check.add(tr.album) + self.__store.remove(tr) + self.__deleted_tracks += 1 + def __find_album(self, artist, album): ar = self.__find_artist(artist) al = ar.albums.find(name = album).one() @@ -218,12 +243,6 @@ class Scanner: except: return default - def __cleanup_folder(self, folder): - for f in folder.children: - self.__cleanup_folder(f) - if folder.children.count() == 0 and folder.tracks.count() == 0 and not folder.root: - self.__store.remove(folder) - def stats(self): return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks)