args and logging
This commit is contained in:
parent
1b151d3905
commit
a0d25381c4
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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__':
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
Loading…
Reference in New Issue