args and logging

This commit is contained in:
dave 2017-08-13 21:13:46 -07:00
parent 1b151d3905
commit a0d25381c4
5 changed files with 94 additions and 37 deletions

View File

@ -1,17 +1,17 @@
import sys import sys
import logging
import cherrypy import cherrypy
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from pysonic.library import LETTER_GROUPS from pysonic.library import LETTER_GROUPS
logging = logging.getLogger("api")
class PysonicApi(object): class PysonicApi(object):
def __init__(self, db, library): def __init__(self, db, library):
self.db = db self.db = db
self.library = library 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"): def response(self, status="ok"):
doc = BeautifulSoup('', features='lxml-xml') doc = BeautifulSoup('', features='lxml-xml')
root = doc.new_tag("subsonic-response", xmlns="http://subsonic.org/restapi", status=status, version="1.15.0") 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", root.append(doc.new_tag("license",
valid="true", valid="true",
email="admin@localhost", email="admin@localhost",
licenseExpires="2018-06-22T10:31:49.921Z", licenseExpires="2100-01-01T00:00:00.000Z",
trialExpires="2016-06-29T03:03:58.200Z")) trialExpires="2100-01-01T01:01:00.000Z"))
yield doc.prettify() yield doc.prettify()
@cherrypy.expose @cherrypy.expose
@ -74,7 +74,7 @@ class PysonicApi(object):
index.attrs["name"] = letter.upper() index.attrs["name"] = letter.upper()
indexes.append(index) indexes.append(index)
for artist in self.library.get_artists(): 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 = doc.new_tag("artist")
artist_tag.attrs.update({"id": artist["id"], "name": artist["name"]}) artist_tag.attrs.update({"id": artist["id"], "name": artist["name"]})
index.append(artist_tag) index.append(artist_tag)
@ -149,7 +149,7 @@ class PysonicApi(object):
yield data yield data
sys.stdout.write('.') sys.stdout.write('.')
sys.stdout.flush() sys.stdout.flush()
print("\nSent {} bytes for {}".format(total, fpath)) logging.info("\nSent {} bytes for {}".format(total, fpath))
return content() return content()
stream_view._cp_config = {'response.stream': True} stream_view._cp_config = {'response.stream': True}
@ -158,7 +158,11 @@ class PysonicApi(object):
# /rest/getCoverArt.view?u=dave&s=bfk9mir8is02u3m5as8ucsehn0 # /rest/getCoverArt.view?u=dave&s=bfk9mir8is02u3m5as8ucsehn0
# &t=e2b09fb9233d1bfac9abe3dc73017f1e&v=1.2.0&c=DSub&id=12833 # &t=e2b09fb9233d1bfac9abe3dc73017f1e&v=1.2.0&c=DSub&id=12833
fpath = self.library.get_filepath(id) 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(): def content():
total = 0 total = 0
@ -171,7 +175,7 @@ class PysonicApi(object):
yield data yield data
sys.stdout.write('.') sys.stdout.write('.')
sys.stdout.flush() sys.stdout.flush()
print("\nSent {} bytes for {}".format(total, fpath)) logging.info("\nSent {} bytes for {}".format(total, fpath))
return content() return content()
getCoverArt_view._cp_config = {'response.stream': True} getCoverArt_view._cp_config = {'response.stream': True}
@ -198,8 +202,6 @@ class PysonicApi(object):
continue continue
tag = doc.new_tag(key) tag = doc.new_tag(key)
tag.append(str(value)) tag.append(str(value))
# print(dir(tag))
# print(value)
dirtag.append(tag) dirtag.append(tag)
yield doc.prettify() yield doc.prettify()

View File

