pysonic/pysonic/api.py

609 lines
24 KiB
Python
Raw Normal View History

2017-08-20 14:54:13 -07:00
import re
2017-08-19 23:01:47 -07:00
import json
2017-08-13 21:13:46 -07:00
import logging
2017-08-13 22:08:40 -07:00
import subprocess
from time import time
2017-08-13 23:54:37 -07:00
from random import shuffle
2017-08-16 21:36:15 -07:00
from threading import Thread
2017-08-19 23:01:47 -07:00
import cherrypy
from collections import defaultdict
2017-08-13 18:56:13 -07:00
from bs4 import BeautifulSoup
from pysonic.library import LETTER_GROUPS
2017-08-13 22:08:40 -07:00
from pysonic.types import MUSIC_TYPES
2017-08-13 18:56:13 -07:00
2017-08-20 14:54:13 -07:00
CALLBACK_RE = re.compile(r'^[a-zA-Z0-9_]+$')
2017-08-13 21:13:46 -07:00
logging = logging.getLogger("api")
2017-08-13 18:56:13 -07:00
2017-08-20 14:54:13 -07:00
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
2017-08-19 23:01:47 -07:00
class ApiResponse(object):
2017-08-20 14:54:13 -07:00
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:
"""
2017-08-19 23:01:47 -07:00
self.status = status
self.version = version
2017-08-20 14:54:13 -07:00
self.data = defaultdict(lambda: list())
def add_child(self, _type, _parent="", _real_parent=None, **kwargs):
2018-04-04 16:44:21 -07:00
kwargs = {k: v for k, v in kwargs.items() if v or type(v) is int} # filter out empty keys (0 is ok)
2017-08-20 14:54:13 -07:00
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)
2017-08-19 23:01:47 -07:00
def render_json(self):
2017-08-20 14:54:13 -07:00
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())
2017-08-19 23:01:47 -07:00
def render_xml(self):
2017-08-20 14:54:13 -07:00
text_attrs = ['largeImageUrl', 'musicBrainzId', 'smallImageUrl', 'mediumImageUrl', 'lastFmUrl', 'biography',
'folder']
2017-08-20 15:57:45 -07:00
selftext_attrs = ['value']
2017-08-20 14:54:13 -07:00
# These attributes will be placed in <hello>{{ value }}</hello> tags instead of hello="{{ value }}" on parent
2017-08-19 23:01:47 -07:00
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)
2017-08-20 14:54:13 -07:00
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))
2017-08-20 15:57:45 -07:00
elif key in selftext_attrs:
parent.append(str(value))
2017-08-20 14:54:13 -07:00
else:
parent.attrs[key] = value
_render_xml(self.data, root)
2017-08-19 23:01:47 -07:00
return doc.prettify()
2017-08-13 18:56:13 -07:00
class PysonicApi(object):
2017-08-13 22:08:40 -07:00
def __init__(self, db, library, options):
2017-08-13 18:56:13 -07:00
self.db = db
self.library = library
2017-08-13 22:08:40 -07:00
self.options = options
2017-08-13 18:56:13 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-13 18:56:13 -07:00
def ping_view(self, **kwargs):
# Called when the app hits the "test connection" server option
2017-08-20 14:54:13 -07:00
return ApiResponse()
2017-08-13 18:56:13 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-13 18:56:13 -07:00
def getLicense_view(self, **kwargs):
# Called after ping.view
2017-08-20 14:54:13 -07:00
response = ApiResponse()
response.add_child("license",
valid="true",
email="admin@localhost",
licenseExpires="2100-01-01T00:00:00.000Z",
trialExpires="2100-01-01T01:01:00.000Z")
return response
2017-08-13 18:56:13 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-13 18:56:13 -07:00
def getMusicFolders_view(self, **kwargs):
2017-08-20 14:54:13 -07:00
response = ApiResponse()
response.add_child("musicFolders")
2017-08-13 18:56:13 -07:00
for folder in self.library.get_libraries():
2017-08-20 14:54:13 -07:00
response.add_child("musicFolder", _parent="musicFolders", id=folder["id"], name=folder["name"])
return response
2017-08-13 18:56:13 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-13 18:56:13 -07:00
def getIndexes_view(self, **kwargs):
# Get listing of top-level dir
2017-08-20 14:54:13 -07:00
response = ApiResponse()
2018-04-03 21:04:55 -07:00
# TODO real lastmodified date
# TODO deal with ignoredArticles
2017-08-20 14:54:13 -07:00
response.add_child("indexes", lastModified="1502310831000", ignoredArticles="The El La Los Las Le Les")
2018-04-03 21:04:55 -07:00
artists = self.library.get_artists(sortby="name", order="asc")
2017-08-13 18:56:13 -07:00
for letter in LETTER_GROUPS:
2017-08-20 14:54:13 -07:00
index = response.add_child("index", _parent="indexes", name=letter.upper())
2018-04-03 21:04:55 -07:00
for artist in artists:
2017-08-13 21:13:46 -07:00
if artist["name"][0].lower() in letter:
2018-04-03 23:33:43 -07:00
response.add_child("artist", _real_parent=index, id=artist["dir"], name=artist["name"])
2017-08-20 14:54:13 -07:00
return response
2017-08-13 18:56:13 -07:00
2017-08-13 23:54:37 -07:00
@cherrypy.expose
def savePlayQueue_view(self, id, current, position, **kwargs):
print("TODO save playlist with items {} current {} position {}".format(id, current, position))
@cherrypy.expose
2017-08-19 23:01:47 -07:00
@formatresponse
2017-08-13 23:54:37 -07:00
def getAlbumList_view(self, type, size=50, offset=0, **kwargs):
albums = self.library.get_albums()
if type == "random":
shuffle(albums)
elif type == "alphabeticalByName":
albums.sort(key=lambda item: item.get("id3_album", item["album"] if item["album"] else "zzzzzUnsortable"))
2017-08-13 23:54:37 -07:00
else:
raise NotImplemented()
albumset = albums[0 + int(offset):int(size) + int(offset)]
2017-08-20 14:54:13 -07:00
response = ApiResponse()
response.add_child("albumList")
2017-08-13 23:54:37 -07:00
for album in albumset:
2017-08-16 00:05:26 -07:00
album_meta = album['metadata']
2017-08-19 23:01:47 -07:00
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"]),
# playCount="0"
# created="2016-05-08T05:31:31.000Z"/>)
)
2017-08-13 23:54:37 -07:00
if 'cover' in album_meta:
2017-08-19 23:01:47 -07:00
album_kw["coverArt"] = album_meta["cover"]
2017-08-13 23:54:37 -07:00
if 'id3_year' in album_meta:
2017-08-19 23:01:47 -07:00
album_kw["year"] = album_meta['id3_year']
2017-08-20 14:54:13 -07:00
response.add_child("album", _parent="albumList", **album_kw)
2017-08-19 23:01:47 -07:00
return response
2017-08-13 23:54:37 -07:00
2017-08-13 18:56:13 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-13 18:56:13 -07:00
def getMusicDirectory_view(self, id, **kwargs):
"""
List an artist dir
"""
2018-04-03 23:33:43 -07:00
dir_id = int(id)
2017-08-13 18:56:13 -07:00
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
2017-08-20 14:54:13 -07:00
response = ApiResponse()
response.add_child("directory")
2017-08-13 18:56:13 -07:00
2018-04-03 23:33:43 -07:00
dirtype, dirinfo, entity = self.library.db.get_musicdir(dirid=dir_id)
2017-08-13 18:56:13 -07:00
2018-04-03 23:33:43 -07:00
from pprint import pprint
pprint(dirinfo)
pprint(entity)
response.set_attrs(_path="directory", name=entity['name'], id=entity['id'],
parent=dirinfo['parent'], playCount=420)
for childtype, child in entity["children"]:
2017-08-13 22:08:40 -07:00
# omit not dirs and media in browser
2018-04-03 21:04:55 -07:00
# if not item["isdir"] and item["type"] not in MUSIC_TYPES:
# continue
# item_meta = item['metadata']
2018-04-03 23:33:43 -07:00
moreargs = {}
if childtype == "album":
moreargs.update(name=child["name"],
isDir="true", # TODO song files in artist dir
parent=entity["id"],
id=child["dir"])
2018-04-03 23:50:04 -07:00
if child["coverid"]:
moreargs.update(coverArt=child["coverid"])
2018-04-03 23:33:43 -07:00
# 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"],
2018-04-03 23:50:04 -07:00
id=child["id"],
2018-04-03 23:33:43 -07:00
duration=child["length"],
isDir="false",
parent=entity["dir"],
# title=xxx
)
2018-04-03 23:50:04 -07:00
if entity["coverid"]:
moreargs.update(coverArt=entity["coverid"])
2018-04-03 23:33:43 -07:00
# duration="230" size="8409237" suffix="mp3" track="2" year="2005"/>
2018-04-03 21:04:55 -07:00
response.add_child("child", _parent="directory",
size="4096",
2018-04-03 23:33:43 -07:00
type="music",
**moreargs)
2017-08-13 18:56:13 -07:00
2017-08-20 14:54:13 -07:00
return response
2017-08-20 15:57:45 -07:00
def render_node(self, item, item_meta, directory, dir_meta):
2017-08-20 14:54:13 -07:00
"""
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:
"""
2018-04-03 23:33:43 -07:00
raise Exception("stop using this")
2017-08-20 14:54:13 -07:00
child = dict(id=item["id"],
parent=item["id"],
2018-04-03 21:04:55 -07:00
isDir="true" if "file" not in item else "false",
2017-08-20 14:54:13 -07:00
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"]
2017-08-19 22:03:09 -07:00
if item["size"] != -1:
2017-08-20 14:54:13 -07:00
child["size"] = item["size"]
2017-08-19 22:03:09 -07:00
if "media_length" in item_meta:
2017-08-20 14:54:13 -07:00
child["duration"] = item_meta["media_length"]
2017-08-15 22:16:48 -07:00
if "albumId" in directory:
2017-08-20 14:54:13 -07:00
child["albumId"] = directory["id"]
2017-08-15 22:16:48 -07:00
if "artistId" in directory:
2017-08-20 14:54:13 -07:00
child["artistId"] = directory["parent"]
2017-08-15 22:16:48 -07:00
if "." in item["name"]:
2017-08-20 14:54:13 -07:00
child["suffix"] = item["name"].split(".")[-1]
2017-08-15 22:16:48 -07:00
if item["type"]:
2017-08-20 14:54:13 -07:00
child["contentType"] = item["type"]
2017-08-15 22:16:48 -07:00
if 'cover' in item_meta:
2017-08-20 14:54:13 -07:00
child["coverArt"] = item_meta["cover"]
2017-08-15 22:16:48 -07:00
elif 'cover' in dir_meta:
2017-08-20 14:54:13 -07:00
child["coverArt"] = dir_meta["cover"]
2017-08-15 22:16:48 -07:00
if 'track' in item_meta:
2017-08-20 14:54:13 -07:00
child["track"] = item_meta['track']
2017-08-15 22:16:48 -07:00
if 'id3_year' in item_meta:
2017-08-20 14:54:13 -07:00
child["year"] = item_meta['id3_year']
2017-08-15 22:16:48 -07:00
return child
2017-08-13 18:56:13 -07:00
@cherrypy.expose
2017-08-13 22:08:40 -07:00
def stream_view(self, id, maxBitRate="256", **kwargs):
maxBitRate = int(maxBitRate)
assert maxBitRate >= 32 and maxBitRate <= 320
2018-04-03 23:33:43 -07:00
song = self.library.get_song(id)
2018-04-03 23:50:04 -07:00
fpath = song["_fullpath"]
2018-04-04 16:44:21 -07:00
media_bitrate = song.get("bitrate") / 1024 if song.get("bitrate") else 320
to_bitrate = min(maxBitRate,
self.options.max_bitrate,
media_bitrate)
2017-08-13 18:56:13 -07:00
cherrypy.response.headers['Content-Type'] = 'audio/mpeg'
2018-04-03 23:33:43 -07:00
#if "media_length" in meta:
# cherrypy.response.headers['X-Content-Duration'] = str(int(meta['media_length']))
2017-08-19 22:03:09 -07:00
cherrypy.response.headers['X-Content-Kbitrate'] = str(to_bitrate)
2018-04-04 16:44:21 -07:00
if (self.options.skip_transcode or (song.get("bitrate") and media_bitrate == to_bitrate)) \
and song["format"] == "audio/mpeg":
2017-08-13 22:08:40 -07:00
def content():
with open(fpath, "rb") as f:
while True:
data = f.read(16 * 1024)
if not data:
break
yield data
2017-08-19 22:03:09 -07:00
return content()
2017-08-13 22:08:40 -07:00
else:
2018-04-03 23:33:43 -07:00
# transcode_meta = "transcoded_{}_size".format(to_bitrate)
# if transcode_meta in meta:
# cherrypy.response.headers['Content-Length'] = str(int(meta[transcode_meta]))
print(fpath)
2017-08-16 21:36:15 -07:00
transcode_args = ["ffmpeg", "-i", fpath, "-map", "0:0", "-b:a",
2017-08-19 22:03:09 -07:00
"{}k".format(to_bitrate),
2017-08-16 21:36:15 -07:00
"-v", "0", "-f", "mp3", "-"]
logging.info(' '.join(transcode_args))
proc = subprocess.Popen(transcode_args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def content(proc):
2017-08-19 22:03:09 -07:00
length = 0
2018-04-03 23:33:43 -07:00
# completed = False
2017-08-13 22:08:40 -07:00
start = time()
2017-08-16 21:36:15 -07:00
try:
while True:
data = proc.stdout.read(16 * 1024)
if not data:
2018-04-03 23:33:43 -07:00
# completed = True
2017-08-16 21:36:15 -07:00
break
yield data
2017-08-19 22:03:09 -07:00
length += len(data)
2017-08-16 21:36:15 -07:00
finally:
proc.poll()
2017-08-19 22:03:09 -07:00
if proc.returncode is None or proc.returncode == 0:
2017-08-16 21:36:15 -07:00
logging.warning("transcoded {} in {}s".format(id, int(time() - start)))
2018-04-03 23:33:43 -07:00
# if completed:
# self.library.report_transcode(id, to_bitrate, length)
2017-08-16 21:36:15 -07:00
else:
logging.error("transcode of {} exited with code {} after {}s".format(id, proc.returncode,
int(time() - start)))
def stopit(proc):
try:
proc.wait(timeout=90)
except subprocess.TimeoutExpired:
logging.warning("killing timed-out transcoder")
proc.kill()
proc.wait()
Thread(target=stopit, args=(proc, )).start()
2017-08-19 22:03:09 -07:00
return content(proc)
2017-08-13 18:56:13 -07:00
stream_view._cp_config = {'response.stream': True}
@cherrypy.expose
def getCoverArt_view(self, id, **kwargs):
2018-04-03 23:33:43 -07:00
cover = self.library.get_cover(id)
2018-04-03 23:50:04 -07:00
fpath = cover["_fullpath"]
2017-08-13 21:13:46 -07:00
type2ct = {
'jpg': 'image/jpeg',
2017-08-20 14:54:13 -07:00
'png': 'image/png',
'gif': 'image/gif'
2017-08-13 21:13:46 -07:00
}
cherrypy.response.headers['Content-Type'] = type2ct[fpath[-3:]]
2017-08-13 18:56:13 -07:00
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
2017-08-13 21:13:46 -07:00
logging.info("\nSent {} bytes for {}".format(total, fpath))
2017-08-13 18:56:13 -07:00
return content()
getCoverArt_view._cp_config = {'response.stream': True}
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-13 18:56:13 -07:00
def getArtistInfo_view(self, id, includeNotPresent="true", **kwargs):
info = self.library.get_artist_info(id)
2017-08-20 14:54:13 -07:00
response = ApiResponse()
response.add_child("artistInfo")
response.set_attrs("artistInfo", **info)
return response
2017-08-13 18:56:13 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-20 15:57:45 -07:00
def getUser_view(self, username, **kwargs):
2017-08-15 21:40:38 -07:00
user = {} if self.options.disable_auth else self.library.db.get_user(cherrypy.request.login)
2017-08-20 14:54:13 -07:00
response = ApiResponse()
response.add_child("user",
username=user["username"],
email=user["email"],
scrobblingEnabled="false",
adminRole="true" if user["admin"] else "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",
folder=0)
return response
2017-08-15 21:40:38 -07:00
2017-08-15 21:41:02 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-15 21:41:02 -07:00
def star_view(self, id, **kwargs):
self.library.set_starred(cherrypy.request.login, int(id), starred=True)
2017-08-20 14:54:13 -07:00
return ApiResponse()
2017-08-15 21:41:02 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-15 21:41:02 -07:00
def unstar_view(self, id, **kwargs):
self.library.set_starred(cherrypy.request.login, int(id), starred=False)
2017-08-20 14:54:13 -07:00
return ApiResponse()
2017-08-15 22:16:48 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-15 22:16:48 -07:00
def getStarred_view(self, **kwargs):
2017-08-16 00:05:26 -07:00
children = self.library.get_starred(cherrypy.request.login)
2017-08-20 14:54:13 -07:00
response = ApiResponse()
response.add_child("starred")
2017-08-16 00:05:26 -07:00
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"
2017-08-20 15:57:45 -07:00
response.add_child(itemtype, _parent="starred", **self.render_node(item, item_meta, {}, {}))
2017-08-20 14:54:13 -07:00
return response
2017-08-15 22:16:48 -07:00
2017-08-16 00:05:26 -07:00
@cherrypy.expose
2017-08-20 14:54:13 -07:00
@formatresponse
2017-08-16 00:05:26 -07:00
def getRandomSongs_view(self, size=50, genre=None, fromYear=0, toYear=0, **kwargs):
2017-08-20 15:57:45 -07:00
"""
Get a playlist of random songs
:param genre: genre name to find songs under
:type genre: str
"""
2017-08-20 14:54:13 -07:00
response = ApiResponse()
response.add_child("randomSongs")
2018-04-04 16:44:21 -07:00
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)
2017-08-20 15:57:45 -07:00
return response
@cherrypy.expose
@formatresponse
def getGenres_view(self, **kwargs):
response = ApiResponse()
response.add_child("genres")
2018-04-04 16:44:05 -07:00
for row in self.library.db.get_genres():
response.add_child("genre", _parent="genres", value=row["name"], songCount=420, albumCount=69)
2017-08-20 14:54:13 -07:00
return response
2017-08-20 15:57:45 -07:00
@cherrypy.expose
@formatresponse
def scrobble_view(self, id, submission, **kwargs):
"""
:param id: song id being played
: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
return ApiResponse()
2017-08-20 16:09:17 -07:00
@cherrypy.expose
@formatresponse
def search2_view(self, query, artistCount, albumCount, songCount, **kwargs):
response = ApiResponse()
response.add_child("searchResult2")
artistCount = int(artistCount)
albumCount = int(albumCount)
songCount = int(songCount)
query = query.replace("*", "") # TODO handle this
artists = 0
for item in self.library.get_artists():
if query in item["name"].lower():
response.add_child("artist", _parent="searchResult2", id=item["id"], name=item["name"])
artists += 1
if artists >= artistCount:
break
# TODO make this more efficient
albums = 0
for item in self.library.get_artists():
if query in item["name"].lower():
response.add_child("album", _parent="searchResult2", **self.render_node(item, item["metadata"], {}, {}))
albums += 1
if albums >= albumCount:
break
# TODO make this more efficient
songs = 0
for item in self.library.get_songs(limit=9999999, shuffle=False):
if query in item["name"].lower():
response.add_child("song", _parent="searchResult2", **self.render_node(item, item["metadata"], {}, {}))
songs += 1
if songs > songCount:
break
return response
2017-08-20 16:12:11 -07:00
@cherrypy.expose
@formatresponse
def setRating_view(self, id, rating):
# rating is 1-5
pass