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-16 21:36:15 -07:00
|
|
|
from threading import Thread
|
2017-08-13 18:56:13 -07:00
|
|
|
from pysonic.library import LETTER_GROUPS
|
2017-08-13 22:08:40 -07:00
|
|
|
from pysonic.types import MUSIC_TYPES
|
2018-04-05 19:02:17 -07:00
|
|
|
from pysonic.apilib import formatresponse, ApiResponse
|
|
|
|
import cherrypy
|
2017-08-13 22:08:40 -07:00
|
|
|
|
2017-08-13 21:13:46 -07:00
|
|
|
logging = logging.getLogger("api")
|
|
|
|
|
2017-08-13 18:56:13 -07:00
|
|
|
|
2018-04-07 15:04:41 -07:00
|
|
|
class PysonicSubsonicApi(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
|
|
|
|
2018-04-07 15:04:41 -07:00
|
|
|
@cherrypy.expose
|
2018-04-07 15:24:02 -07:00
|
|
|
@formatresponse
|
2018-04-07 15:04:41 -07:00
|
|
|
def index(self):
|
2018-04-07 15:24:02 -07:00
|
|
|
response = ApiResponse()
|
|
|
|
response.add_child("totals", **self.library.db.get_stats())
|
|
|
|
return response
|
2018-04-07 15:04:41 -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 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-05 19:02:17 -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-05 19:02:17 -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-05 19:02:17 -07:00
|
|
|
for artist in artists:
|
2017-08-13 21:13:46 -07:00
|
|
|
if artist["name"][0].lower() in letter:
|
2018-04-05 19:02:17 -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
|
2017-08-19 23:01:47 -07:00
|
|
|
@formatresponse
|
2018-04-07 15:04:41 -07:00
|
|
|
def getAlbumList_view(self, type, size=250, offset=0, **kwargs):
|
2018-04-05 19:02:17 -07:00
|
|
|
qargs = {}
|
2017-08-13 23:54:37 -07:00
|
|
|
if type == "random":
|
2018-04-05 19:02:17 -07:00
|
|
|
qargs.update(sortby="random")
|
2017-08-13 23:54:37 -07:00
|
|
|
elif type == "alphabeticalByName":
|
2018-04-05 19:02:17 -07:00
|
|
|
qargs.update(sortby="name", order="asc")
|
|
|
|
elif type == "newest":
|
|
|
|
qargs.update(sortby="added", order="desc")
|
2018-04-07 15:24:02 -07:00
|
|
|
elif type == "recent":
|
|
|
|
qargs.update(sortby="played", order="desc")
|
|
|
|
elif type == "frequent":
|
|
|
|
qargs.update(sortby="plays", order="desc")
|
2018-04-05 19:02:17 -07:00
|
|
|
|
|
|
|
qargs.update(limit=(offset, size))
|
|
|
|
|
|
|
|
albums = self.library.get_albums(**qargs)
|
2017-08-13 23:54:37 -07:00
|
|
|
|
2017-08-20 14:54:13 -07:00
|
|
|
response = ApiResponse()
|
|
|
|
|
|
|
|
response.add_child("albumList")
|
2017-08-13 23:54:37 -07:00
|
|
|
|
2018-04-05 19:02:17 -07:00
|
|
|
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
|
2017-08-19 23:01:47 -07:00
|
|
|
# playCount="0"
|
|
|
|
# created="2016-05-08T05:31:31.000Z"/>)
|
|
|
|
)
|
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
|
|
|
|
"""
|
|
|
|
dir_id = int(id)
|
2018-04-05 19:02:17 -07:00
|
|
|
dirtype, dirinfo, entity = self.library.db.get_subsonic_musicdir(dirid=dir_id)
|
2017-08-13 18:56:13 -07:00
|
|
|
|
2017-08-20 14:54:13 -07:00
|
|
|
response = ApiResponse()
|
|
|
|
response.add_child("directory")
|
2018-04-05 19:02:17 -07:00
|
|
|
response.set_attrs(_path="directory", name=entity['name'], id=entity['id'],
|
|
|
|
parent=dirinfo['parent'], playCount=420)
|
2017-08-13 18:56:13 -07:00
|
|
|
|
2018-04-05 19:02:17 -07:00
|
|
|
for childtype, child in entity["children"]:
|
2017-08-13 22:08:40 -07:00
|
|
|
# omit not dirs and media in browser
|
2018-04-05 19:02:17 -07:00
|
|
|
# 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)
|
2017-08-13 18:56:13 -07:00
|
|
|
|
2018-04-05 19:02:17 -07:00
|
|
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
2017-08-20 14:54:13 -07:00
|
|
|
return response
|
|
|
|
|
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-05 19:02:17 -07:00
|
|
|
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)
|
2017-08-13 18:56:13 -07:00
|
|
|
cherrypy.response.headers['Content-Type'] = 'audio/mpeg'
|
2018-04-05 19:02:17 -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-05 19:02:17 -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-05 19:02:17 -07:00
|
|
|
# transcode_meta = "transcoded_{}_size".format(to_bitrate)
|
|
|
|
# if transcode_meta in meta:
|
|
|
|
# cherrypy.response.headers['Content-Length'] = str(int(meta[transcode_meta]))
|
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-05 19:02:17 -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-05 19:02:17 -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-05 19:02:17 -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-05 19:02:17 -07:00
|
|
|
cover = self.library.get_cover(id)
|
|
|
|
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",
|
2018-04-07 16:26:27 -07:00
|
|
|
podcastRole="true",
|
2017-08-20 14:54:13 -07:00
|
|
|
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-05 19:02:17 -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-05 19:02:17 -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
|
2018-04-05 19:02:17 -07:00
|
|
|
# TODO save played track stats and/or do last.fm bullshit
|
2017-08-20 15:57:45 -07:00
|
|
|
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
|
2018-04-05 19:02:17 -07:00
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
def savePlayQueue_view(self, id, current, position, **kwargs):
|
|
|
|
print("TODO save playqueue with items {} current {} position {}".format(id, current, position))
|
2018-04-07 15:24:02 -07:00
|
|
|
|
|
|
|
song = self.library.get_song(int(current))
|
|
|
|
self.library.db.update_album_played(song['albumid'], time())
|
|
|
|
self.library.db.increment_album_plays(song['albumid'])
|
2018-04-05 19:02:17 -07:00
|
|
|
# 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()
|
2018-04-07 16:26:27 -07:00
|
|
|
|
|
|
|
#
|
|
|
|
#
|
|
|
|
#
|
|
|
|
#
|
|
|
|
# Podcast related endpoints
|
|
|
|
@cherrypy.expose
|
|
|
|
@formatresponse
|
|
|
|
def getPodcasts_view(self, includeEpisodes=False, **kwargs):
|
|
|
|
#TODO implement includeEpisodes
|
|
|
|
response = ApiResponse()
|
|
|
|
response.add_child("podcasts")
|
|
|
|
for podcast in self.library.get_podcasts():
|
|
|
|
response.add_child("channel",
|
|
|
|
_parent="podcasts",
|
|
|
|
id=podcast["id"],
|
|
|
|
title=podcast["title"],
|
|
|
|
url=podcast["url"],
|
|
|
|
description=podcast["description"],
|
|
|
|
# coverArt="pl-1"
|
|
|
|
# originalImageUrl="",
|
|
|
|
status="completed" # or "downloading"
|
|
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
@formatresponse
|
|
|
|
def createPodcastChannel_view(self, url, **kwargs):
|
|
|
|
self.library.db.add_postcast(url)
|
|
|
|
return ApiResponse()
|
|
|
|
|
|
|
|
@cherrypy.expose
|
|
|
|
@formatresponse
|
|
|
|
def refreshPodcasts_view(self, **kwargs):
|
|
|
|
return ApiResponse()
|