|
|
|
@ -2,7 +2,7 @@ import logging
|
|
|
|
|
import subprocess |
|
|
|
|
from time import time |
|
|
|
|
from threading import Thread |
|
|
|
|
from pysonic.library import LETTER_GROUPS |
|
|
|
|
from pysonic.database import LETTER_GROUPS |
|
|
|
|
from pysonic.types import MUSIC_TYPES |
|
|
|
|
from pysonic.apilib import formatresponse, ApiResponse |
|
|
|
|
import cherrypy |
|
|
|
@ -11,16 +11,15 @@ logging = logging.getLogger("api")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PysonicSubsonicApi(object): |
|
|
|
|
def __init__(self, db, library, options): |
|
|
|
|
def __init__(self, db, options): |
|
|
|
|
self.db = db |
|
|
|
|
self.library = library |
|
|
|
|
self.options = options |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def index(self): |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("totals", **self.library.db.get_stats()) |
|
|
|
|
response.add_child("totals", **self.db.get_stats()) |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
@ -46,7 +45,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
def getMusicFolders_view(self, **kwargs): |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("musicFolders") |
|
|
|
|
for folder in self.library.get_libraries(): |
|
|
|
|
for folder in self.db.get_libraries(): |
|
|
|
|
response.add_child("musicFolder", _parent="musicFolders", id=folder["id"], name=folder["name"]) |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
@ -58,7 +57,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
# 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") |
|
|
|
|
artists = self.db.get_artists(sortby="name", order="asc") |
|
|
|
|
for letter in LETTER_GROUPS: |
|
|
|
|
index = response.add_child("index", _parent="indexes", name=letter.upper()) |
|
|
|
|
for artist in artists: |
|
|
|
@ -83,7 +82,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
|
|
|
|
|
qargs.update(limit=(offset, size)) |
|
|
|
|
|
|
|
|
|
albums = self.library.get_albums(**qargs) |
|
|
|
|
albums = self.db.get_albums(**qargs) |
|
|
|
|
|
|
|
|
|
response = ApiResponse() |
|
|
|
|
|
|
|
|
@ -111,7 +110,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
List either and artist or album dir |
|
|
|
|
""" |
|
|
|
|
dir_id = int(id) |
|
|
|
|
dirtype, dirinfo, entity = self.library.db.get_subsonic_musicdir(dirid=dir_id) |
|
|
|
|
dirtype, dirinfo, entity = self.db.get_subsonic_musicdir(dirid=dir_id) |
|
|
|
|
|
|
|
|
|
response = ApiResponse() |
|
|
|
|
|
|
|
|
@ -171,7 +170,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
def stream_view(self, id, maxBitRate="256", **kwargs): |
|
|
|
|
maxBitRate = int(maxBitRate) |
|
|
|
|
assert maxBitRate >= 32 and maxBitRate <= 320 |
|
|
|
|
song = self.library.get_song(int(id)) |
|
|
|
|
song = self.db.get_songs(id=int(id))[0] |
|
|
|
|
fpath = song["_fullpath"] |
|
|
|
|
media_bitrate = song.get("bitrate") / 1024 if song.get("bitrate") else 320 |
|
|
|
|
to_bitrate = min(maxBitRate, |
|
|
|
@ -219,7 +218,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
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) |
|
|
|
|
# self.db.report_transcode(id, to_bitrate, length) |
|
|
|
|
else: |
|
|
|
|
logging.error("transcode of {} exited with code {} after {}s".format(id, proc.returncode, |
|
|
|
|
int(time() - start))) |
|
|
|
@ -248,7 +247,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
""" |
|
|
|
|
if id.startswith("pl-"): # get art from first track in playlist |
|
|
|
|
playlist_id = int(id[len("pl-"):]) |
|
|
|
|
_, songs = self.library.get_playlist(playlist_id) |
|
|
|
|
songs = self.db.get_playlist_songs(playlist_id) |
|
|
|
|
for song in songs: |
|
|
|
|
if song["albumcoverid"]: |
|
|
|
|
id = song["albumcoverid"] |
|
|
|
@ -262,8 +261,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
else: |
|
|
|
|
id = int(id) |
|
|
|
|
|
|
|
|
|
cover = self.library.get_cover(id) |
|
|
|
|
fpath = cover["_fullpath"] |
|
|
|
|
fpath = self.db.get_cover_path(id) |
|
|
|
|
type2ct = { |
|
|
|
|
'jpg': 'image/jpeg', |
|
|
|
|
'png': 'image/png', |
|
|
|
@ -280,14 +278,14 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
break |
|
|
|
|
total += len(data) |
|
|
|
|
yield data |
|
|
|
|
logging.info("\nSent {} bytes for {}".format(total, fpath)) |
|
|
|
|
logging.info("sent {} bytes for {}".format(total, fpath)) |
|
|
|
|
return content() |
|
|
|
|
getCoverArt_view._cp_config = {'response.stream': True} |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def getArtistInfo_view(self, id, includeNotPresent="true", **kwargs): |
|
|
|
|
info = self.library.get_artist_info(id) |
|
|
|
|
info = self.db.get_artist_info(id) |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("artistInfo") |
|
|
|
|
response.set_attrs("artistInfo", **info) |
|
|
|
@ -296,7 +294,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def getUser_view(self, username, **kwargs): |
|
|
|
|
user = {} if self.options.disable_auth else self.library.db.get_user(cherrypy.request.login) |
|
|
|
|
user = {} if self.options.disable_auth else self.db.get_user(cherrypy.request.login) |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("user", |
|
|
|
|
username=user["username"], |
|
|
|
@ -321,19 +319,19 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def star_view(self, id, **kwargs): |
|
|
|
|
self.library.set_starred(cherrypy.request.login, int(id), starred=True) |
|
|
|
|
self.db.set_starred(cherrypy.request.login, int(id), starred=True) |
|
|
|
|
return ApiResponse() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def unstar_view(self, id, **kwargs): |
|
|
|
|
self.library.set_starred(cherrypy.request.login, int(id), starred=False) |
|
|
|
|
self.db.set_starred(cherrypy.request.login, int(id), starred=False) |
|
|
|
|
return ApiResponse() |
|
|
|
|
|
|
|
|
|
@cherrypy.expose |
|
|
|
|
@formatresponse |
|
|
|
|
def getStarred_view(self, **kwargs): |
|
|
|
|
children = self.library.get_starred(cherrypy.request.login) |
|
|
|
|
children = self.db.get_starred(cherrypy.request.login) |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("starred") |
|
|
|
|
for item in children: |
|
|
|
@ -355,7 +353,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
""" |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("randomSongs") |
|
|
|
|
children = self.library.db.get_songs(limit=size, sortby="random") |
|
|
|
|
children = self.db.get_songs(limit=size, sortby="random") |
|
|
|
|
for song in children: |
|
|
|
|
moreargs = {} |
|
|
|
|
if song["format"]: |
|
|
|
@ -390,7 +388,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
def getGenres_view(self, **kwargs): |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("genres") |
|
|
|
|
for row in self.library.db.get_genres(): |
|
|
|
|
for row in self.db.get_genres(): |
|
|
|
|
response.add_child("genre", _parent="genres", value=row["name"], songCount=420, albumCount=69) |
|
|
|
|
return response |
|
|
|
|
|
|
|
|
@ -418,7 +416,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
query = query.replace("*", "") # TODO handle this |
|
|
|
|
|
|
|
|
|
artists = 0 |
|
|
|
|
for item in self.library.get_artists(name_contains=query): |
|
|
|
|
for item in self.db.get_artists(name_contains=query): |
|
|
|
|
response.add_child("artist", _parent="searchResult2", id=item["id"], name=item["name"]) |
|
|
|
|
artists += 1 |
|
|
|
|
if artists >= artistCount: |
|
|
|
@ -426,7 +424,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
|
|
|
|
|
# TODO make this more efficient |
|
|
|
|
albums = 0 |
|
|
|
|
for album in self.library.get_albums(name_contains=query): |
|
|
|
|
for album in self.db.get_albums(name_contains=query): |
|
|
|
|
response.add_child("album", _parent="searchResult2", |
|
|
|
|
id=album["dir"], |
|
|
|
|
parent=album["artistdir"], |
|
|
|
@ -445,7 +443,7 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
|
|
|
|
|
# TODO make this more efficient |
|
|
|
|
songs = 0 |
|
|
|
|
for song in self.library.db.get_songs(title_contains=query): |
|
|
|
|
for song in self.db.get_songs(title_contains=query): |
|
|
|
|
response.add_child("song", _parent="searchResult2", |
|
|
|
|
id=song["id"], |
|
|
|
|
parent=song["albumdir"], |
|
|
|
@ -485,11 +483,11 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
def savePlayQueue_view(self, id, current, position, **kwargs): |
|
|
|
|
print("TODO save playqueue with items {} current {} position {}".format(id, repr(current), repr(position))) |
|
|
|
|
current = int(current) |
|
|
|
|
song = self.library.get_song(current) |
|
|
|
|
self.library.db.update_album_played(song['albumid'], time()) |
|
|
|
|
self.library.db.increment_album_plays(song['albumid']) |
|
|
|
|
song = self.db.get_songs(id=current)[0] |
|
|
|
|
self.db.update_album_played(song['albumid'], time()) |
|
|
|
|
self.db.increment_album_plays(song['albumid']) |
|
|
|
|
if int(position) == 0: |
|
|
|
|
self.library.db.increment_track_plays(current) |
|
|
|
|
self.db.increment_track_plays(current) |
|
|
|
|
# TODO save playlist with items ['378', '386', '384', '380', '383'] current 383 position 4471 |
|
|
|
|
# id entries are strings! |
|
|
|
|
|
|
|
|
@ -498,19 +496,19 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
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) |
|
|
|
|
user = self.db.get_user(cherrypy.request.login) |
|
|
|
|
self.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) |
|
|
|
|
user = self.db.get_user(cherrypy.request.login) |
|
|
|
|
|
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("playlists") |
|
|
|
|
for playlist in self.library.db.get_playlists(user["id"]): |
|
|
|
|
for playlist in self.db.get_playlists(user["id"]): |
|
|
|
|
response.add_child("playlist", |
|
|
|
|
_parent="playlists", |
|
|
|
|
id=playlist["id"], |
|
|
|
@ -529,9 +527,10 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
@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)) |
|
|
|
|
|
|
|
|
|
id = int(id) |
|
|
|
|
user = self.db.get_user(cherrypy.request.login) |
|
|
|
|
plinfo = self.db.get_playlist(id) |
|
|
|
|
songs = self.db.get_playlist_songs(id) |
|
|
|
|
response = ApiResponse() |
|
|
|
|
response.add_child("playlist", |
|
|
|
|
id=plinfo["id"], |
|
|
|
@ -569,15 +568,16 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
@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)) |
|
|
|
|
playlistId = int(playlistId) |
|
|
|
|
user = self.db.get_user(cherrypy.request.login) |
|
|
|
|
plinfo = self.db.get_playlist(playlistId) |
|
|
|
|
|
|
|
|
|
assert plinfo["ownerid"] == user["id"] |
|
|
|
|
|
|
|
|
|
if songIndexToRemove: |
|
|
|
|
self.library.db.remove_index_from_playlist(playlistId, songIndexToRemove) |
|
|
|
|
self.db.remove_index_from_playlist(playlistId, songIndexToRemove) |
|
|
|
|
elif songIdToAdd: |
|
|
|
|
self.library.db.add_to_playlist(playlistId, songIdToAdd) |
|
|
|
|
self.db.add_to_playlist(playlistId, songIdToAdd) |
|
|
|
|
#TODO there are more modification methods |
|
|
|
|
|
|
|
|
|
return ApiResponse() |
|
|
|
@ -585,9 +585,9 @@ class PysonicSubsonicApi(object):
|
|
|
|
|
@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)) |
|
|
|
|
user = self.db.get_user(cherrypy.request.login) |
|
|
|
|
plinfo = self.db.get_playlist(int(id)) |
|
|
|
|
assert plinfo["ownerid"] == user["id"] |
|
|
|
|
|
|
|
|
|
self.library.delete_playlist(plinfo["id"]) |
|
|
|
|
self.db.delete_playlist(plinfo["id"]) |
|
|
|
|
return ApiResponse() |
|
|
|
|