|
|
|
@ -1,500 +1,51 @@
|
|
|
|
|
import logging |
|
|
|
|
import cherrypy |
|
|
|
|
from bs4 import BeautifulSoup |
|
|
|
|
import sqlite3 |
|
|
|
|
import os |
|
|
|
|
from contextlib import closing |
|
|
|
|
import json |
|
|
|
|
from threading import Thread |
|
|
|
|
from itertools import chain |
|
|
|
|
import sys |
|
|
|
|
from pysonic.api import PysonicApi |
|
|
|
|
from pysonic.library import PysonicLibrary |
|
|
|
|
from pysonic.database import PysonicDatabase |
|
|
|
|
|
|
|
|
|
# import pdb |
|
|
|
|
# from pprint import pprint |
|
|
|
|
|
|
|
|
|
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", "#"] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def dict_factory(cursor, row): |
|
|
|
|
d = {} |
|
|
|
|
for idx, col in enumerate(cursor.description): |
|
|
|
|
d[col[0]] = row[idx] |
|
|
|
|
return d |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PysonicDatabase(object): |
|
|
|
|
def __init__(self): |
|
|
|
|
self.sqlite_opts = dict(check_same_thread=False, cached_statements=0, isolation_level=None) |
|
|
|
|
self.db = None |
|
|
|
|
|
|
|
|
|
self.open() |
|
|
|
|
self.migrate() |
|
|
|
|
|
|
|
|
|
self.scanner = Thread(target=self.rescan, daemon=True) |
|
|
|
|
self.scanner.start() |
|
|
|
|
|
|
|
|
|
def open(self): |
|
|
|
|
self.db = sqlite3.connect("db.sqlite", **self.sqlite_opts) |
|
|
|
|
self.db.row_factory = dict_factory |
|
|
|
|
|
|
|
|
|
def migrate(self): |
|
|
|
|
# Create db |
|
|
|
|
queries = ["""CREATE TABLE 'meta' ( |
|
|
|
|
'key' TEXT PRIMARY KEY NOT NULL, |
|
|
|
|
'value' TEXT);""", |
|
|
|
|
"""INSERT INTO meta VALUES ('db_version', '0');""", |
|
|
|
|
"""CREATE TABLE 'nodes' ( |
|
|
|
|
'id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, |
|
|
|
|
'parent' INTEGER NOT NULL, |
|
|
|
|
'isdir' BOOLEAN NOT NULL, |
|
|
|
|
'name' TEXT NOT NULL, |
|
|
|
|
'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") |
|
|
|
|
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)) |
|
|
|
|
|
|
|
|
|
# Virtual file tree |
|
|
|
|
def getnode(self, node_id): |
|
|
|
|
with closing(self.db.cursor()) as cursor: |
|
|
|
|
return cursor.execute("SELECT * FROM nodes WHERE id=?;", (node_id, )).fetchone() |
|
|
|
|
|
|
|
|
|
def getnodes(self, *parent_ids): |
|
|
|
|
with closing(self.db.cursor()) as cursor: |
|
|
|
|
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): |
|
|
|
|
fullpath = os.path.join(fspath, name) |
|
|
|
|
print("Adding ", fullpath) |
|
|
|
|
is_dir = os.path.isdir(fullpath) |
|
|
|
|
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)) |
|
|
|
|
return self.getnode(cursor.lastrowid) |
|
|
|
|
|
|
|
|
|
def delnode(self, node_id): |
|
|
|
|
deleted = 1 |
|
|
|
|
for child in self.getnodes(node_id): |
|
|
|
|
deleted += self.delnode(child["id"]) |
|
|
|
|
with closing(self.db.cursor()) as cursor: |
|
|
|
|
cursor.execute("DELETE FROM nodes WHERE id=?;", (node_id, )) |
|
|
|
|
return deleted |
|
|
|
|
|
|
|
|
|
def update_metadata(self, node_id, mergedict=None, **kwargs): |
|
|
|
|
mergedict = mergedict if mergedict else {} |
|
|
|
|
keys_in_table = ["title", "album", "artist"] |
|
|
|
|
mergedict.update(kwargs) |
|
|
|
|
with closing(self.db.cursor()) as cursor: |
|
|
|
|
for table_key in keys_in_table: |
|
|
|
|
if table_key in mergedict: |
|
|
|
|
cursor.execute("UPDATE nodes SET {}=? WHERE id=?;".format(table_key), |
|
|
|
|
(mergedict[table_key], node_id)) |
|
|
|
|
other_meta = {k: v for k, v in mergedict.items() if k not in keys_in_table} |
|
|
|
|
if other_meta: |
|
|
|
|
metadata = self.get_metadata(node_id) |
|
|
|
|
metadata.update(other_meta) |
|
|
|
|
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"]) |
|
|
|
|
|
|
|
|
|
def decode_metadata(self, metadata): |
|
|
|
|
if metadata: |
|
|
|
|
return json.loads(metadata) |
|
|
|
|
return {} |
|
|
|
|
|
|
|
|
|
def rescan(self): |
|
|
|
|
# Perform directory scan |
|
|
|
|
with closing(self.db.cursor()) as cursor: |
|
|
|
|
|
|
|
|
|
# Find top level dirs, parent=-1 |
|
|
|
|
for parent in cursor.execute("SELECT id, name, metadata FROM nodes WHERE parent=-1;").fetchall(): |
|
|
|
|
meta = json.loads(parent["metadata"]) |
|
|
|
|
# print("Scanning {}".format(meta["fspath"])) |
|
|
|
|
|
|
|
|
|
def recurse_dir(path, parent): |
|
|
|
|
# print("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.getnodes(parent["id"]) |
|
|
|
|
db_entires_names = set([i['name'] for i in db_entires]) |
|
|
|
|
to_delete = db_entires_names - fs_entries |
|
|
|
|
to_create = fs_entries - db_entires_names |
|
|
|
|
|
|
|
|
|
# Create any nodes not found in the db |
|
|
|
|
for create in to_create: |
|
|
|
|
new_node = self.addnode(parent, 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) |
|
|
|
|
node = [i for i in db_entires if i["name"] == delete] |
|
|
|
|
if node: |
|
|
|
|
deleted = self.delnode(node[0]["id"]) |
|
|
|
|
print("Pruned {}, deleting total of {}".format(node, deleted)) |
|
|
|
|
|
|
|
|
|
for entry in db_entires: |
|
|
|
|
if entry["name"] in to_delete: |
|
|
|
|
continue |
|
|
|
|
|
|
|
|
|
if int(entry['isdir']): # 1 means dir |
|
|
|
|
recurse_dir(os.path.join(path, entry["name"]), entry) |
|
|
|
|
# Populate all files for this top-level root |
|
|
|
|
recurse_dir(meta["fspath"], parent) |
|
|
|
|
# |
|
|
|
|
# |
|
|
|
|
# |
|
|
|
|
# Add simple metadata |
|
|
|
|
for artist_dir in self.getnodes(parent["id"]): |
|
|
|
|
artist = artist_dir["name"] |
|
|
|
|
for album_dir in self.getnodes(artist_dir["id"]): |
|
|
|
|
album = album_dir["name"] |
|
|
|
|
album_meta = self.get_metadata(album_dir["id"]) |
|
|
|
|
for track_file in self.getnodes(album_dir["id"]): |
|
|
|
|
title = track_file["name"] |
|
|
|
|
if not track_file["title"]: |
|
|
|
|
self.update_metadata(track_file["id"], artist=artist, album=album, title=title) |
|
|
|
|
print("Adding simple metadata for {}/{}/{} #{}".format(artist, album, |
|
|
|
|
title, track_file["id"])) |
|
|
|
|
if not album_dir["album"]: |
|
|
|
|
self.update_metadata(album_dir["id"], artist=artist, album=album) |
|
|
|
|
print("Adding simple metadata for {}/{} #{}".format(artist, album, album_dir["id"])) |
|
|
|
|
if not artist_dir["artist"]: |
|
|
|
|
self.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: |
|
|
|
|
# // add cover art |
|
|
|
|
self.update_metadata(album_dir["id"], cover=track_file["id"]) |
|
|
|
|
print("added cover for {}".format(album_dir['id'])) |
|
|
|
|
print("Metadata scan complete.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def memoize(function): |
|
|
|
|
memo = {} |
|
|
|
|
|
|
|
|
|
def wrapper(*args): |
|
|
|
|
if args in memo: |
|
|
|
|
return memo[args] |
|
|
|
|
else: |
|
|
|
|
rv = function(*args) |
|
|
|
|
memo[args] = rv |
|
|
|
|
return rv |
|
|
|
|
return wrapper |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NoDataException(Exception): |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PysonicLibrary(object): |
|
|
|
|
def __init__(self, database): |
|
|
|
|
self.db = database |
|
|
|
|
print("library ready") |
|
|
|
|
|
|
|
|
|
@memoize |
|
|
|
|
def get_libraries(self): |
|
|
|
|
""" |
|
|
|
|
Libraries are top-level nodes |
|
|
|
|
""" |
|
|
|
|
return self.db.getnodes(-1) |
|
|
|
|
|
|
|
|
|
@memoize |
|
|
|
|
def get_artists(self): |
|
|
|
|
# Assume artists are second level dirs |
|
|
|
|
return self.db.getnodes(*[item["id"] for item in self.get_libraries()]) |
|
|
|
|
|
|
|
|
|
def get_dir(self, dirid): |
|
|
|
|
return self.db.getnode(dirid) |
|
|
|
|
|
|
|
|
|
def get_dir_children(self, dirid): |
|
|
|
|
return self.db.getnodes(dirid) |
|
|
|
|
|
|
|
|
|
@memoize |
|
|
|
|
def get_filepath(self, fileid): |
|
|
|
|
parents = [self.db.getnode(fileid)] |
|
|
|
|
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_artist_info(self, item_id): |
|
|
|
|
# artist = self.db.getnode(item_id) |
|
|
|
|
return {"biography": "placeholder biography", |
|
|
|
|
"musicBrainzId": "playerholder", |
|
|
|
|
"lastFmUrl": "https://www.last.fm/music/Placeholder", |
|
|
|
|
"smallImageUrl": "", |
|
|
|
|
"mediumImageUrl": "", |
|
|
|
|
"largeImageUrl": "", |
|
|
|
|
"similarArtists": []} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PysonicApi(object): |
|
|
|
|
def __init__(self): |
|
|
|
|
self.db = PysonicDatabase() |
|
|
|
|
self.library = PysonicLibrary(self.db) |
|
|
|
|
|
|
|
|
|
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") |
|
|
|
|
doc.append(root) |
|
|
|
|
return doc, root |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def ping_view(self, **kwargs): |
|
|
|
|
# Called when the app hits the "test connection" server option |
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
doc, root = self.response() |
|
|
|
|
yield doc.prettify() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def getLicense_view(self, **kwargs): |
|
|
|
|
# Called after ping.view |
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
doc, root = self.response() |
|
|
|
|
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")) |
|
|
|
|
yield doc.prettify() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def getMusicFolders_view(self, **kwargs): |
|
|
|
|
# Get list of configured dirs |
|
|
|
|
# {'c': 'DSub', 's': 'bfk9mir8is02u3m5as8ucsehn0', 'v': '1.2.0', |
|
|
|
|
# 't': 'e2b09fb9233d1bfac9abe3dc73017f1e', 'u': 'dave'} |
|
|
|
|
# Access-Control-Allow-Origin:* |
|
|
|
|
# Content-Encoding:gzip |
|
|
|
|
# Content-Type:text/xml; charset=utf-8 |
|
|
|
|
# Server:Jetty(6.1.x) |
|
|
|
|
# Transfer-Encoding:chunked |
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
|
|
|
|
|
doc, root = self.response() |
|
|
|
|
folder_list = doc.new_tag("musicFolders") |
|
|
|
|
root.append(folder_list) |
|
|
|
|
|
|
|
|
|
for folder in self.library.get_libraries(): |
|
|
|
|
entry = doc.new_tag("musicFolder", id=folder["id"]) |
|
|
|
|
entry.attrs["name"] = folder["name"] |
|
|
|
|
folder_list.append(entry) |
|
|
|
|
yield doc.prettify() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def getIndexes_view(self, **kwargs): |
|
|
|
|
# Get listing of top-level dir |
|
|
|
|
# /rest/getIndexes.view?u=dave&s=bfk9mir8is02u3m5as8ucsehn0 |
|
|
|
|
# &t=e2b09fb9233d1bfac9abe3dc73017f1e&v=1.2.0&c=DSub HTTP/1.1 |
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
doc, root = self.response() |
|
|
|
|
indexes = doc.new_tag("indexes", lastModified="1502310831000", ignoredArticles="The El La Los Las Le Les") |
|
|
|
|
doc.append(indexes) |
|
|
|
|
|
|
|
|
|
for letter in LETTER_GROUPS: |
|
|
|
|
index = doc.new_tag("index") |
|
|
|
|
index.attrs["name"] = letter.upper() |
|
|
|
|
indexes.append(index) |
|
|
|
|
for artist in self.library.get_artists(): |
|
|
|
|
if artist["name"][0].lower() == letter: |
|
|
|
|
artist_tag = doc.new_tag("artist") |
|
|
|
|
artist_tag.attrs.update({"id": artist["id"], "name": artist["name"]}) |
|
|
|
|
index.append(artist_tag) |
|
|
|
|
yield doc.prettify() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def getMusicDirectory_view(self, id, **kwargs): |
|
|
|
|
""" |
|
|
|
|
List an artist dir |
|
|
|
|
""" |
|
|
|
|
dir_id = int(id) |
|
|
|
|
|
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
doc, root = self.response() |
|
|
|
|
|
|
|
|
|
dirtag = doc.new_tag("directory") |
|
|
|
|
|
|
|
|
|
directory = self.library.get_dir(dir_id) |
|
|
|
|
dir_meta = self.db.decode_metadata(directory["metadata"]) |
|
|
|
|
children = self.library.get_dir_children(dir_id) |
|
|
|
|
dirtag.attrs.update(name=directory['name'], id=directory['id'], |
|
|
|
|
parent=directory['parent'], playCount=10) |
|
|
|
|
root.append(dirtag) |
|
|
|
|
|
|
|
|
|
for item in children: |
|
|
|
|
child = doc.new_tag("child", |
|
|
|
|
id=item["id"], |
|
|
|
|
parent=directory["id"], |
|
|
|
|
isDir="true" if item['isdir'] else "false", |
|
|
|
|
title=item["name"], |
|
|
|
|
album=item["name"], |
|
|
|
|
artist=directory["name"], |
|
|
|
|
# playCount="5", |
|
|
|
|
# created="2016-04-25T07:31:33.000Z" |
|
|
|
|
# track="3", |
|
|
|
|
# year="2012", |
|
|
|
|
# genre="Other", |
|
|
|
|
# coverArt="12835", |
|
|
|
|
# contentType="audio/mpeg" |
|
|
|
|
# suffix="mp3" |
|
|
|
|
# size="15838864" |
|
|
|
|
# duration="395" |
|
|
|
|
# bitRate="320" |
|
|
|
|
# path="Cosmic Gate/Sign Of The Times/03 Flatline (featuring Kyler England).mp3" |
|
|
|
|
# albumId="933" |
|
|
|
|
# artistId="353" |
|
|
|
|
# type="music"/> |
|
|
|
|
) |
|
|
|
|
item_meta = self.db.decode_metadata(item['metadata']) |
|
|
|
|
if 'cover' in item_meta: |
|
|
|
|
child.attrs["coverArt"] = item_meta["cover"] |
|
|
|
|
elif 'cover' in dir_meta: |
|
|
|
|
child.attrs["coverArt"] = dir_meta["cover"] |
|
|
|
|
dirtag.append(child) |
|
|
|
|
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 |
|
|
|
|
fpath = self.library.get_filepath(id) |
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'audio/mpeg' |
|
|
|
|
|
|
|
|
|
def content(): |
|
|
|
|
total = 0 |
|
|
|
|
with open(fpath, "rb") as f: |
|
|
|
|
while True: |
|
|
|
|
data = f.read(8192) |
|
|
|
|
if not data: |
|
|
|
|
break |
|
|
|
|
total += len(data) |
|
|
|
|
yield data |
|
|
|
|
sys.stdout.write('.') |
|
|
|
|
sys.stdout.flush() |
|
|
|
|
print("\nSent {} bytes for {}".format(total, fpath)) |
|
|
|
|
return content() |
|
|
|
|
stream_view._cp_config = {'response.stream': True} |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def getCoverArt_view(self, id, **kwargs): |
|
|
|
|
# /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' |
|
|
|
|
|
|
|
|
|
def content(): |
|
|
|
|
total = 0 |
|
|
|
|
with open(fpath, "rb") as f: |
|
|
|
|
while True: |
|
|
|
|
data = f.read(8192) |
|
|
|
|
if not data: |
|
|
|
|
break |
|
|
|
|
total += len(data) |
|
|
|
|
yield data |
|
|
|
|
sys.stdout.write('.') |
|
|
|
|
sys.stdout.flush() |
|
|
|
|
print("\nSent {} bytes for {}".format(total, fpath)) |
|
|
|
|
return content() |
|
|
|
|
|
|
|
|
|
getCoverArt_view._cp_config = {'response.stream': True} |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def getArtistInfo_view(self, id, includeNotPresent="true", **kwargs): |
|
|
|
|
#/rest/getArtistInfo.view? |
|
|
|
|
# u=dave |
|
|
|
|
# s=gqua9i6c414aomjok8f6b0kdp1 |
|
|
|
|
# t=ed1d31850bbd27690687305d9ccbdabf |
|
|
|
|
# v=1.2.0 |
|
|
|
|
# c=DSub |
|
|
|
|
# id=7 |
|
|
|
|
# includeNotPresent=true |
|
|
|
|
info = self.library.get_artist_info(id) |
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
doc, root = self.response() |
|
|
|
|
|
|
|
|
|
dirtag = doc.new_tag("artistInfo") |
|
|
|
|
root.append(dirtag) |
|
|
|
|
|
|
|
|
|
for key, value in info.items(): |
|
|
|
|
if key == "similarArtists": |
|
|
|
|
continue |
|
|
|
|
tag = doc.new_tag(key) |
|
|
|
|
tag.append(str(value)) |
|
|
|
|
# print(dir(tag)) |
|
|
|
|
# print(value) |
|
|
|
|
dirtag.append(tag) |
|
|
|
|
yield doc.prettify() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def getUser_view(self, u, username, **kwargs): |
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
doc, root = self.response() |
|
|
|
|
user = doc.new_tag("user", |
|
|
|
|
username="admin", |
|
|
|
|
email="admin@localhost", |
|
|
|
|
scrobblingEnabled="false", |
|
|
|
|
adminRole="false", |
|
|
|
|
settingsRole="false", |
|
|
|
|
downloadRole="true", |
|
|
|
|
uploadRole="false", |
|
|
|
|
playlistRole="true", |
|
|
|
|
coverArtRole="false", |
|
|
|
|
commentRole="false", |
|
|
|
|
podcastRole="false", |
|
|
|
|
streamRole="true", |
|
|
|
|
jukeboxRole="false", |
|
|
|
|
shareRole="true", |
|
|
|
|
videoConversionRole="false", |
|
|
|
|
avatarLastChanged="2017-08-07T20:16:24.596Z") |
|
|
|
|
root.append(user) |
|
|
|
|
folder = doc.new_tag("folder") |
|
|
|
|
folder.append("0") |
|
|
|
|
user.append(folder) |
|
|
|
|
yield doc.prettify() |
|
|
|
|
def main(): |
|
|
|
|
import argparse |
|
|
|
|
import signal |
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Pysonic music streaming server") |
|
|
|
|
|
|
|
|
|
def main(): |
|
|
|
|
parser.add_argument('-p', '--port', default=8080, type=int, help="tcp port to listen on") |
|
|
|
|
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") |
|
|
|
|
args = parser.parse_args() |
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
|
|
logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING) |
|
|
|
|
|
|
|
|
|
cherrypy.tree.mount(PysonicApi(), '/rest/', {'/': {}}) |
|
|
|
|
db = PysonicDatabase(path=args.database_path) |
|
|
|
|
library = PysonicLibrary(db) |
|
|
|
|
library.update() |
|
|
|
|
|
|
|
|
|
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, |
|
|
|
|
'request.show_tracebacks': True, |
|
|
|
|
'server.socket_port': 3000, |
|
|
|
|
'server.socket_port': args.port, |
|
|
|
|
'server.thread_pool': 25, |
|
|
|
|
'server.socket_host': '0.0.0.0', |
|
|
|
|
'server.show_tracebacks': True, |
|
|
|
|
'server.socket_timeout': 5, |
|
|
|
|
'log.screen': False, |
|
|
|
|
'engine.autoreload.on': True |
|
|
|
|
'engine.autoreload.on': args.debug |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
def signal_handler(signum, stack): |
|
|
|
|
print('Got sig {}, exiting...'.format(signum)) |
|
|
|
|
cherrypy.engine.exit() |
|
|
|
|
|
|
|
|
|
signal.signal(signal.SIGINT, signal_handler) |
|
|
|
|
signal.signal(signal.SIGTERM, signal_handler) |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
cherrypy.engine.start() |
|
|
|
|
cherrypy.engine.block() |
|
|
|
|