diff --git a/app/api/folder.py b/app/api/folder.py index e1de1f7..fa05fce 100644 --- a/app/api/folder.py +++ b/app/api/folder.py @@ -30,8 +30,11 @@ def get_folder_tree(): root_dirs = db.get_root_dirs() - if req_dir == "$home" and root_dirs[0] == "$home": - req_dir = settings.USER_HOME_DIR + try: + if req_dir == "$home" and root_dirs[0] == "$home": + req_dir = settings.USER_HOME_DIR + except IndexError: + pass if req_dir == "$home": folders = [Path(f) for f in root_dirs] diff --git a/app/api/settings.py b/app/api/settings.py index b2de4d6..bc2c8e0 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -1,12 +1,13 @@ from flask import Blueprint, request from app import settings -from app.db.sqlite.settings import SettingsSQLMethods as sdb -from app.lib import populate -from app.logger import log +from app.logger import log +from app.lib import populate from app.db.store import Store from app.utils import background +from app.lib.watchdogg import Watcher as WatchDog +from app.db.sqlite.settings import SettingsSQLMethods as sdb api = Blueprint("settings", __name__, url_prefix="/") @@ -19,6 +20,10 @@ def get_child_dirs(parent: str, children: list[str]): @background def rebuild_store(db_dirs: list[str]): + """ + Restarts the watchdog and rebuilds the music library. + """ + log.info("Rebuilding library...") Store.remove_tracks_by_dir_except(db_dirs) @@ -28,6 +33,7 @@ def rebuild_store(db_dirs: list[str]): Store.load_artists() populate.Populate() + WatchDog().restart() log.info("Rebuilding library... ✅") @@ -59,13 +65,13 @@ def add_root_dirs(): db_dirs = sdb.get_root_dirs() _h = "$home" - if db_dirs[0] == _h and new_dirs[0] == _h.strip(): - return {"msg": "Not changed!"} - - if db_dirs[0] == _h: - sdb.remove_root_dirs(db_dirs) - try: + if db_dirs[0] == _h and new_dirs[0] == _h.strip(): + return {"msg": "Not changed!"} + + if db_dirs[0] == _h: + sdb.remove_root_dirs(db_dirs) + if new_dirs[0] == _h: finalize([_h], db_dirs, [settings.USER_HOME_DIR]) diff --git a/app/lib/populate.py b/app/lib/populate.py index b71eb00..d818fae 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -26,31 +26,16 @@ class Populate: """ def __init__(self) -> None: - text = { - "root_unset": "The root directory is not set. Trying to scan the default directory: %s", - "default_not_exists": "The directory: %s does not exist. Please open the app in your web browser to set the root directory.", - "no_tracks": "No tracks found in: %s. Please open the app in your web browser to set the root directory.", - } - tracks = get_all_tracks() tracks = list(tracks) dirs_to_scan = sdb.get_root_dirs() - initial_dirs_count = len(dirs_to_scan) - - def_dir = "~/Music" if len(dirs_to_scan) == 0: - log.warning(text["root_unset"], def_dir) - print("...") - - exists = os.path.exists(settings.MUSIC_DIR) - - if not exists: - log.warning(text["default_not_exists"], def_dir) - return - - dirs_to_scan = [settings.MUSIC_DIR] + log.warning( + "The root directory is not configured. Open the app in your web browser to configure." + ) + return try: if dirs_to_scan[0] == "$home": @@ -65,26 +50,6 @@ class Populate: untagged = self.filter_untagged(tracks, files) - if initial_dirs_count == 0 and len(untagged) == 0: - log.warning(text["no_tracks"], def_dir) - return - - if initial_dirs_count == 0 and len(untagged) > 0: - log.info( - "%sFound %s tracks 💪 %s", - settings.TCOLOR.OKGREEN, - len(untagged), - settings.TCOLOR.ENDC, - ) - log.info( - "%s%s saved as the default root directory. 😶%s", - settings.TCOLOR.OKGREEN, - def_dir, - settings.TCOLOR.ENDC, - ) - sdb.add_root_dirs(dirs_to_scan) - # return - if len(untagged) == 0: log.info("All clear, no unread files.") return diff --git a/app/lib/watchdogg.py b/app/lib/watchdogg.py index 276f6ba..ccef335 100644 --- a/app/lib/watchdogg.py +++ b/app/lib/watchdogg.py @@ -2,17 +2,22 @@ This library contains the classes and functions related to the watchdog file watcher. """ import os +import sqlite3 import time from watchdog.events import PatternMatchingEventHandler from watchdog.observers import Observer -from app.db.sqlite.tracks import SQLiteManager -from app.db.sqlite.tracks import SQLiteTrackMethods as db + +from app.logger import log from app.db.store import Store from app.lib.taglib import get_tags -from app.logger import log from app.models import Artist, Track +from app import settings + +from app.db.sqlite.tracks import SQLiteManager +from app.db.sqlite.tracks import SQLiteTrackMethods as db +from app.db.sqlite.settings import SettingsSQLMethods as sdb class Watcher: @@ -20,39 +25,96 @@ class Watcher: Contains the methods for initializing and starting watchdog. """ - home_dir = os.path.expanduser("~") - dirs = [home_dir] observers: list[Observer] = [] def __init__(self): self.observer = Observer() def run(self): - event_handler = Handler() + """ + Starts watchers for each dir in root_dirs + """ - for dir_ in self.dirs: + trials = 0 + + while trials < 10: + try: + dirs = sdb.get_root_dirs() + print(dirs) + dir_map = [ + {"original": d, "realpath": os.path.realpath(d)} for d in dirs + ] + break + except sqlite3.OperationalError: + trials += 1 + time.sleep(1) + else: + log.error( + "WatchDogError: Failed to start Watchdog. Waiting for database timed out!" + ) + return + + if len(dirs) == 0: + log.warning( + "WatchDogInfo: No root directories configured. Watchdog not started." + ) + return + + dir_map = [d for d in dir_map if d['realpath'] != d['original']] + + if len(dirs) > 0 and dirs[0] == "$home": + dirs = [settings.USER_HOME_DIR] + + event_handler = Handler(root_dirs=dirs, dir_map=dir_map) + + for _dir in dirs: + exists = os.path.exists(_dir) + + if not exists: + log.error("WatchdogError: Directory not found: %s", _dir) + + for _dir in dirs: self.observer.schedule( - event_handler, os.path.realpath(dir_), recursive=True + event_handler, os.path.realpath(_dir), recursive=True ) self.observers.append(self.observer) try: self.observer.start() - except OSError: - log.error("Could not start watchdog.") + log.info("Started watchdog") + except FileNotFoundError: + log.error( + "WatchdogError: Failed to start watchdog, root directories could not be resolved." + ) return try: while True: time.sleep(1) except KeyboardInterrupt: - for obsv in self.observers: - obsv.unschedule_all() - obsv.stop() + self.stop_all() for obsv in self.observers: obsv.join() + def stop_all(self): + """ + Unschedules and stops all existing watchers. + """ + log.info("Stopping all watchdog observers") + for obsv in self.observers: + obsv.unschedule_all() + obsv.stop() + + def restart(self): + """ + Stops all existing watchers, refetches root_dirs from the db + and restarts the watchers. + """ + log.info("🔃 Restarting watchdog") + self.stop_all() + self.run() + def add_track(filepath: str) -> None: """ @@ -118,9 +180,13 @@ def remove_track(filepath: str) -> None: class Handler(PatternMatchingEventHandler): files_to_process = [] + root_dirs = [] + dir_map = [] + + def __init__(self, root_dirs: list[str], dir_map: dict[str:str]): + self.root_dirs = root_dirs + self.dir_map = dir_map - def __init__(self): - log.info("✅ started watchdog") PatternMatchingEventHandler.__init__( self, patterns=["*.flac", "*.mp3"], @@ -128,6 +194,16 @@ class Handler(PatternMatchingEventHandler): case_sensitive=False, ) + def get_abs_path(self, path: str): + """ + Convert a realpath to a path relative to the matching root directory. + """ + for d in self.dir_map: + if d["realpath"] in path: + return path.replace(d["realpath"], d["original"]) + + return path + def on_created(self, event): """ Fired when a supported file is created. @@ -138,8 +214,8 @@ class Handler(PatternMatchingEventHandler): """ Fired when a delete event occurs on a supported file. """ - - remove_track(event.src_path) + path = self.get_abs_path(event.src_path) + remove_track(path) def on_moved(self, event): """ @@ -148,14 +224,19 @@ class Handler(PatternMatchingEventHandler): trash = "share/Trash" if trash in event.dest_path: - remove_track(event.src_path) + path = self.get_abs_path(event.src_path) + remove_track(path) elif trash in event.src_path: - add_track(event.dest_path) + path = self.get_abs_path(event.dest_path) + add_track(path) elif trash not in event.dest_path and trash not in event.src_path: - add_track(event.dest_path) - remove_track(event.src_path) + dest_path = self.get_abs_path(event.dest_path) + src_path = self.get_abs_path(event.src_path) + + add_track(dest_path) + remove_track(src_path) def on_closed(self, event): """ @@ -164,9 +245,7 @@ class Handler(PatternMatchingEventHandler): try: self.files_to_process.remove(event.src_path) if os.path.getsize(event.src_path) > 0: - add_track(event.src_path) + path = self.get_abs_path(event.src_path) + add_track(path) except ValueError: pass - - -# watcher = Watcher() diff --git a/app/logger.py b/app/logger.py index 9a82b2b..8139128 100644 --- a/app/logger.py +++ b/app/logger.py @@ -10,9 +10,9 @@ class CustomFormatter(logging.Formatter): Custom log formatter """ - grey = "\x1b[38;20m" + grey = "\033[92m" yellow = "\x1b[33;20m" - red = "\x1b[31;20m" + red = "\033[41m" bold_red = "\x1b[31;1m" reset = "\x1b[0m" # format = ( @@ -45,5 +45,4 @@ handler.setLevel(logging.DEBUG) handler.setFormatter(CustomFormatter()) log.addHandler(handler) - # copied from: https://stackoverflow.com/a/56944256: diff --git a/app/settings.py b/app/settings.py index 529332f..e346287 100644 --- a/app/settings.py +++ b/app/settings.py @@ -105,7 +105,7 @@ class TCOLOR: OKBLUE = "\033[94m" OKCYAN = "\033[96m" OKGREEN = "\033[92m" - WARNING = "\033[93m" + YELLOW = "\033[93m" FAIL = "\033[91m" ENDC = "\033[0m" BOLD = "\033[1m"