From a0d25381c483bc48eddd7c5b98722a1bbc5cbe06 Mon Sep 17 00:00:00 2001 From: dave Date: Sun, 13 Aug 2017 21:13:46 -0700 Subject: [PATCH] args and logging --- pysonic/api.py | 24 ++++++++++++----------- pysonic/daemon.py | 17 ++++++++++++++--- pysonic/database.py | 23 ++++++++++++++--------- pysonic/library.py | 21 +++++++++++++++++++-- pysonic/scanner.py | 46 +++++++++++++++++++++++++++++++++------------ 5 files changed, 94 insertions(+), 37 deletions(-) diff --git a/pysonic/api.py b/pysonic/api.py index bf1e8e8..21758a3 100644 --- a/pysonic/api.py +++ b/pysonic/api.py @@ -1,17 +1,17 @@ import sys +import logging import cherrypy from bs4 import BeautifulSoup from pysonic.library import LETTER_GROUPS +logging = logging.getLogger("api") + class PysonicApi(object): def __init__(self, db, library): self.db = db self.library = library - print("Libraries:", [i["name"] for i in self.library.get_libraries()]) - print("Artists:", [i["name"] for i in self.library.get_artists()]) - def response(self, status="ok"): doc = BeautifulSoup('', features='lxml-xml') root = doc.new_tag("subsonic-response", xmlns="http://subsonic.org/restapi", status=status, version="1.15.0") @@ -33,8 +33,8 @@ class PysonicApi(object): root.append(doc.new_tag("license", valid="true", email="admin@localhost", - licenseExpires="2018-06-22T10:31:49.921Z", - trialExpires="2016-06-29T03:03:58.200Z")) + licenseExpires="2100-01-01T00:00:00.000Z", + trialExpires="2100-01-01T01:01:00.000Z")) yield doc.prettify() @cherrypy.expose @@ -74,7 +74,7 @@ class PysonicApi(object): index.attrs["name"] = letter.upper() indexes.append(index) for artist in self.library.get_artists(): - if artist["name"][0].lower() == letter: + if artist["name"][0].lower() in letter: artist_tag = doc.new_tag("artist") artist_tag.attrs.update({"id": artist["id"], "name": artist["name"]}) index.append(artist_tag) @@ -149,7 +149,7 @@ class PysonicApi(object): yield data sys.stdout.write('.') sys.stdout.flush() - print("\nSent {} bytes for {}".format(total, fpath)) + logging.info("\nSent {} bytes for {}".format(total, fpath)) return content() stream_view._cp_config = {'response.stream': True} @@ -158,7 +158,11 @@ class PysonicApi(object): # /rest/getCoverArt.view?u=dave&s=bfk9mir8is02u3m5as8ucsehn0 # &t=e2b09fb9233d1bfac9abe3dc73017f1e&v=1.2.0&c=DSub&id=12833 fpath = self.library.get_filepath(id) - cherrypy.response.headers['Content-Type'] = 'image/jpeg' + type2ct = { + 'jpg': 'image/jpeg', + 'png': 'image/png' + } + cherrypy.response.headers['Content-Type'] = type2ct[fpath[-3:]] def content(): total = 0 @@ -171,7 +175,7 @@ class PysonicApi(object): yield data sys.stdout.write('.') sys.stdout.flush() - print("\nSent {} bytes for {}".format(total, fpath)) + logging.info("\nSent {} bytes for {}".format(total, fpath)) return content() getCoverArt_view._cp_config = {'response.stream': True} @@ -198,8 +202,6 @@ class PysonicApi(object): continue tag = doc.new_tag(key) tag.append(str(value)) - # print(dir(tag)) - # print(value) dirtag.append(tag) yield doc.prettify() diff --git a/pysonic/daemon.py b/pysonic/daemon.py index 527ba61..33c3e05 100644 --- a/pysonic/daemon.py +++ b/pysonic/daemon.py @@ -1,7 +1,8 @@ +import os import logging import cherrypy from pysonic.api import PysonicApi -from pysonic.library import PysonicLibrary +from pysonic.library import PysonicLibrary, DuplicateRootException from pysonic.database import PysonicDatabase @@ -21,14 +22,24 @@ def main(): db = PysonicDatabase(path=args.database_path) library = PysonicLibrary(db) + for dirname in args.dirs: + assert os.path.exists(dirname) and dirname.startswith("/"), "--dirs must be absolute paths and exist!" + try: + library.add_dir(dirname) + except DuplicateRootException: + pass library.update() + logging.warning("Libraries: {}".format([i["name"] for i in library.get_libraries()])) + logging.warning("Artists: {}".format([i["name"] for i in library.get_artists()])) + cherrypy.tree.mount(PysonicApi(db, library), '/rest/', {'/': {}}) cherrypy.config.update({ 'sessionFilter.on': True, 'tools.sessions.on': True, 'tools.sessions.locking': 'explicit', 'tools.sessions.timeout': 525600, + 'tools.gzip.on': True, 'request.show_tracebacks': True, 'server.socket_port': args.port, 'server.thread_pool': 25, @@ -40,7 +51,7 @@ def main(): }) def signal_handler(signum, stack): - print('Got sig {}, exiting...'.format(signum)) + logging.critical('Got sig {}, exiting...'.format(signum)) cherrypy.engine.exit() signal.signal(signal.SIGINT, signal_handler) @@ -50,7 +61,7 @@ def main(): cherrypy.engine.start() cherrypy.engine.block() finally: - print("API has shut down") + logging.info("API has shut down") cherrypy.engine.exit() if __name__ == '__main__': diff --git a/pysonic/database.py b/pysonic/database.py index 49c7f1a..603f571 100644 --- a/pysonic/database.py +++ b/pysonic/database.py @@ -1,10 +1,14 @@ import os import json import sqlite3 +import logging from itertools import chain from contextlib import closing +logging = logging.getLogger("database") + + def dict_factory(cursor, row): d = {} for idx, col in enumerate(cursor.description): @@ -36,26 +40,25 @@ class PysonicDatabase(object): 'parent' INTEGER NOT NULL, 'isdir' BOOLEAN NOT NULL, 'name' TEXT NOT NULL, + 'type' TEXT, 'title' TEXT, 'album' TEXT, 'artist' TEXT, 'metadata' TEXT - )""", - """INSERT INTO nodes (parent, isdir, name, metadata) - VALUES (-1, 1, 'Main Library', '{"fspath": "/home/dave/Code/pysonic/music/"}');"""] + )"""] with closing(self.db.cursor()) as cursor: cursor.execute("SELECT * FROM sqlite_master WHERE type='table' AND name='meta';") # Initialize DB if len(cursor.fetchall()) == 0: - print("Initializing database") + logging.waring("Initializing database") for query in queries: cursor.execute(query) else: # Migrate if old db exists version = int(cursor.execute("SELECT * FROM meta WHERE key='db_version';").fetchone()['value']) - print("db schema is version {}".format(version)) + logging.warning("db schema is version {}".format(version)) # Virtual file tree def getnode(self, node_id): @@ -67,13 +70,15 @@ class PysonicDatabase(object): return list(chain(*[cursor.execute("SELECT * FROM nodes WHERE parent=?;", (parent_id, )).fetchall() for parent_id in parent_ids])) - def addnode(self, parent, fspath, name): + def addnode(self, parent_id, fspath, name): fullpath = os.path.join(fspath, name) - print("Adding ", fullpath) is_dir = os.path.isdir(fullpath) + return self._addnode(parent_id, name, is_dir) + + def _addnode(self, parent_id, name, is_dir=True): with closing(self.db.cursor()) as cursor: cursor.execute("INSERT INTO nodes (parent, isdir, name) VALUES (?, ?, ?);", - (parent["id"], 1 if is_dir else 0, name)) + (parent_id, 1 if is_dir else 0, name)) return self.getnode(cursor.lastrowid) def delnode(self, node_id): @@ -86,7 +91,7 @@ class PysonicDatabase(object): def update_metadata(self, node_id, mergedict=None, **kwargs): mergedict = mergedict if mergedict else {} - keys_in_table = ["title", "album", "artist"] + keys_in_table = ["title", "album", "artist", "type"] mergedict.update(kwargs) with closing(self.db.cursor()) as cursor: for table_key in keys_in_table: diff --git a/pysonic/library.py b/pysonic/library.py index 95ad967..eb8c3be 100644 --- a/pysonic/library.py +++ b/pysonic/library.py @@ -1,10 +1,14 @@ import os import json +import logging from pysonic.scanner import PysonicFilesystemScanner LETTER_GROUPS = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", - "u", "v", "w", "x-z", "#"] + "u", "v", "w", "xyz", "0123456789"] + + +logging = logging.getLogger("library") def memoize(function): @@ -24,15 +28,28 @@ class NoDataException(Exception): pass +class DuplicateRootException(Exception): + pass + + class PysonicLibrary(object): def __init__(self, database): self.db = database self.scanner = PysonicFilesystemScanner(self) - print("library ready") + logging.info("library ready") def update(self): self.scanner.init_scan() + def add_dir(self, dir_path): + dir_path = os.path.abspath(os.path.normpath(dir_path)) + libraries = [self.db.decode_metadata(i['metadata'])['fspath'] for i in self.db.getnodes(-1)] + if dir_path in libraries: + raise DuplicateRootException("Dir already in library") + else: + new_root = self.db._addnode(-1, 'New Library', is_dir=True) + self.db.update_metadata(new_root['id'], fspath=dir_path) + @memoize def get_libraries(self): """ diff --git a/pysonic/scanner.py b/pysonic/scanner.py index f99f553..9249292 100644 --- a/pysonic/scanner.py +++ b/pysonic/scanner.py @@ -1,8 +1,15 @@ import os import json +import logging +import mimetypes +from time import time from threading import Thread +KNOWN_MIMES = ["audio/mpeg", "audio/flac", "audio/x-wav", "image/jpeg", "image/png"] +logging = logging.getLogger("scanner") + + class PysonicFilesystemScanner(object): def __init__(self, library): self.library = library @@ -13,12 +20,14 @@ class PysonicFilesystemScanner(object): def rescan(self): # Perform directory scan + logging.warning("Beginning library rescan") + start = time() for parent in self.library.get_libraries(): meta = json.loads(parent["metadata"]) - # print("Scanning {}".format(meta["fspath"])) + logging.info("Scanning {}".format(meta["fspath"])) def recurse_dir(path, parent): - # print("Scanning {} with parent {}".format(path, parent)) + logging.info("Scanning {} with parent {}".format(path, parent)) # create or update the database of nodes by comparing sets of names fs_entries = set(os.listdir(path)) db_entires = self.library.db.getnodes(parent["id"]) @@ -28,16 +37,17 @@ class PysonicFilesystemScanner(object): # Create any nodes not found in the db for create in to_create: - new_node = self.library.db.addnode(parent, path, create) + new_node = self.library.db.addnode(parent["id"], path, create) + logging.info("Added", os.path.join(path, create)) db_entires.append(new_node) # Delete any db nodes not found on disk for delete in to_delete: - print("Prune ", delete, "in parent", path) + logging.info("Prune ", delete, "in parent", path) node = [i for i in db_entires if i["name"] == delete] if node: deleted = self.library.db.delnode(node[0]["id"]) - print("Pruned {}, deleting total of {}".format(node, deleted)) + logging.info("Pruned {}, deleting total of {}".format(node, deleted)) for entry in db_entires: if entry["name"] in to_delete: @@ -60,16 +70,28 @@ class PysonicFilesystemScanner(object): title = track_file["name"] if not track_file["title"]: self.library.db.update_metadata(track_file["id"], artist=artist, album=album, title=title) - print("Adding simple metadata for {}/{}/{} #{}".format(artist, album, - title, track_file["id"])) + logging.info("Adding simple metadata for {}/{}/{} #{}".format(artist, album, + title, track_file["id"])) if not album_dir["album"]: self.library.db.update_metadata(album_dir["id"], artist=artist, album=album) - print("Adding simple metadata for {}/{} #{}".format(artist, album, album_dir["id"])) + logging.info("Adding simple metadata for {}/{} #{}".format(artist, album, album_dir["id"])) if not artist_dir["artist"]: self.library.db.update_metadata(artist_dir["id"], artist=artist) - print("Adding simple metadata for {} #{}".format(artist, artist_dir["id"])) - if title == "cover.jpg" and 'cover' not in album_meta: + logging.info("Adding simple metadata for {} #{}".format(artist, artist_dir["id"])) + if title in ["cover.jpg", "cover.png"] and 'cover' not in album_meta: # // add cover art self.library.db.update_metadata(album_dir["id"], cover=track_file["id"]) - print("added cover for {}".format(album_dir['id'])) - print("Metadata scan complete.") + logging.info("added cover for {}".format(album_dir['id'])) + + if track_file["type"] is None: + fpath = self.library.get_filepath(track_file['id']) + ftype, extra = mimetypes.guess_type(fpath) + + if ftype in KNOWN_MIMES: + self.library.db.update_metadata(track_file["id"], type=ftype) + logging.info("added type {} for {}".format(ftype, track_file['id'])) + else: + logging.warning("Ignoring unreadable file at {}, unknown ftype ({}, {})" + .format(fpath, ftype, extra)) + + logging.warning("Library scan complete in {}s".format(int(time() - start)))