diff --git a/bin/supysonic-watcher b/bin/supysonic-watcher index c258789..d964de7 100755 --- a/bin/supysonic-watcher +++ b/bin/supysonic-watcher @@ -12,15 +12,14 @@ import logging from logging.handlers import TimedRotatingFileHandler from signal import signal, SIGTERM, SIGINT -from time import sleep from supysonic.config import IniConfig +from supysonic.daemon import Daemon from supysonic.db import init_database, release_database -from supysonic.watcher import SupysonicWatcher logger = logging.getLogger('supysonic') -watcher = None +daemon = None def setup_logging(config): if config['log_file']: @@ -39,7 +38,7 @@ def setup_logging(config): def __terminate(signum, frame): logger.debug("Got signal %i. Stopping...", signum) - watcher.stop() + daemon.terminate() release_database() if __name__ == "__main__": @@ -50,11 +49,6 @@ if __name__ == "__main__": signal(SIGINT, __terminate) init_database(config.BASE['database_uri']) - - watcher = SupysonicWatcher(config) - watcher.start() - - while watcher.running: - sleep(2) - + daemon = Daemon(config.DAEMON['socket']) + daemon.run(config) release_database() diff --git a/supysonic/config.py b/supysonic/config.py index 1501346..8c4a2cb 100644 --- a/supysonic/config.py +++ b/supysonic/config.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2018 Alban 'spl0k' Féron +# Copyright (C) 2013-2019 Alban 'spl0k' Féron # 2017 Óscar García Amor # # Distributed under terms of the GNU AGPLv3 license. @@ -35,6 +35,7 @@ class DefaultConfig(object): 'mount_api': True } DAEMON = { + 'socket': os.path.join(tempdir, 'supysonic.sock'), 'wait_delay': 5, 'log_file': None, 'log_level': 'WARNING' diff --git a/supysonic/daemon.py b/supysonic/daemon.py new file mode 100644 index 0000000..adef113 --- /dev/null +++ b/supysonic/daemon.py @@ -0,0 +1,78 @@ +# coding: utf-8 +# +# This file is part of Supysonic. +# Supysonic is a Python implementation of the Subsonic server API. +# +# Copyright (C) 2019 Alban 'spl0k' Féron +# +# Distributed under terms of the GNU AGPLv3 license. + +import logging + +from multiprocessing.connection import Client, Listener + +from .config import IniConfig +from .py23 import strtype +from .utils import get_secret_key +from .watcher import SupysonicWatcher + +__all__ = [ 'Daemon', 'DaemonClient' ] + +logger = logging.getLogger(__name__) + +WATCHER = 0 + +W_ADD = 0 +W_DEL = 1 + +class DaemonClient(object): + def __init__(self, address = None): + self.__address = address or IniConfig.from_common_locations().DAEMON['socket'] + self.__key = get_secret_key('daemon_key') + + def __get_connection(self): + return Client(address = self.__address, authkey = self.__key) + + def add_watched_folder(self, folder): + if not isinstance(folder, strtype): + raise TypeError('Expecting string, got ' + str(type(folder))) + with self.__get_connection() as c: + c.send((WATCHER, W_ADD, folder)) + + def remove_watched_folder(self, folder): + if not isinstance(folder, strtype): + raise TypeError('Expecting string, got ' + str(type(folder))) + with self.__get_connection() as c: + c.send((WATCHER, W_DEL, folder)) + +class Daemon(object): + def __init__(self, address): + self.__address = address + self.__listener = None + self.__watcher = None + + def __handle_connection(self, connection): + try: + module, cmd, *args = connection.recv() + if module == WATCHER: + if cmd == W_ADD: + self.__watcher.add_folder(*args) + elif cmd == W_DEL: + self.__watcher.remove_folder(*args) + except ValueError: + logger.warn('Received unknown data') + + def run(self, config): + self.__listener = Listener(address = self.__address, authkey = get_secret_key('daemon_key')) + logger.info("Listening to %s", self.__listener.address) + + self.__watcher = SupysonicWatcher(config) + self.__watcher.start() + + while True: + conn = self.__listener.accept() + self.__handle_connection(conn) + + def terminate(self): + self.__listener.close() + self.__watcher.stop() diff --git a/supysonic/managers/folder.py b/supysonic/managers/folder.py index 9ec4643..ffbcb50 100644 --- a/supysonic/managers/folder.py +++ b/supysonic/managers/folder.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2018 Alban 'spl0k' Féron +# Copyright (C) 2013-2019 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. @@ -13,6 +13,7 @@ import uuid from pony.orm import select from pony.orm import ObjectNotFound +from ..daemon import DaemonClient from ..db import Folder, Track, Artist, Album, User, RatingTrack, StarredTrack from ..py23 import strtype @@ -43,7 +44,13 @@ class FolderManager: if Folder.exists(lambda f: f.path.startswith(path)): raise ValueError('This path contains a folder that is already registered') - return Folder(root = True, name = name, path = path) + folder = Folder(root = True, name = name, path = path) + try: + DaemonClient().add_watched_folder(path) + except (ConnectionRefusedError, FileNotFoundError): + pass + + return folder @staticmethod def delete(uid): @@ -51,6 +58,11 @@ class FolderManager: if not folder.root: raise ObjectNotFound(Folder) + try: + DaemonClient().remove_watched_folder(folder.path) + except (ConnectionRefusedError, FileNotFoundError): + pass + for user in User.select(lambda u: u.last_play.root_folder == folder): user.last_play = None RatingTrack.select(lambda r: r.rated.root_folder == folder).delete(bulk = True) diff --git a/supysonic/watcher.py b/supysonic/watcher.py index 0aea6d7..325d767 100644 --- a/supysonic/watcher.py +++ b/supysonic/watcher.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2014-2018 Alban 'spl0k' Féron +# Copyright (C) 2014-2019 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. @@ -18,7 +18,7 @@ from watchdog.events import PatternMatchingEventHandler from . import covers from .db import Folder -from .py23 import dict +from .py23 import dict, strtype from .scanner import Scanner OP_SCAN = 1 @@ -246,25 +246,35 @@ class SupysonicWatcher(object): self.__delay = config.DAEMON['wait_delay'] self.__handler = SupysonicWatcherEventHandler(config.BASE['scanner_extensions']) + self.__folders = {} self.__queue = None self.__observer = None - def start(self): - with db_session: - folders = Folder.select(lambda f: f.root) - shouldrun = folders.exists() - if not shouldrun: - logger.info("No folder set.") - return + def add_folder(self, folder): + if isinstance(folder, Folder): + path = folder.path + elif isinstance(folder, strtype): + path = folder + else: + raise TypeError('Expecting string or Folder, got ' + str(type(folder))) + logger.info("Scheduling watcher for %s", path) + watch = self.__observer.schedule(self.__handler, path, recursive = True) + self.__folders[path] = watch + + def remove_folder(self, path): + logger.info("Unscheduling watcher for %s", path) + self.__observer.unschedule(self.__folders[path]) + del self.__folders[path] + + def start(self): self.__queue = ScannerProcessingQueue(self.__delay) self.__observer = Observer() self.__handler.queue = self.__queue with db_session: - for folder in folders: - logger.info("Scheduling watcher for %s", folder.path) - self.__observer.schedule(self.__handler, folder.path, recursive = True) + for folder in Folder.select(lambda f: f.root): + self.add_folder(folder) logger.info("Starting watcher") self.__queue.start()