|
|
|
@ -1,157 +1,15 @@
|
|
|
|
|
import re |
|
|
|
|
import json |
|
|
|
|
import logging |
|
|
|
|
import subprocess |
|
|
|
|
from time import time |
|
|
|
|
from random import shuffle |
|
|
|
|
from threading import Thread |
|
|
|
|
import cherrypy |
|
|
|
|
from collections import defaultdict |
|
|
|
|
from bs4 import BeautifulSoup |
|
|
|
|
from pysonic.library import LETTER_GROUPS |
|
|
|
|
from pysonic.types import MUSIC_TYPES |
|
|
|
|
from pysonic.apilib import formatresponse, ApiResponse |
|
|
|
|
import cherrypy |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CALLBACK_RE = re.compile(r'^[a-zA-Z0-9_]+$') |
|
|
|
|
logging = logging.getLogger("api") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
response_formats = defaultdict(lambda: "render_xml") |
|
|
|
|
response_formats["json"] = "render_json" |
|
|
|
|
response_formats["jsonp"] = "render_jsonp" |
|
|
|
|
|
|
|
|
|
response_headers = defaultdict(lambda: "text/xml; charset=utf-8") |
|
|
|
|
response_headers["json"] = "application/json; charset=utf-8" |
|
|
|
|
response_headers["jsonp"] = "text/javascript; charset=utf-8" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def formatresponse(func): |
|
|
|
|
""" |
|
|
|
|
Decorator for rendering ApiResponse responses |
|
|
|
|
""" |
|
|
|
|
def wrapper(*args, **kwargs): |
|
|
|
|
response = func(*args, **kwargs) |
|
|
|
|
response_format = kwargs.get("f", "xml") |
|
|
|
|
callback = kwargs.get("callback", None) |
|
|
|
|
cherrypy.response.headers['Content-Type'] = response_headers[response_format] |
|
|
|
|
renderer = getattr(response, response_formats[response_format]) |
|
|
|
|
if response_format == "jsonp": |
|
|
|
|
if callback is None: |
|
|
|
|
return response.render_xml().encode('UTF-8') # copy original subsonic behavior |
|
|
|
|
else: |
|
|
|
|
return renderer(callback).encode('UTF-8') |
|
|
|
|
return renderer().encode('UTF-8') |
|
|
|
|
return wrapper |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApiResponse(object): |
|
|
|
|
def __init__(self, status="ok", version="1.15.0"): |
|
|
|
|
""" |
|
|
|
|
ApiResponses are python data structures that can be converted to other formats. The response has a status and a |
|
|
|
|
version. The response data structure is stored in self.data and follows these rules: |
|
|
|
|
- self.data is a dict |
|
|
|
|
- the dict's values become either child nodes or attributes, named by the key |
|
|
|
|
- lists become many oner one child |
|
|
|
|
- dict values are not allowed |
|
|
|
|
- all other types (str, int, NoneType) are attributes |
|
|
|
|
:param status: |
|
|
|
|
:param version: |
|
|
|
|
""" |
|
|
|
|
self.status = status |
|
|
|
|
self.version = version |
|
|
|
|
self.data = defaultdict(lambda: list()) |
|
|
|
|
|
|
|
|
|
def add_child(self, _type, _parent="", _real_parent=None, **kwargs): |
|
|
|
|
parent = _real_parent if _real_parent else self.get_child(_parent) |
|
|
|
|
m = defaultdict(lambda: list()) |
|
|
|
|
m.update(dict(kwargs)) |
|
|
|
|
parent[_type].append(m) |
|
|
|
|
return m |
|
|
|
|
|
|
|
|
|
def get_child(self, _path): |
|
|
|
|
parent_path = _path.split(".") |
|
|
|
|
parent = self.data |
|
|
|
|
for item in parent_path: |
|
|
|
|
if not item: |
|
|
|
|
continue |
|
|
|
|
parent = parent.get(item)[0] |
|
|
|
|
return parent |
|
|
|
|
|
|
|
|
|
def set_attrs(self, _path, **attrs): |
|
|
|
|
parent = self.get_child(_path) |
|
|
|
|
if type(parent) not in (dict, defaultdict): |
|
|
|
|
raise Exception("wot") |
|
|
|
|
parent.update(attrs) |
|
|
|
|
|
|
|
|
|
def render_json(self): |
|
|
|
|
def _flatten_json(item): |
|
|
|
|
""" |
|
|
|
|
Convert defaultdicts to dicts and remove lists where node has 1 or no child |
|
|
|
|
""" |
|
|
|
|
listed_attrs = ["folder"] |
|
|
|
|
d = {} |
|
|
|
|
for k, v in item.items(): |
|
|
|
|
if type(v) is list: |
|
|
|
|
if len(v) > 1: |
|
|
|
|
d[k] = [] |
|
|
|
|
for subitem in v: |
|
|
|
|
d[k].append(_flatten_json(subitem)) |
|
|
|
|
elif len(v) == 1: |
|
|
|
|
d[k] = _flatten_json(v[0]) |
|
|
|
|
else: |
|
|
|
|
d[k] = {} |
|
|
|
|
else: |
|
|
|
|
d[k] = [v] if k in listed_attrs else v |
|
|
|
|
return d |
|
|
|
|
|
|
|
|
|
data = _flatten_json(self.data) |
|
|
|
|
return json.dumps({"subsonic-response": dict(status=self.status, version=self.version, **data)}, indent=4) |
|
|
|
|
|
|
|
|
|
def render_jsonp(self, callback): |
|
|
|
|
assert CALLBACK_RE.match(callback), "Invalid callback" |
|
|
|
|
return "{}({});".format(callback, self.render_json()) |
|
|
|
|
|
|
|
|
|
def render_xml(self): |
|
|
|
|
text_attrs = ['largeImageUrl', 'musicBrainzId', 'smallImageUrl', 'mediumImageUrl', 'lastFmUrl', 'biography', |
|
|
|
|
'folder'] |
|
|
|
|
selftext_attrs = ['value'] |
|
|
|
|
# These attributes will be placed in <hello>{{ value }}</hello> tags instead of hello="{{ value }}" on parent |
|
|
|
|
doc = BeautifulSoup('', features='lxml-xml') |
|
|
|
|
root = doc.new_tag("subsonic-response", xmlns="http://subsonic.org/restapi", |
|
|
|
|
status=self.status, |
|
|
|
|
version=self.version) |
|
|
|
|
doc.append(root) |
|
|
|
|
|
|
|
|
|
def _render_xml(node, parent): |
|
|
|
|
""" |
|
|
|
|
For every key in the node dict, the parent gets a new child tag with name == key |
|
|
|
|
If the value is a dict, it becomes the new tag's attrs |
|
|
|
|
If the value is a list, the parent gets many new tags with each dict as attrs |
|
|
|
|
If the value is str int etc, parent gets attrs |
|
|
|
|
""" |
|
|
|
|
for key, value in node.items(): |
|
|
|
|
if type(value) in (dict, defaultdict): |
|
|
|
|
tag = doc.new_tag(key) |
|
|
|
|
parent.append(tag) |
|
|
|
|
tag.attrs.update(value) |
|
|
|
|
elif type(value) is list: |
|
|
|
|
for item in value: |
|
|
|
|
tag = doc.new_tag(key) |
|
|
|
|
parent.append(tag) |
|
|
|
|
_render_xml(item, tag) |
|
|
|
|
else: |
|
|
|
|
if key in text_attrs: |
|
|
|
|
tag = doc.new_tag(key) |
|
|
|
|
parent.append(tag) |
|
|
|
|
tag.append(str(value)) |
|
|
|
|
elif key in selftext_attrs: |
|
|
|
|
parent.append(str(value)) |
|
|
|
|
else: |
|
|
|
|
parent.attrs[key] = value |
|
|
|
|
_render_xml(self.data, root) |
|
|
|
|
return doc.prettify() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PysonicApi(object): |
|
|
|
|
def __init__(self, db, library, options): |
|
|
|
|
self.db = db |
|
|
|
@ -190,49 +48,50 @@ class PysonicApi(object):
|
|
|
|
|
def getIndexes_view(self, **kwargs): |
|
|
|
|
# Get listing of top-level dir |
|
|
|
|
response = ApiResponse() |
|
|
|
|
# TODO real lastmodified date |
|
|
|
|
# TODO deal with ignoredArticles |
|
|
|
|
response.add_child("indexes", lastModified="1502310831000", ignoredArticles="The El La Los Las Le Les") |
|
|
|
|
artists = self.library.get_artists(sortby="name", order="asc") |
|
|
|
|
for letter in LETTER_GROUPS: |
|
|
|
|
index = response.add_child("index", _parent="indexes", name=letter.upper()) |
|
|
|
|
for artist in self.library.get_artists(): |
|
|
|
|
for artist in artists: |
|
|
|
|
if artist["name"][0].lower() in letter: |
|
|
|
|
response.add_child("artist", _real_parent=index, id=artist["id"], name=artist["name"]) |
|
|
|
|
response.add_child("artist", _real_parent=index, id=artist["dir"], name=artist["name"]) |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def savePlayQueue_view(self, id, current, position, **kwargs): |
|
|
|
|
print("TODO save playlist with items {} current {} position {}".format(id, current, position)) |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def getAlbumList_view(self, type, size=50, offset=0, **kwargs): |
|
|
|
|
albums = self.library.get_albums() |
|
|
|
|
qargs = {} |
|
|
|
|
if type == "random": |
|
|
|
|
shuffle(albums) |
|
|
|
|
qargs.update(sortby="random") |
|
|
|
|
elif type == "alphabeticalByName": |
|
|
|
|
albums.sort(key=lambda item: item.get("id3_album", item["album"] if item["album"] else "zzzzzUnsortable")) |
|
|
|
|
qargs.update(sortby="name", order="asc") |
|
|
|
|
elif type == "newest": |
|
|
|
|
qargs.update(sortby="added", order="desc") |
|
|
|
|
else: |
|
|
|
|
raise NotImplemented() |
|
|
|
|
albumset = albums[0 + int(offset):int(size) + int(offset)] |
|
|
|
|
|
|
|
|
|
qargs.update(limit=(offset, size)) |
|
|
|
|
|
|
|
|
|
albums = self.library.get_albums(**qargs) |
|
|
|
|
|
|
|
|
|
response = ApiResponse() |
|
|
|
|
|
|
|
|
|
response.add_child("albumList") |
|
|
|
|
|
|
|
|
|
for album in albumset: |
|
|
|
|
album_meta = album['metadata'] |
|
|
|
|
album_kw = dict(id=album["id"], |
|
|
|
|
parent=album["parent"], |
|
|
|
|
isDir="true" if album['isdir'] else "false", |
|
|
|
|
title=album_meta.get("id3_title", album["name"]), #TODO these cant be blank or dsub gets mad |
|
|
|
|
album=album_meta.get("id3_album", album["album"]), |
|
|
|
|
artist=album_meta.get("id3_artist", album["artist"]), |
|
|
|
|
for album in albums: |
|
|
|
|
album_kw = dict(id=album["dir"], |
|
|
|
|
parent=album["artistdir"], |
|
|
|
|
isDir="true", |
|
|
|
|
title=album["name"], |
|
|
|
|
album=album["name"], |
|
|
|
|
artist=album["artistname"], |
|
|
|
|
coverArt=album["coverid"] |
|
|
|
|
#year=TODO |
|
|
|
|
# playCount="0" |
|
|
|
|
# created="2016-05-08T05:31:31.000Z"/>) |
|
|
|
|
) |
|
|
|
|
if 'cover' in album_meta: |
|
|
|
|
album_kw["coverArt"] = album_meta["cover"] |
|
|
|
|
if 'id3_year' in album_meta: |
|
|
|
|
album_kw["year"] = album_meta['id3_year'] |
|
|
|
|
response.add_child("album", _parent="albumList", **album_kw) |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
@ -243,84 +102,67 @@ class PysonicApi(object):
|
|
|
|
|
List an artist dir |
|
|
|
|
""" |
|
|
|
|
dir_id = int(id) |
|
|
|
|
|
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
dirtype, dirinfo, entity = self.library.db.get_subsonic_musicdir(dirid=dir_id) |
|
|
|
|
|
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("directory") |
|
|
|
|
response.set_attrs(_path="directory", name=entity['name'], id=entity['id'], |
|
|
|
|
parent=dirinfo['parent'], playCount=420) |
|
|
|
|
|
|
|
|
|
directory = self.library.get_dir(dir_id) |
|
|
|
|
dir_meta = directory["metadata"] |
|
|
|
|
children = self.library.get_dir_children(dir_id) |
|
|
|
|
response.set_attrs(_path="directory", name=directory['name'], id=directory['id'], |
|
|
|
|
parent=directory['parent'], playCount=10) |
|
|
|
|
|
|
|
|
|
for item in children: |
|
|
|
|
for childtype, child in entity["children"]: |
|
|
|
|
# omit not dirs and media in browser |
|
|
|
|
if not item["isdir"] and item["type"] not in MUSIC_TYPES: |
|
|
|
|
continue |
|
|
|
|
item_meta = item['metadata'] |
|
|
|
|
response.add_child("child", _parent="directory", **self.render_node(item, item_meta, directory, dir_meta)) |
|
|
|
|
# if not item["isdir"] and item["type"] not in MUSIC_TYPES: |
|
|
|
|
# continue |
|
|
|
|
# item_meta = item['metadata'] |
|
|
|
|
moreargs = {} |
|
|
|
|
if childtype == "album": |
|
|
|
|
moreargs.update(name=child["name"], |
|
|
|
|
isDir="true", # TODO song files in artist dir |
|
|
|
|
parent=entity["id"], |
|
|
|
|
id=child["dir"]) |
|
|
|
|
if child["coverid"]: |
|
|
|
|
moreargs.update(coverArt=child["coverid"]) |
|
|
|
|
# album=item["name"], |
|
|
|
|
# title=item["name"], # TODO dupe? |
|
|
|
|
# artist=artist["name"], |
|
|
|
|
# coverArt=item["coverid"], |
|
|
|
|
elif childtype == "song": |
|
|
|
|
moreargs.update(name=child["title"], |
|
|
|
|
artist=child["_artist"]["name"], |
|
|
|
|
contentType=child["format"], |
|
|
|
|
id=child["id"], |
|
|
|
|
duration=child["length"], |
|
|
|
|
isDir="false", |
|
|
|
|
parent=entity["dir"], |
|
|
|
|
# title=xxx |
|
|
|
|
) |
|
|
|
|
if entity["coverid"]: |
|
|
|
|
moreargs.update(coverArt=entity["coverid"]) |
|
|
|
|
# duration="230" size="8409237" suffix="mp3" track="2" year="2005"/> |
|
|
|
|
response.add_child("child", _parent="directory", |
|
|
|
|
size="4096", |
|
|
|
|
type="music", |
|
|
|
|
**moreargs) |
|
|
|
|
|
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
|
def render_node(self, item, item_meta, directory, dir_meta): |
|
|
|
|
""" |
|
|
|
|
Given a node and it's parent directory, and meta, return a dict with the keys formatted how the subsonic clients |
|
|
|
|
expect them to be |
|
|
|
|
:param item: |
|
|
|
|
:param item_meta: |
|
|
|
|
:param directory: |
|
|
|
|
:param dir_meta: |
|
|
|
|
""" |
|
|
|
|
child = dict(id=item["id"], |
|
|
|
|
parent=item["id"], |
|
|
|
|
isDir="true" if item['isdir'] else "false", |
|
|
|
|
title=item_meta.get("id3_title", item["name"]), |
|
|
|
|
album=item_meta.get("id3_album", item["album"]), |
|
|
|
|
artist=item_meta.get("id3_artist", item["artist"]), |
|
|
|
|
# playCount="5", |
|
|
|
|
# created="2016-04-25T07:31:33.000Z" |
|
|
|
|
# genre="Other", |
|
|
|
|
# path="Cosmic Gate/Sign Of The Times/03 Flatline (featuring Kyler England).mp3" |
|
|
|
|
type="music") |
|
|
|
|
if 'kbitrate' in item_meta: |
|
|
|
|
child["bitrate"] = item_meta["kbitrate"] |
|
|
|
|
if item["size"] != -1: |
|
|
|
|
child["size"] = item["size"] |
|
|
|
|
if "media_length" in item_meta: |
|
|
|
|
child["duration"] = item_meta["media_length"] |
|
|
|
|
if "albumId" in directory: |
|
|
|
|
child["albumId"] = directory["id"] |
|
|
|
|
if "artistId" in directory: |
|
|
|
|
child["artistId"] = directory["parent"] |
|
|
|
|
if "." in item["name"]: |
|
|
|
|
child["suffix"] = item["name"].split(".")[-1] |
|
|
|
|
if item["type"]: |
|
|
|
|
child["contentType"] = item["type"] |
|
|
|
|
if 'cover' in item_meta: |
|
|
|
|
child["coverArt"] = item_meta["cover"] |
|
|
|
|
elif 'cover' in dir_meta: |
|
|
|
|
child["coverArt"] = dir_meta["cover"] |
|
|
|
|
if 'track' in item_meta: |
|
|
|
|
child["track"] = item_meta['track'] |
|
|
|
|
if 'id3_year' in item_meta: |
|
|
|
|
child["year"] = item_meta['id3_year'] |
|
|
|
|
return child |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
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) |
|
|
|
|
to_bitrate = min(maxBitRate, self.options.max_bitrate, meta.get("media_kbitrate", 320)) |
|
|
|
|
song = self.library.get_song(int(id)) |
|
|
|
|
fpath = song["_fullpath"] |
|
|
|
|
media_bitrate = song.get("bitrate") / 1024 if song.get("bitrate") else 320 |
|
|
|
|
to_bitrate = min(maxBitRate, |
|
|
|
|
self.options.max_bitrate, |
|
|
|
|
media_bitrate) |
|
|
|
|
cherrypy.response.headers['Content-Type'] = 'audio/mpeg' |
|
|
|
|
if "media_length" in meta: |
|
|
|
|
cherrypy.response.headers['X-Content-Duration'] = str(int(meta['media_length'])) |
|
|
|
|
#if "media_length" in meta: |
|
|
|
|
# cherrypy.response.headers['X-Content-Duration'] = str(int(meta['media_length'])) |
|
|
|
|
cherrypy.response.headers['X-Content-Kbitrate'] = str(to_bitrate) |
|
|
|
|
if (self.options.skip_transcode or meta.get("media_kbitrate", -1) == to_bitrate) \ |
|
|
|
|
and meta["type"] == "audio/mpeg": |
|
|
|
|
if (self.options.skip_transcode or (song.get("bitrate") and media_bitrate == to_bitrate)) \ |
|
|
|
|
and song["format"] == "audio/mpeg": |
|
|
|
|
def content(): |
|
|
|
|
with open(fpath, "rb") as f: |
|
|
|
|
while True: |
|
|
|
@ -330,10 +172,9 @@ class PysonicApi(object):
|
|
|
|
|
yield data |
|
|
|
|
return content() |
|
|
|
|
else: |
|
|
|
|
transcode_meta = "transcoded_{}_size".format(to_bitrate) |
|
|
|
|
if transcode_meta in meta: |
|
|
|
|
cherrypy.response.headers['Content-Length'] = str(int(meta[transcode_meta])) |
|
|
|
|
|
|
|
|
|
# transcode_meta = "transcoded_{}_size".format(to_bitrate) |
|
|
|
|
# if transcode_meta in meta: |
|
|
|
|
# cherrypy.response.headers['Content-Length'] = str(int(meta[transcode_meta])) |
|
|
|
|
transcode_args = ["ffmpeg", "-i", fpath, "-map", "0:0", "-b:a", |
|
|
|
|
"{}k".format(to_bitrate), |
|
|
|
|
"-v", "0", "-f", "mp3", "-"] |
|
|
|
@ -343,13 +184,13 @@ class PysonicApi(object):
|
|
|
|
|
|
|
|
|
|
def content(proc): |
|
|
|
|
length = 0 |
|
|
|
|
completed = False |
|
|
|
|
# completed = False |
|
|
|
|
start = time() |
|
|
|
|
try: |
|
|
|
|
while True: |
|
|
|
|
data = proc.stdout.read(16 * 1024) |
|
|
|
|
if not data: |
|
|
|
|
completed = True |
|
|
|
|
# completed = True |
|
|
|
|
break |
|
|
|
|
yield data |
|
|
|
|
length += len(data) |
|
|
|
@ -357,8 +198,8 @@ class PysonicApi(object):
|
|
|
|
|
proc.poll() |
|
|
|
|
if proc.returncode is None or proc.returncode == 0: |
|
|
|
|
logging.warning("transcoded {} in {}s".format(id, int(time() - start))) |
|
|
|
|
if completed: |
|
|
|
|
self.library.report_transcode(id, to_bitrate, length) |
|
|
|
|
# if completed: |
|
|
|
|
# self.library.report_transcode(id, to_bitrate, length) |
|
|
|
|
else: |
|
|
|
|
logging.error("transcode of {} exited with code {} after {}s".format(id, proc.returncode, |
|
|
|
|
int(time() - start))) |
|
|
|
@ -378,7 +219,8 @@ class PysonicApi(object):
|
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def getCoverArt_view(self, id, **kwargs): |
|
|
|
|
fpath = self.library.get_filepath(id) |
|
|
|
|
cover = self.library.get_cover(id) |
|
|
|
|
fpath = cover["_fullpath"] |
|
|
|
|
type2ct = { |
|
|
|
|
'jpg': 'image/jpeg', |
|
|
|
|
'png': 'image/png', |
|
|
|
@ -397,7 +239,6 @@ class PysonicApi(object):
|
|
|
|
|
yield data |
|
|
|
|
logging.info("\nSent {} bytes for {}".format(total, fpath)) |
|
|
|
|
return content() |
|
|
|
|
|
|
|
|
|
getCoverArt_view._cp_config = {'response.stream': True} |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
@ -471,15 +312,34 @@ class PysonicApi(object):
|
|
|
|
|
""" |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("randomSongs") |
|
|
|
|
children = self.library.get_songs(size, shuffle=True) |
|
|
|
|
for item in children: |
|
|
|
|
# omit not dirs and media in browser |
|
|
|
|
if not item["isdir"] and item["type"] not in MUSIC_TYPES: |
|
|
|
|
continue |
|
|
|
|
item_meta = item['metadata'] |
|
|
|
|
itemtype = "song" if item["type"] in MUSIC_TYPES else "album" |
|
|
|
|
response.add_child(itemtype, _parent="randomSongs", |
|
|
|
|
**self.render_node(item, item_meta, {}, self.db.getnode(item["parent"])["metadata"])) |
|
|
|
|
children = self.library.db.get_songs(limit=size, sortby="random") |
|
|
|
|
for song in children: |
|
|
|
|
moreargs = {} |
|
|
|
|
if song["format"]: |
|
|
|
|
moreargs.update(contentType=song["format"]) |
|
|
|
|
if song["albumcoverid"]: |
|
|
|
|
moreargs.update(coverArt=song["albumcoverid"]) |
|
|
|
|
if song["length"]: |
|
|
|
|
moreargs.update(duration=song["length"]) |
|
|
|
|
if song["track"]: |
|
|
|
|
moreargs.update(track=song["track"]) |
|
|
|
|
if song["year"]: |
|
|
|
|
moreargs.update(year=song["year"]) |
|
|
|
|
|
|
|
|
|
file_extension = song["file"].split(".")[-1] |
|
|
|
|
|
|
|
|
|
response.add_child("song", |
|
|
|
|
_parent="randomSongs", |
|
|
|
|
title=song["title"], |
|
|
|
|
album=song["albumname"], |
|
|
|
|
artist=song["artistname"], |
|
|
|
|
id=song["id"], |
|
|
|
|
isDir="false", |
|
|
|
|
parent=song["albumid"], |
|
|
|
|
size=song["size"], |
|
|
|
|
suffix=file_extension, |
|
|
|
|
type="music", |
|
|
|
|
**moreargs) |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
@ -487,9 +347,8 @@ class PysonicApi(object):
|
|
|
|
|
def getGenres_view(self, **kwargs): |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("genres") |
|
|
|
|
response.add_child("genre", _parent="genres", value="Death Metal", songCount=420, albumCount=69) |
|
|
|
|
response.add_child("genre", _parent="genres", value="Metal", songCount=52, albumCount=3) |
|
|
|
|
response.add_child("genre", _parent="genres", value="Punk", songCount=34, albumCount=3) |
|
|
|
|
for row in self.library.db.get_genres(): |
|
|
|
|
response.add_child("genre", _parent="genres", value=row["name"], songCount=420, albumCount=69) |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
@ -500,7 +359,7 @@ class PysonicApi(object):
|
|
|
|
|
:param submission: True if end of song reached. False on start of track. |
|
|
|
|
""" |
|
|
|
|
submission = True if submission == "true" else False |
|
|
|
|
# TODO save played track stats |
|
|
|
|
# TODO save played track stats and/or do last.fm bullshit |
|
|
|
|
return ApiResponse() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
@ -548,3 +407,108 @@ class PysonicApi(object):
|
|
|
|
|
def setRating_view(self, id, rating): |
|
|
|
|
# rating is 1-5 |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
def savePlayQueue_view(self, id, current, position, **kwargs): |
|
|
|
|
print("TODO save playqueue with items {} current {} position {}".format(id, current, position)) |
|
|
|
|
# TODO save playlist with items ['378', '386', '384', '380', '383'] current 383 position 4471 |
|
|
|
|
# id entries are strings! |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def createPlaylist_view(self, name, songId, **kwargs): |
|
|
|
|
if type(songId) != list: |
|
|
|
|
songId = [songId] |
|
|
|
|
user = self.library.db.get_user(cherrypy.request.login) |
|
|
|
|
self.library.db.add_playlist(user["id"], name, songId) |
|
|
|
|
return ApiResponse() |
|
|
|
|
#TODO the response should be the new playlist, check the cap |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def getPlaylists_view(self, **kwargs): |
|
|
|
|
user = self.library.db.get_user(cherrypy.request.login) |
|
|
|
|
|
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("playlists") |
|
|
|
|
for playlist in self.library.db.get_playlists(user["id"]): |
|
|
|
|
response.add_child("playlist", |
|
|
|
|
_parent="playlists", |
|
|
|
|
id=playlist["id"], |
|
|
|
|
name=playlist["name"], |
|
|
|
|
owner=user["username"], |
|
|
|
|
public=playlist["public"], |
|
|
|
|
songCount=69, |
|
|
|
|
duration=420, |
|
|
|
|
# changed="2018-04-05T23:23:38.263Z" |
|
|
|
|
# created="2018-04-05T23:23:38.252Z" |
|
|
|
|
# coverArt="pl-1" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
return response |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def getPlaylist_view(self, id, **kwargs): |
|
|
|
|
user = self.library.db.get_user(cherrypy.request.login) |
|
|
|
|
plinfo, songs = self.library.get_playlist(int(id)) |
|
|
|
|
|
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("playlist", |
|
|
|
|
id=plinfo["id"], |
|
|
|
|
name=plinfo["name"], # TODO this element should match getPlaylists_view |
|
|
|
|
owner=user["username"], # TODO translate id to name |
|
|
|
|
public=plinfo["public"], |
|
|
|
|
songCount=69, |
|
|
|
|
duration=420) |
|
|
|
|
for song in songs: |
|
|
|
|
response.add_child("entry", |
|
|
|
|
_parent="playlist", |
|
|
|
|
id=song["id"], |
|
|
|
|
parent=song["albumid"], # albumid seems wrong? should be dir parent? |
|
|
|
|
isDir="false", |
|
|
|
|
title=song["title"], |
|
|
|
|
album=song["albumname"], |
|
|
|
|
artist=song["artistname"], |
|
|
|
|
track=song["track"], |
|
|
|
|
year=song["year"], |
|
|
|
|
genre=song["genrename"], |
|
|
|
|
coverArt=song["albumcoverid"], |
|
|
|
|
size=song["size"], |
|
|
|
|
contentType=song["format"], |
|
|
|
|
# suffix="mp3" |
|
|
|
|
duration=song["length"], |
|
|
|
|
bitRate=song["bitrate"] / 1024, |
|
|
|
|
path=song["file"], |
|
|
|
|
playCount="1", |
|
|
|
|
# created="2015-06-09T15:26:01.000Z" |
|
|
|
|
albumId=song["albumid"], |
|
|
|
|
artistId=song["artistid"], |
|
|
|
|
type="music") |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def updatePlaylist_view(self, playlistId, songIndexToRemove=None, songIdToAdd=None, **kwargs): |
|
|
|
|
user = self.library.db.get_user(cherrypy.request.login) |
|
|
|
|
plinfo, songs = self.library.get_playlist(int(playlistId)) |
|
|
|
|
|
|
|
|
|
assert plinfo["ownerid"] == user["id"] |
|
|
|
|
|
|
|
|
|
if songIndexToRemove: |
|
|
|
|
self.library.db.remove_index_from_playlist(playlistId, songIndexToRemove) |
|
|
|
|
elif songIdToAdd: |
|
|
|
|
self.library.db.add_to_playlist(playlistId, songIdToAdd) |
|
|
|
|
#TODO there are more modification methods |
|
|
|
|
|
|
|
|
|
return ApiResponse() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def deletePlaylist_view(self, id, **kwargs): |
|
|
|
|
user = self.library.db.get_user(cherrypy.request.login) |
|
|
|
|
plinfo, _ = self.library.get_playlist(int(id)) |
|
|
|
|
assert plinfo["ownerid"] == user["id"] |
|
|
|
|
|
|
|
|
|
self.library.delete_playlist(plinfo["id"]) |
|
|
|
|
return ApiResponse() |
|
|
|
|