@ -1,7 +1,8 @@
import os
import logging import logging
import cherrypy import cherrypy
from pysonic.api import PysonicApi from pysonic.api import PysonicApi
from pysonic.library import PysonicLibrary from pysonic.library import PysonicLibrary, DuplicateRootException
from pysonic.database import PysonicDatabase from pysonic.database import PysonicDatabase
@ -21,14 +22,24 @@ def main():
db = PysonicDatabase(path=args.database_path) db = PysonicDatabase(path=args.database_path)
library = PysonicLibrary(db) 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() 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.tree.mount(PysonicApi(db, library), '/rest/', {'/': {}})
cherrypy.config.update({ cherrypy.config.update({
'sessionFilter.on': True, 'sessionFilter.on': True,
'tools.sessions.on': True, 'tools.sessions.on': True,
'tools.sessions.locking': 'explicit', 'tools.sessions.locking': 'explicit',
'tools.sessions.timeout': 525600, 'tools.sessions.timeout': 525600,
'tools.gzip.on': True,
'request.show_tracebacks': True, 'request.show_tracebacks': True,
'server.socket_port': args.port, 'server.socket_port': args.port,
'server.thread_pool': 25, 'server.thread_pool': 25,
@ -40,7 +51,7 @@ def main():
}) })
def signal_handler(signum, stack): def signal_handler(signum, stack):
print('Got sig {}, exiting...'.format(signum)) logging.critical('Got sig {}, exiting...'.format(signum))
cherrypy.engine.exit() cherrypy.engine.exit()
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
@ -50,7 +61,7 @@ def main():
cherrypy.engine.start() cherrypy.engine.start()
cherrypy.engine.block() cherrypy.engine.block()
finally: finally:
print("API has shut down") logging.info("API has shut down")
cherrypy.engine.exit() cherrypy.engine.exit()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,10 +1,14 @@
import os import os
import json import json
import sqlite3 import sqlite3
import logging
from itertools import chain from itertools import chain
from contextlib import closing from contextlib import closing
logging = logging.getLogger("database")
def dict_factory(cursor, row): def dict_factory(cursor, row):
d = {} d = {}
for idx, col in enumerate(cursor.description): for idx, col in enumerate(cursor.description):
@ -36,26 +40,25 @@ class PysonicDatabase(object):
'parent' INTEGER NOT NULL, 'parent' INTEGER NOT NULL,
'isdir' BOOLEAN NOT NULL, 'isdir' BOOLEAN NOT NULL,
'name' TEXT NOT NULL, 'name' TEXT NOT NULL,
'type' TEXT,
'title' TEXT, 'title' TEXT,
'album' TEXT, 'album' TEXT,
'artist' TEXT, 'artist' TEXT,
'metadata' 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: with closing(self.db.cursor()) as cursor:
cursor.execute("SELECT * FROM sqlite_master WHERE type='table' AND name='meta';") cursor.execute("SELECT * FROM sqlite_master WHERE type='table' AND name='meta';")
# Initialize DB # Initialize DB
if len(cursor.fetchall()) == 0: if len(cursor.fetchall()) == 0:
print("Initializing database") logging.waring("Initializing database")
for query in queries: for query in queries:
cursor.execute(query) cursor.execute(query)
else: else:
# Migrate if old db exists # Migrate if old db exists
version = int(cursor.execute("SELECT * FROM meta WHERE key='db_version';").fetchone()['value']) 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 # Virtual file tree
def getnode(self, node_id): 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() return list(chain(*[cursor.execute("SELECT * FROM nodes WHERE parent=?;", (parent_id, )).fetchall()
for parent_id in parent_ids])) 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) fullpath = os.path.join(fspath, name)
print("Adding ", fullpath)
is_dir = os.path.isdir(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: with closing(self.db.cursor()) as cursor:
cursor.execute("INSERT INTO nodes (parent, isdir, name) VALUES (?, ?, ?);", 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) return self.getnode(cursor.lastrowid)
def delnode(self, node_id): def delnode(self, node_id):
@ -86,7 +91,7 @@ class PysonicDatabase(object):
def update_metadata(self, node_id, mergedict=None, **kwargs): def update_metadata(self, node_id, mergedict=None, **kwargs):
mergedict = mergedict if mergedict else {} mergedict = mergedict if mergedict else {}
keys_in_table = ["title", "album", "artist"] keys_in_table = ["title", "album", "artist", "type"]
mergedict.update(kwargs) mergedict.update(kwargs)
with closing(self.db.cursor()) as cursor: with closing(self.db.cursor()) as cursor:
for table_key in keys_in_table: for table_key in keys_in_table:

View File

@ -1,10 +1,14 @@
import os import os
import json import json
import logging
from pysonic.scanner import PysonicFilesystemScanner 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", 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): def memoize(function):
@ -24,15 +28,28 @@ class NoDataException(Exception):
pass pass
class DuplicateRootException(Exception):
pass
class PysonicLibrary(object): class PysonicLibrary(object):
def __init__(self, database): def __init__(self, database):
self.db = database self.db = database
self.scanner = PysonicFilesystemScanner(self) self.scanner = PysonicFilesystemScanner(self)
print("library ready") logging.info("library ready")
def update(self): def update(self):
self.scanner.init_scan() 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 @memoize
def get_libraries(self): def get_libraries(self):
""" """

View File

@ -1,8 +1,15 @@
import os import os
import json import json
import logging
import mimetypes
from time import time
from threading import Thread from threading import Thread
KNOWN_MIMES = ["audio/mpeg", "audio/flac", "audio/x-wav", "image/jpeg", "image/png"]
logging = logging.getLogger("scanner")
class PysonicFilesystemScanner(object): class PysonicFilesystemScanner(object):
def __init__(self, library): def __init__(self, library):
self.library = library self.library = library
@ -13,12 +20,14 @@ class PysonicFilesystemScanner(object):
def rescan(self): def rescan(self):
# Perform directory scan # Perform directory scan
logging.warning("Beginning library rescan")
start = time()
for parent in self.library.get_libraries(): for parent in self.library.get_libraries():
meta = json.loads(parent["metadata"]) meta = json.loads(parent["metadata"])
# print("Scanning {}".format(meta["fspath"])) logging.info("Scanning {}".format(meta["fspath"]))
def recurse_dir(path, parent): 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 # create or update the database of nodes by comparing sets of names
fs_entries = set(os.listdir(path)) fs_entries = set(os.listdir(path))
db_entires = self.library.db.getnodes(parent["id"]) db_entires = self.library.db.getnodes(parent["id"])
@ -28,16 +37,17 @@ class PysonicFilesystemScanner(object):
# Create any nodes not found in the db # Create any nodes not found in the db
for create in to_create: 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) db_entires.append(new_node)
# Delete any db nodes not found on disk # Delete any db nodes not found on disk
for delete in to_delete: 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] node = [i for i in db_entires if i["name"] == delete]
if node: if node:
deleted = self.library.db.delnode(node[0]["id"]) 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: for entry in db_entires:
if entry["name"] in to_delete: if entry["name"] in to_delete:
@ -60,16 +70,28 @@ class PysonicFilesystemScanner(object):
title = track_file["name"] title = track_file["name"]
if not track_file["title"]: if not track_file["title"]:
self.library.db.update_metadata(track_file["id"], artist=artist, album=album, title=title) self.library.db.update_metadata(track_file["id"], artist=artist, album=album, title=title)
print("Adding simple metadata for {}/{}/{} #{}".format(artist, album, logging.info("Adding simple metadata for {}/{}/{} #{}".format(artist, album,
title, track_file["id"])) title, track_file["id"]))
if not album_dir["album"]: if not album_dir["album"]:
self.library.db.update_metadata(album_dir["id"], artist=artist, album=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"]: if not artist_dir["artist"]:
self.library.db.update_metadata(artist_dir["id"], artist=artist) self.library.db.update_metadata(artist_dir["id"], artist=artist)
print("Adding simple metadata for {} #{}".format(artist, artist_dir["id"])) logging.info("Adding simple metadata for {} #{}".format(artist, artist_dir["id"]))
if title == "cover.jpg" and 'cover' not in album_meta: if title in ["cover.jpg", "cover.png"] and 'cover' not in album_meta:
# // add cover art # // add cover art
self.library.db.update_metadata(album_dir["id"], cover=track_file["id"]) self.library.db.update_metadata(album_dir["id"], cover=track_file["id"])
print("added cover for {}".format(album_dir['id'])) logging.info("added cover for {}".format(album_dir['id']))
print("Metadata scan complete.")
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)))