mirror of
https://github.com/spl0k/supysonic.git
synced 2024-11-09 11:42:16 +00:00
parent
9736622ce1
commit
9c58b695ff
@ -14,7 +14,7 @@ import time
|
||||
|
||||
from pony.orm import db_session
|
||||
|
||||
from .covers import find_cover_in_folder
|
||||
from .covers import find_cover_in_folder, CoverFile
|
||||
from .db import Folder, Artist, Album, Track, User
|
||||
from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
|
||||
from .db import RatingFolder, RatingTrack
|
||||
@ -90,14 +90,7 @@ class Scanner:
|
||||
f.delete() # Pony will cascade
|
||||
continue
|
||||
|
||||
album_name = None
|
||||
track = f.tracks.select().first()
|
||||
if track is not None:
|
||||
album_name = track.album.name
|
||||
|
||||
cover = find_cover_in_folder(f.path, album_name)
|
||||
f.cover_art = cover.name if cover is not None else None
|
||||
|
||||
self.find_cover(f.path)
|
||||
folders += f.children
|
||||
|
||||
folder.last_scan = int(time.time())
|
||||
@ -212,6 +205,46 @@ class Scanner:
|
||||
tr.folder = folder
|
||||
tr.path = dst_path
|
||||
|
||||
@db_session
|
||||
def find_cover(self, dirpath):
|
||||
if not isinstance(dirpath, strtype): # pragma: nocover
|
||||
raise TypeError('Expecting string, got ' + str(type(dirpath)))
|
||||
|
||||
folder = Folder.get(path = dirpath)
|
||||
if folder is None:
|
||||
return
|
||||
|
||||
album_name = None
|
||||
track = folder.tracks.select().first()
|
||||
if track is not None:
|
||||
album_name = track.album.name
|
||||
|
||||
cover = find_cover_in_folder(folder.path, album_name)
|
||||
folder.cover_art = cover.name if cover is not None else None
|
||||
|
||||
@db_session
|
||||
def add_cover(self, path):
|
||||
if not isinstance(path, strtype): # pragma: nocover
|
||||
raise TypeError('Expecting string, got ' + str(type(path)))
|
||||
|
||||
folder = Folder.get(path = os.path.dirname(path))
|
||||
if folder is None:
|
||||
return
|
||||
|
||||
cover_name = os.path.basename(path)
|
||||
if not folder.cover_art:
|
||||
folder.cover_art = cover_name
|
||||
else:
|
||||
album_name = None
|
||||
track = folder.tracks.select().first()
|
||||
if track is not None:
|
||||
album_name = track.album.name
|
||||
|
||||
current_cover = CoverFile(folder.cover_art, album_name)
|
||||
new_cover = CoverFile(cover_name, album_name)
|
||||
if new_cover.score > current_cover.score:
|
||||
folder.cover_art = cover_name
|
||||
|
||||
def __find_album(self, artist, album):
|
||||
ar = self.__find_artist(artist)
|
||||
al = ar.albums.select(lambda a: a.name == album).first()
|
||||
|
@ -8,6 +8,7 @@
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
import time
|
||||
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
@ -17,6 +18,7 @@ from threading import Thread, Condition, Timer
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
|
||||
from . import covers
|
||||
from .db import init_database, release_database, Folder
|
||||
from .py23 import dict
|
||||
from .scanner import Scanner
|
||||
@ -25,10 +27,13 @@ OP_SCAN = 1
|
||||
OP_REMOVE = 2
|
||||
OP_MOVE = 4
|
||||
FLAG_CREATE = 8
|
||||
FLAG_COVER = 16
|
||||
|
||||
class SupysonicWatcherEventHandler(PatternMatchingEventHandler):
|
||||
def __init__(self, extensions, queue, logger):
|
||||
patterns = map(lambda e: "*." + e.lower(), extensions.split()) if extensions else None
|
||||
patterns = None
|
||||
if extensions:
|
||||
patterns = list(map(lambda e: "*." + e.lower(), extensions.split())) + list(map(lambda e: "*" + e, covers.EXTENSIONS))
|
||||
super(SupysonicWatcherEventHandler, self).__init__(patterns = patterns, ignore_directories = True)
|
||||
|
||||
self.__queue = queue
|
||||
@ -37,29 +42,51 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler):
|
||||
def dispatch(self, event):
|
||||
try:
|
||||
super(SupysonicWatcherEventHandler, self).dispatch(event)
|
||||
except Exception as e:
|
||||
except Exception as e: # pragma: nocover
|
||||
self.__logger.critical(e)
|
||||
|
||||
def on_created(self, event):
|
||||
self.__logger.debug("File created: '%s'", event.src_path)
|
||||
self.__queue.put(event.src_path, OP_SCAN | FLAG_CREATE)
|
||||
|
||||
op = OP_SCAN | FLAG_CREATE
|
||||
if not covers.is_valid_cover(event.src_path):
|
||||
self.__queue.put(event.src_path, op)
|
||||
|
||||
dirname = os.path.dirname(event.src_path)
|
||||
with db_session:
|
||||
folder = Folder.get(path = dirname)
|
||||
if folder is None:
|
||||
self.__queue.put(dirname, op | FLAG_COVER)
|
||||
else:
|
||||
self.__queue.put(event.src_path, op | FLAG_COVER)
|
||||
|
||||
def on_deleted(self, event):
|
||||
self.__logger.debug("File deleted: '%s'", event.src_path)
|
||||
self.__queue.put(event.src_path, OP_REMOVE)
|
||||
|
||||
op = OP_REMOVE
|
||||
_, ext = os.path.splitext(event.src_path)
|
||||
if ext in covers.EXTENSIONS:
|
||||
op |= FLAG_COVER
|
||||
self.__queue.put(event.src_path, op)
|
||||
|
||||
def on_modified(self, event):
|
||||
self.__logger.debug("File modified: '%s'", event.src_path)
|
||||
self.__queue.put(event.src_path, OP_SCAN)
|
||||
if not covers.is_valid_cover(event.src_path):
|
||||
self.__queue.put(event.src_path, OP_SCAN)
|
||||
|
||||
def on_moved(self, event):
|
||||
self.__logger.debug("File moved: '%s' -> '%s'", event.src_path, event.dest_path)
|
||||
self.__queue.put(event.dest_path, OP_MOVE, src_path = event.src_path)
|
||||
|
||||
op = OP_MOVE
|
||||
_, ext = os.path.splitext(event.src_path)
|
||||
if ext in covers.EXTENSIONS:
|
||||
op |= FLAG_COVER
|
||||
self.__queue.put(event.dest_path, op, src_path = event.src_path)
|
||||
|
||||
class Event(object):
|
||||
def __init__(self, path, operation, **kwargs):
|
||||
if operation & (OP_SCAN | OP_REMOVE) == (OP_SCAN | OP_REMOVE):
|
||||
raise Exception("Flags SCAN and REMOVE both set")
|
||||
raise Exception("Flags SCAN and REMOVE both set") # pragma: nocover
|
||||
|
||||
self.__path = path
|
||||
self.__time = time.time()
|
||||
@ -68,7 +95,7 @@ class Event(object):
|
||||
|
||||
def set(self, operation, **kwargs):
|
||||
if operation & (OP_SCAN | OP_REMOVE) == (OP_SCAN | OP_REMOVE):
|
||||
raise Exception("Flags SCAN and REMOVE both set")
|
||||
raise Exception("Flags SCAN and REMOVE both set") # pragma: nocover
|
||||
|
||||
self.__time = time.time()
|
||||
if operation & OP_SCAN:
|
||||
@ -113,7 +140,7 @@ class ScannerProcessingQueue(Thread):
|
||||
def run(self):
|
||||
try:
|
||||
self.__run()
|
||||
except Exception as e:
|
||||
except Exception as e: # pragma: nocover
|
||||
self.__logger.critical(e)
|
||||
raise e
|
||||
|
||||
@ -132,21 +159,48 @@ class ScannerProcessingQueue(Thread):
|
||||
|
||||
item = self.__next_item()
|
||||
while item:
|
||||
if item.operation & OP_MOVE:
|
||||
self.__logger.info("Moving: '%s' -> '%s'", item.src_path, item.path)
|
||||
scanner.move_file(item.src_path, item.path)
|
||||
if item.operation & OP_SCAN:
|
||||
self.__logger.info("Scanning: '%s'", item.path)
|
||||
scanner.scan_file(item.path)
|
||||
if item.operation & OP_REMOVE:
|
||||
self.__logger.info("Removing: '%s'", item.path)
|
||||
scanner.remove_file(item.path)
|
||||
if item.operation & FLAG_COVER:
|
||||
self.__process_cover_item(scanner, item)
|
||||
else:
|
||||
self.__process_regular_item(scanner, item)
|
||||
|
||||
item = self.__next_item()
|
||||
|
||||
scanner.finish()
|
||||
self.__logger.debug("Freeing scanner")
|
||||
del scanner
|
||||
|
||||
def __process_regular_item(self, scanner, item):
|
||||
if item.operation & OP_MOVE:
|
||||
self.__logger.info("Moving: '%s' -> '%s'", item.src_path, item.path)
|
||||
scanner.move_file(item.src_path, item.path)
|
||||
|
||||
if item.operation & OP_SCAN:
|
||||
self.__logger.info("Scanning: '%s'", item.path)
|
||||
scanner.scan_file(item.path)
|
||||
|
||||
if item.operation & OP_REMOVE:
|
||||
self.__logger.info("Removing: '%s'", item.path)
|
||||
scanner.remove_file(item.path)
|
||||
|
||||
def __process_cover_item(self, scanner, item):
|
||||
if item.operation & OP_SCAN:
|
||||
if os.path.isdir(item.path):
|
||||
self.__logger.info("Looking for covers: '%s'", item.path)
|
||||
scanner.find_cover(item.path)
|
||||
else:
|
||||
self.__logger.info("Potentially adding cover: '%s'", item.path)
|
||||
scanner.add_cover(item.path)
|
||||
|
||||
if item.operation & OP_REMOVE:
|
||||
self.__logger.info("Removing cover: '%s'", item.path)
|
||||
scanner.find_cover(os.path.dirname(item.path))
|
||||
|
||||
if item.operation & OP_MOVE:
|
||||
self.__logger.info("Moving cover: '%s' -> '%s'", item.src_path, item.path)
|
||||
scanner.find_cover(os.path.dirname(item.src_path))
|
||||
scanner.add_cover(item.path)
|
||||
|
||||
def stop(self):
|
||||
self.__running = False
|
||||
with self.__cond:
|
||||
@ -232,7 +286,7 @@ class SupysonicWatcher(object):
|
||||
logger.info("Starting watcher for %s", folder.path)
|
||||
observer.schedule(handler, folder.path, recursive = True)
|
||||
|
||||
try:
|
||||
try: # pragma: nocover
|
||||
signal(SIGTERM, self.__terminate)
|
||||
signal(SIGINT, self.__terminate)
|
||||
except ValueError:
|
||||
@ -254,5 +308,5 @@ class SupysonicWatcher(object):
|
||||
self.__running = False
|
||||
|
||||
def __terminate(self, signum, frame):
|
||||
self.stop()
|
||||
self.stop() # pragma: nocover
|
||||
|
||||
|
@ -21,7 +21,7 @@ from hashlib import sha1
|
||||
from pony.orm import db_session
|
||||
from threading import Thread
|
||||
|
||||
from supysonic.db import init_database, release_database, Track, Artist
|
||||
from supysonic.db import init_database, release_database, Track, Artist, Folder
|
||||
from supysonic.managers.folder import FolderManager
|
||||
from supysonic.watcher import SupysonicWatcher
|
||||
|
||||
@ -99,14 +99,26 @@ class WatcherTestCase(WatcherTestBase):
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
return os.path.basename(f.name)
|
||||
|
||||
def _temppath(self):
|
||||
return os.path.join(self.__dir, self._tempname() + '.mp3')
|
||||
def _temppath(self, suffix, depth = 0):
|
||||
if depth > 0:
|
||||
dirpath = os.path.join(self.__dir, *(self._tempname() for _ in range(depth)))
|
||||
os.makedirs(dirpath)
|
||||
else:
|
||||
dirpath = self.__dir
|
||||
return os.path.join(dirpath, self._tempname() + suffix)
|
||||
|
||||
def _addfile(self):
|
||||
path = self._temppath()
|
||||
def _addfile(self, depth = 0):
|
||||
path = self._temppath('.mp3', depth)
|
||||
shutil.copyfile('tests/assets/folder/silence.mp3', path)
|
||||
return path
|
||||
|
||||
def _addcover(self, suffix = None, depth = 0):
|
||||
suffix = '.jpg' if suffix is None else (suffix + '.jpg')
|
||||
path = self._temppath(suffix, depth)
|
||||
shutil.copyfile('tests/assets/cover.jpg', path)
|
||||
return path
|
||||
|
||||
class AudioWatcherTestCase(WatcherTestCase):
|
||||
@db_session
|
||||
def assertTrackCountEqual(self, expected):
|
||||
self.assertEqual(Track.select().count(), expected)
|
||||
@ -163,7 +175,7 @@ class WatcherTestCase(WatcherTestBase):
|
||||
self.assertEqual(Track.select().count(), 1)
|
||||
trackid = Track.select().first().id
|
||||
|
||||
newpath = self._temppath()
|
||||
newpath = self._temppath('.mp3')
|
||||
shutil.move(path, newpath)
|
||||
self._sleep()
|
||||
|
||||
@ -179,7 +191,7 @@ class WatcherTestCase(WatcherTestBase):
|
||||
filename = self._tempname() + '.mp3'
|
||||
initialpath = os.path.join(tempfile.gettempdir(), filename)
|
||||
shutil.copyfile('tests/assets/folder/silence.mp3', initialpath)
|
||||
shutil.move(initialpath, os.path.join(self.__dir, filename))
|
||||
shutil.move(initialpath, self._temppath('.mp3'))
|
||||
self._sleep()
|
||||
self.assertTrackCountEqual(1)
|
||||
|
||||
@ -212,7 +224,7 @@ class WatcherTestCase(WatcherTestBase):
|
||||
|
||||
def test_add_rename(self):
|
||||
path = self._addfile()
|
||||
shutil.move(path, self._temppath())
|
||||
shutil.move(path, self._temppath('.mp3'))
|
||||
self._sleep()
|
||||
self.assertTrackCountEqual(1)
|
||||
|
||||
@ -221,7 +233,7 @@ class WatcherTestCase(WatcherTestBase):
|
||||
self._sleep()
|
||||
self.assertTrackCountEqual(1)
|
||||
|
||||
newpath = self._temppath()
|
||||
newpath = self._temppath('.mp3')
|
||||
shutil.move(path, newpath)
|
||||
os.unlink(newpath)
|
||||
self._sleep()
|
||||
@ -229,7 +241,7 @@ class WatcherTestCase(WatcherTestBase):
|
||||
|
||||
def test_add_rename_delete(self):
|
||||
path = self._addfile()
|
||||
newpath = self._temppath()
|
||||
newpath = self._temppath('.mp3')
|
||||
shutil.move(path, newpath)
|
||||
os.unlink(newpath)
|
||||
self._sleep()
|
||||
@ -240,18 +252,112 @@ class WatcherTestCase(WatcherTestBase):
|
||||
self._sleep()
|
||||
self.assertTrackCountEqual(1)
|
||||
|
||||
newpath = self._temppath()
|
||||
finalpath = self._temppath()
|
||||
newpath = self._temppath('.mp3')
|
||||
finalpath = self._temppath('.mp3')
|
||||
shutil.move(path, newpath)
|
||||
shutil.move(newpath, finalpath)
|
||||
self._sleep()
|
||||
self.assertTrackCountEqual(1)
|
||||
|
||||
class CoverWatcherTestCase(WatcherTestCase):
|
||||
def test_add_file_then_cover(self):
|
||||
self._addfile()
|
||||
path = self._addcover()
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Folder.select().first().cover_art, os.path.basename(path))
|
||||
|
||||
def test_add_cover_then_file(self):
|
||||
path = self._addcover()
|
||||
self._addfile()
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Folder.select().first().cover_art, os.path.basename(path))
|
||||
|
||||
def test_remove_cover(self):
|
||||
self._addfile()
|
||||
path = self._addcover()
|
||||
self._sleep()
|
||||
|
||||
os.unlink(path)
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertIsNone(Folder.select().first().cover_art)
|
||||
|
||||
def test_naming_add_good(self):
|
||||
bad = os.path.basename(self._addcover())
|
||||
self._sleep()
|
||||
good = os.path.basename(self._addcover('cover'))
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Folder.select().first().cover_art, good)
|
||||
|
||||
def test_naming_add_bad(self):
|
||||
good = os.path.basename(self._addcover('cover'))
|
||||
self._sleep()
|
||||
bad = os.path.basename(self._addcover())
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Folder.select().first().cover_art, good)
|
||||
|
||||
def test_naming_remove_good(self):
|
||||
bad = self._addcover()
|
||||
good = self._addcover('cover')
|
||||
self._sleep()
|
||||
os.unlink(good)
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Folder.select().first().cover_art, os.path.basename(bad))
|
||||
|
||||
def test_naming_remove_bad(self):
|
||||
bad = self._addcover()
|
||||
good = self._addcover('cover')
|
||||
self._sleep()
|
||||
os.unlink(bad)
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Folder.select().first().cover_art, os.path.basename(good))
|
||||
|
||||
def test_rename(self):
|
||||
path = self._addcover()
|
||||
self._sleep()
|
||||
newpath = self._temppath('.jpg')
|
||||
shutil.move(path, newpath)
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Folder.select().first().cover_art, os.path.basename(newpath))
|
||||
|
||||
def test_add_to_folder_without_track(self):
|
||||
path = self._addcover(depth = 1)
|
||||
self._sleep()
|
||||
|
||||
with db_session:
|
||||
self.assertFalse(Folder.exists(cover_art = os.path.basename(path)))
|
||||
|
||||
def test_remove_from_folder_without_track(self):
|
||||
path = self._addcover(depth = 1)
|
||||
self._sleep()
|
||||
os.unlink(path)
|
||||
self._sleep()
|
||||
|
||||
def test_add_track_to_empty_folder(self):
|
||||
self._addfile(1)
|
||||
self._sleep()
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
suite.addTest(unittest.makeSuite(NothingToWatchTestCase))
|
||||
suite.addTest(unittest.makeSuite(WatcherTestCase))
|
||||
suite.addTest(unittest.makeSuite(AudioWatcherTestCase))
|
||||
suite.addTest(unittest.makeSuite(CoverWatcherTestCase))
|
||||
|
||||
return suite
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user