diff --git a/pysonic/api.py b/pysonic/api.py index 21758a3..43f1be5 100644 --- a/pysonic/api.py +++ b/pysonic/api.py @@ -1,16 +1,21 @@ import sys import logging import cherrypy +import subprocess +from time import time from bs4 import BeautifulSoup from pysonic.library import LETTER_GROUPS +from pysonic.types import MUSIC_TYPES + logging = logging.getLogger("api") class PysonicApi(object): - def __init__(self, db, library): + def __init__(self, db, library, options): self.db = db self.library = library + self.options = options def response(self, status="ok"): doc = BeautifulSoup('', features='lxml-xml') @@ -100,6 +105,9 @@ class PysonicApi(object): root.append(dirtag) for item in children: + # omit not dirs and media in browser + if not item["isdir"] and item["type"] not in MUSIC_TYPES: + continue child = doc.new_tag("child", id=item["id"], parent=directory["id"], @@ -132,24 +140,34 @@ class PysonicApi(object): yield doc.prettify() @cherrypy.expose - def stream_view(self, id, **kwargs): - # /rest/stream.view?u=dave&s=rid5h452ag6nmb153r8sjtctk8 - # &t=dad1e6f7331160ea7f04120c7fbab1c8&v=1.2.0&c=DSub&id=167&maxBitRate=256 + def stream_view(self, id, maxBitRate="256", **kwargs): + maxBitRate = int(maxBitRate) + assert maxBitRate >= 32 and maxBitRate <= 320 fpath = self.library.get_filepath(id) + meta = self.library.get_file_metadata(id) cherrypy.response.headers['Content-Type'] = 'audio/mpeg' - - def content(): - total = 0 - with open(fpath, "rb") as f: + if self.options.skip_transcode and meta["type"] == "audio/mpeg": + def content(): + with open(fpath, "rb") as f: + while True: + data = f.read(16 * 1024) + if not data: + break + yield data + else: + def content(): + transcode_args = ["ffmpeg", "-i", fpath, "-map", "0:0", "-b:a", + "{}k".format(min(maxBitRate, self.options.max_bitrate)), + "-v", "0", "-f", "mp3", "-"] + logging.info(' '.join(transcode_args)) + start = time() + proc = subprocess.Popen(transcode_args, stdout=subprocess.PIPE) while True: - data = f.read(8192) + data = proc.stdout.read(16 * 1024) if not data: break - total += len(data) yield data - sys.stdout.write('.') - sys.stdout.flush() - logging.info("\nSent {} bytes for {}".format(total, fpath)) + logging.warning("transcoded {} in {}s".format(id, int(time() - start))) return content() stream_view._cp_config = {'response.stream': True} @@ -173,8 +191,6 @@ class PysonicApi(object): break total += len(data) yield data - sys.stdout.write('.') - sys.stdout.flush() logging.info("\nSent {} bytes for {}".format(total, fpath)) return content() diff --git a/pysonic/daemon.py b/pysonic/daemon.py index 33c3e05..34e370a 100644 --- a/pysonic/daemon.py +++ b/pysonic/daemon.py @@ -16,6 +16,11 @@ def main(): parser.add_argument('-d', '--dirs', required=True, nargs='+', help="new music dirs to share") parser.add_argument('-s', '--database-path', default="./db.sqlite", help="path to persistent sqlite database") parser.add_argument('--debug', action="store_true", help="enable development options") + + group = parser.add_argument_group("app options") + group.add_argument("--skip-transcode", action="store_true", help="instead of trancoding mp3s, send as-is") + group.add_argument("--max-bitrate", type=int, default=320, help="maximum send bitrate") + args = parser.parse_args() logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING) @@ -33,7 +38,7 @@ def main(): 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, args), '/rest/', {'/': {}}) cherrypy.config.update({ 'sessionFilter.on': True, 'tools.sessions.on': True, diff --git a/pysonic/database.py b/pysonic/database.py index 603f571..650bdea 100644 --- a/pysonic/database.py +++ b/pysonic/database.py @@ -52,7 +52,7 @@ class PysonicDatabase(object): # Initialize DB if len(cursor.fetchall()) == 0: - logging.waring("Initializing database") + logging.warning("Initializing database") for query in queries: cursor.execute(query) else: @@ -105,7 +105,11 @@ class PysonicDatabase(object): cursor.execute("UPDATE nodes SET metadata=? WHERE id=?;", (json.dumps(metadata), node_id, )) def get_metadata(self, node_id): - return self.decode_metadata(self.getnode(node_id)["metadata"]) + keys_in_table = ["title", "album", "artist", "type"] + node = self.getnode(node_id) + metadata = self.decode_metadata(node["metadata"]) + metadata.update({item: node[item] for item in ["title", "album", "artist", "type"]}) + return metadata def decode_metadata(self, metadata): if metadata: diff --git a/pysonic/library.py b/pysonic/library.py index eb8c3be..d0e55f2 100644 --- a/pysonic/library.py +++ b/pysonic/library.py @@ -69,14 +69,17 @@ class PysonicLibrary(object): return self.db.getnodes(dirid) @memoize - def get_filepath(self, fileid): - parents = [self.db.getnode(fileid)] + def get_filepath(self, nodeid): + parents = [self.db.getnode(nodeid)] while parents[-1]['parent'] != -1: parents.append(self.db.getnode(parents[-1]['parent'])) root = parents.pop() parents.reverse() return os.path.join(json.loads(root['metadata'])['fspath'], *[i['name'] for i in parents]) + def get_file_metadata(self, nodeid): + return self.db.get_metadata(nodeid) + def get_artist_info(self, item_id): # artist = self.db.getnode(item_id) return {"biography": "placeholder biography", diff --git a/pysonic/scanner.py b/pysonic/scanner.py index 9249292..1eb7378 100644 --- a/pysonic/scanner.py +++ b/pysonic/scanner.py @@ -4,9 +4,9 @@ import logging import mimetypes from time import time from threading import Thread +from pysonic.types import KNOWN_MIMES -KNOWN_MIMES = ["audio/mpeg", "audio/flac", "audio/x-wav", "image/jpeg", "image/png"] logging = logging.getLogger("scanner") @@ -38,7 +38,7 @@ class PysonicFilesystemScanner(object): # Create any nodes not found in the db for create in to_create: new_node = self.library.db.addnode(parent["id"], path, create) - logging.info("Added", os.path.join(path, create)) + logging.info("Added {}".format(os.path.join(path, create))) db_entires.append(new_node) # Delete any db nodes not found on disk diff --git a/pysonic/types.py b/pysonic/types.py new file mode 100644 index 0000000..ab6912a --- /dev/null +++ b/pysonic/types.py @@ -0,0 +1,4 @@ + +KNOWN_MIMES = ["audio/mpeg", "audio/flac", "audio/x-wav", "image/jpeg", "image/png"] +MUSIC_TYPES = ["audio/mpeg", "audio/flac", "audio/x-wav"] +IMAGE_TYPES = ["image/jpeg", "image/png"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6063f22 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +beautifulsoup4==4.6.0 +cheroot==5.8.3 +CherryPy==11.0.0 +lxml==3.8.0 +mutagen==1.38 +pkg-resources==0.0.0 +portend==2.1.2 +pytz==2017.2 +six==1.10.0 +tempora==1.8