mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 08:56:17 +00:00
Implement musicFolderId paramenter on various endpoints
Bumped API version to 1.12.0 along the way. `getArtits` also got it event if it seems it has been added with version 1.14.0, but I'm a bit concerned as to how clients will behave on authentication if the server advertise itself as 1.13.0+ Closes #235 Ref #74
This commit is contained in:
parent
bc373e595f
commit
e52a7043b0
@ -16,7 +16,7 @@ Current supported features are:
|
|||||||
* [Last.fm][lastfm] scrobbling
|
* [Last.fm][lastfm] scrobbling
|
||||||
* Jukebox mode
|
* Jukebox mode
|
||||||
|
|
||||||
Supysonic currently targets the version 1.10.2 of the Subsonic API. For more
|
Supysonic currently targets the version 1.12.0 of the Subsonic API. For more
|
||||||
details, go check the [API implementation status][docs-api].
|
details, go check the [API implementation status][docs-api].
|
||||||
|
|
||||||
[subsonic]: http://www.subsonic.org/
|
[subsonic]: http://www.subsonic.org/
|
||||||
|
18
docs/api.rst
18
docs/api.rst
@ -5,7 +5,7 @@ This page lists all the API methods and their parameters up to the version
|
|||||||
1.16.0 (Subsonic 6.1.2). Here you'll find details about which API features
|
1.16.0 (Subsonic 6.1.2). Here you'll find details about which API features
|
||||||
Supysonic support, plan on supporting, or won't.
|
Supysonic support, plan on supporting, or won't.
|
||||||
|
|
||||||
At the moment, the current target API version is 1.10.2.
|
At the moment, the current target API version is 1.12.0.
|
||||||
|
|
||||||
The following information was gathered by *diff*-ing various snapshots of the
|
The following information was gathered by *diff*-ing various snapshots of the
|
||||||
`Subsonic API page`__.
|
`Subsonic API page`__.
|
||||||
@ -227,7 +227,7 @@ Browsing
|
|||||||
================= ====== =
|
================= ====== =
|
||||||
Parameter Vers.
|
Parameter Vers.
|
||||||
================= ====== =
|
================= ====== =
|
||||||
``musicFolderId`` 1.14.0 📅
|
``musicFolderId`` 1.14.0 ✔️
|
||||||
================= ====== =
|
================= ====== =
|
||||||
|
|
||||||
.. _getArtist:
|
.. _getArtist:
|
||||||
@ -418,7 +418,7 @@ Album/song lists
|
|||||||
``fromYear`` ✔️
|
``fromYear`` ✔️
|
||||||
``toYear`` ✔️
|
``toYear`` ✔️
|
||||||
``genre`` ✔️
|
``genre`` ✔️
|
||||||
``musicFolderId`` 1.12.0 📅
|
``musicFolderId`` 1.12.0 ✔️
|
||||||
================= ====== =
|
================= ====== =
|
||||||
|
|
||||||
.. versionadded:: 1.10.1
|
.. versionadded:: 1.10.1
|
||||||
@ -441,7 +441,7 @@ Album/song lists
|
|||||||
``fromYear`` ✔️
|
``fromYear`` ✔️
|
||||||
``toYear`` ✔️
|
``toYear`` ✔️
|
||||||
``genre`` ✔️
|
``genre`` ✔️
|
||||||
``musicFolderId`` 1.12.0 📅
|
``musicFolderId`` 1.12.0 ✔️
|
||||||
================= ====== =
|
================= ====== =
|
||||||
|
|
||||||
.. versionadded:: 1.10.1
|
.. versionadded:: 1.10.1
|
||||||
@ -479,7 +479,7 @@ Album/song lists
|
|||||||
``genre`` 1.9.0 ✔️
|
``genre`` 1.9.0 ✔️
|
||||||
``count`` 1.9.0 ✔️
|
``count`` 1.9.0 ✔️
|
||||||
``offset`` 1.9.0 ✔️
|
``offset`` 1.9.0 ✔️
|
||||||
``musicFolderId`` 1.12.0 📅
|
``musicFolderId`` 1.12.0 ✔️
|
||||||
================= ====== =
|
================= ====== =
|
||||||
|
|
||||||
.. _getNowPlaying:
|
.. _getNowPlaying:
|
||||||
@ -500,7 +500,7 @@ Album/song lists
|
|||||||
================= ====== =
|
================= ====== =
|
||||||
Parameter Vers.
|
Parameter Vers.
|
||||||
================= ====== =
|
================= ====== =
|
||||||
``musicFolderId`` 1.12.0 📅
|
``musicFolderId`` 1.12.0 ✔️
|
||||||
================= ====== =
|
================= ====== =
|
||||||
|
|
||||||
.. _getStarred2:
|
.. _getStarred2:
|
||||||
@ -514,7 +514,7 @@ Album/song lists
|
|||||||
================= ====== =
|
================= ====== =
|
||||||
Parameter Vers.
|
Parameter Vers.
|
||||||
================= ====== =
|
================= ====== =
|
||||||
``musicFolderId`` 1.12.0 📅
|
``musicFolderId`` 1.12.0 ✔️
|
||||||
================= ====== =
|
================= ====== =
|
||||||
|
|
||||||
Searching
|
Searching
|
||||||
@ -558,7 +558,7 @@ Searching
|
|||||||
``albumOffset`` ✔️
|
``albumOffset`` ✔️
|
||||||
``songCount`` ✔️
|
``songCount`` ✔️
|
||||||
``songOffset`` ✔️
|
``songOffset`` ✔️
|
||||||
``musicFolderId`` 1.12.0 📅
|
``musicFolderId`` 1.12.0 ✔️
|
||||||
================= ====== =
|
================= ====== =
|
||||||
|
|
||||||
.. _search3:
|
.. _search3:
|
||||||
@ -579,7 +579,7 @@ Searching
|
|||||||
``albumOffset`` ✔️
|
``albumOffset`` ✔️
|
||||||
``songCount`` ✔️
|
``songCount`` ✔️
|
||||||
``songOffset`` ✔️
|
``songOffset`` ✔️
|
||||||
``musicFolderId`` 1.12.0 📅
|
``musicFolderId`` 1.12.0 ✔️
|
||||||
================= ====== =
|
================= ====== =
|
||||||
|
|
||||||
Playlists
|
Playlists
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
#
|
#
|
||||||
# Copyright (C) 2013-2020 Alban 'spl0k' Féron
|
# Copyright (C) 2013-2022 Alban 'spl0k' Féron
|
||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ from pony.orm import commit
|
|||||||
from ..db import ClientPrefs, Folder
|
from ..db import ClientPrefs, Folder
|
||||||
from ..managers.user import UserManager
|
from ..managers.user import UserManager
|
||||||
|
|
||||||
from .exceptions import GenericError, Unauthorized
|
from .exceptions import GenericError, Unauthorized, NotFound
|
||||||
from .formatters import JSONFormatter, JSONPFormatter, XMLFormatter
|
from .formatters import JSONFormatter, JSONPFormatter, XMLFormatter
|
||||||
|
|
||||||
api = Blueprint("api", __name__)
|
api = Blueprint("api", __name__)
|
||||||
@ -119,6 +119,22 @@ def get_entity_id(cls, eid):
|
|||||||
raise GenericError("Invalid ID")
|
raise GenericError("Invalid ID")
|
||||||
|
|
||||||
|
|
||||||
|
def get_root_folder(id):
|
||||||
|
if id is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
fid = int(id)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("Invalid folder ID")
|
||||||
|
|
||||||
|
folder = Folder.get(id=fid, root=True)
|
||||||
|
if folder is None:
|
||||||
|
raise NotFound("Folder")
|
||||||
|
|
||||||
|
return folder
|
||||||
|
|
||||||
|
|
||||||
from .errors import *
|
from .errors import *
|
||||||
|
|
||||||
from .system import *
|
from .system import *
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
#
|
#
|
||||||
# Copyright (C) 2013-2020 Alban 'spl0k' Féron
|
# Copyright (C) 2013-2022 Alban 'spl0k' Féron
|
||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
@ -21,8 +21,8 @@ from ..db import (
|
|||||||
)
|
)
|
||||||
from ..db import now
|
from ..db import now
|
||||||
|
|
||||||
from . import api_routing
|
from . import api_routing, get_root_folder
|
||||||
from .exceptions import GenericError, NotFound
|
from .exceptions import GenericError
|
||||||
|
|
||||||
|
|
||||||
@api_routing("/getRandomSongs")
|
@api_routing("/getRandomSongs")
|
||||||
@ -35,12 +35,7 @@ def rand_songs():
|
|||||||
size = int(size) if size else 10
|
size = int(size) if size else 10
|
||||||
fromYear = int(fromYear) if fromYear else None
|
fromYear = int(fromYear) if fromYear else None
|
||||||
toYear = int(toYear) if toYear else None
|
toYear = int(toYear) if toYear else None
|
||||||
fid = None
|
root = get_root_folder(musicFolderId)
|
||||||
if musicFolderId:
|
|
||||||
try:
|
|
||||||
fid = int(musicFolderId)
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError("Invalid folder ID")
|
|
||||||
|
|
||||||
query = Track.select()
|
query = Track.select()
|
||||||
if fromYear:
|
if fromYear:
|
||||||
@ -49,11 +44,8 @@ def rand_songs():
|
|||||||
query = query.filter(lambda t: t.year <= toYear)
|
query = query.filter(lambda t: t.year <= toYear)
|
||||||
if genre:
|
if genre:
|
||||||
query = query.filter(lambda t: t.genre == genre)
|
query = query.filter(lambda t: t.genre == genre)
|
||||||
if fid:
|
if root:
|
||||||
if not Folder.exists(id=fid, root=True):
|
query = query.filter(lambda t: t.root_folder == root)
|
||||||
raise NotFound("Folder")
|
|
||||||
|
|
||||||
query = query.filter(lambda t: t.root_folder.id == fid)
|
|
||||||
|
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"randomSongs",
|
"randomSongs",
|
||||||
@ -70,11 +62,15 @@ def rand_songs():
|
|||||||
def album_list():
|
def album_list():
|
||||||
ltype = request.values["type"]
|
ltype = request.values["type"]
|
||||||
|
|
||||||
size, offset = map(request.values.get, ("size", "offset"))
|
size, offset, mfid = map(request.values.get, ("size", "offset", "musicFolderId"))
|
||||||
size = int(size) if size else 10
|
size = int(size) if size else 10
|
||||||
offset = int(offset) if offset else 0
|
offset = int(offset) if offset else 0
|
||||||
|
root = get_root_folder(mfid)
|
||||||
|
|
||||||
query = select(t.folder for t in Track)
|
query = select(t.folder for t in Track)
|
||||||
|
if root is not None:
|
||||||
|
query = select(t.folder for t in Track if t.root_folder == root)
|
||||||
|
|
||||||
if ltype == "random":
|
if ltype == "random":
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"albumList",
|
"albumList",
|
||||||
@ -94,13 +90,18 @@ def album_list():
|
|||||||
elif ltype == "recent":
|
elif ltype == "recent":
|
||||||
query = select(
|
query = select(
|
||||||
t.folder for t in Track if max(t.folder.tracks.last_play) is not None
|
t.folder for t in Track if max(t.folder.tracks.last_play) is not None
|
||||||
).sort_by(lambda f: desc(max(f.tracks.last_play)))
|
)
|
||||||
|
if root is not None:
|
||||||
|
query = query.where(lambda t: t.root_folder == root)
|
||||||
|
query = query.sort_by(lambda f: desc(max(f.tracks.last_play)))
|
||||||
elif ltype == "starred":
|
elif ltype == "starred":
|
||||||
query = select(
|
query = select(
|
||||||
s.starred
|
s.starred
|
||||||
for s in StarredFolder
|
for s in StarredFolder
|
||||||
if s.user.id == request.user.id and count(s.starred.tracks) > 0
|
if s.user.id == request.user.id and count(s.starred.tracks) > 0
|
||||||
)
|
)
|
||||||
|
if root is not None:
|
||||||
|
query = query.filter(lambda f: f.path.startswith(root.path))
|
||||||
elif ltype == "alphabeticalByName":
|
elif ltype == "alphabeticalByName":
|
||||||
query = query.sort_by(Folder.name).distinct()
|
query = query.sort_by(Folder.name).distinct()
|
||||||
elif ltype == "alphabeticalByArtist":
|
elif ltype == "alphabeticalByArtist":
|
||||||
@ -135,11 +136,15 @@ def album_list():
|
|||||||
def album_list_id3():
|
def album_list_id3():
|
||||||
ltype = request.values["type"]
|
ltype = request.values["type"]
|
||||||
|
|
||||||
size, offset = map(request.values.get, ("size", "offset"))
|
size, offset, mfid = map(request.values.get, ("size", "offset", "musicFolderId"))
|
||||||
size = int(size) if size else 10
|
size = int(size) if size else 10
|
||||||
offset = int(offset) if offset else 0
|
offset = int(offset) if offset else 0
|
||||||
|
root = get_root_folder(mfid)
|
||||||
|
|
||||||
query = Album.select()
|
query = Album.select()
|
||||||
|
if root is not None:
|
||||||
|
query = query.where(lambda a: root in a.tracks.root_folder)
|
||||||
|
|
||||||
if ltype == "random":
|
if ltype == "random":
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"albumList2",
|
"albumList2",
|
||||||
@ -150,11 +155,13 @@ def album_list_id3():
|
|||||||
elif ltype == "frequent":
|
elif ltype == "frequent":
|
||||||
query = query.order_by(lambda a: desc(avg(a.tracks.play_count)))
|
query = query.order_by(lambda a: desc(avg(a.tracks.play_count)))
|
||||||
elif ltype == "recent":
|
elif ltype == "recent":
|
||||||
query = Album.select(lambda a: max(a.tracks.last_play) is not None).order_by(
|
query = query.where(lambda a: max(a.tracks.last_play) is not None).order_by(
|
||||||
lambda a: desc(max(a.tracks.last_play))
|
lambda a: desc(max(a.tracks.last_play))
|
||||||
)
|
)
|
||||||
elif ltype == "starred":
|
elif ltype == "starred":
|
||||||
query = select(s.starred for s in StarredAlbum if s.user.id == request.user.id)
|
query = select(s.starred for s in StarredAlbum if s.user.id == request.user.id)
|
||||||
|
if root is not None:
|
||||||
|
query = query.filter(lambda a: root in a.tracks.root_folder)
|
||||||
elif ltype == "alphabeticalByName":
|
elif ltype == "alphabeticalByName":
|
||||||
query = query.order_by(Album.name)
|
query = query.order_by(Album.name)
|
||||||
elif ltype == "alphabeticalByArtist":
|
elif ltype == "alphabeticalByArtist":
|
||||||
@ -191,14 +198,22 @@ def album_list_id3():
|
|||||||
def songs_by_genre():
|
def songs_by_genre():
|
||||||
genre = request.values["genre"]
|
genre = request.values["genre"]
|
||||||
|
|
||||||
count, offset = map(request.values.get, ("count", "offset"))
|
count, offset, mfid = map(request.values.get, ("count", "offset", "musicFolderId"))
|
||||||
count = int(count) if count else 10
|
count = int(count) if count else 10
|
||||||
offset = int(offset) if offset else 0
|
offset = int(offset) if offset else 0
|
||||||
|
root = get_root_folder(mfid)
|
||||||
|
|
||||||
query = select(t for t in Track if t.genre == genre).limit(count, offset)
|
query = select(t for t in Track if t.genre == genre)
|
||||||
|
if root is not None:
|
||||||
|
query = query.where(lambda t: t.root_folder == root)
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"songsByGenre",
|
"songsByGenre",
|
||||||
{"song": [t.as_subsonic_child(request.user, request.client) for t in query]},
|
{
|
||||||
|
"song": [
|
||||||
|
t.as_subsonic_child(request.user, request.client)
|
||||||
|
for t in query.limit(count, offset)
|
||||||
|
]
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -227,51 +242,49 @@ def now_playing():
|
|||||||
|
|
||||||
@api_routing("/getStarred")
|
@api_routing("/getStarred")
|
||||||
def get_starred():
|
def get_starred():
|
||||||
|
mfid = request.values.get("musicFolderId")
|
||||||
|
root = get_root_folder(mfid)
|
||||||
|
|
||||||
folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id)
|
folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id)
|
||||||
|
if root is not None:
|
||||||
|
folders = folders.filter(lambda f: f.path.startswith(root.path))
|
||||||
|
|
||||||
|
arq = folders.filter(lambda f: count(f.tracks) == 0)
|
||||||
|
alq = folders.filter(lambda f: count(f.tracks) > 0)
|
||||||
|
trq = select(s.starred for s in StarredTrack if s.user.id == request.user.id)
|
||||||
|
|
||||||
|
if root is not None:
|
||||||
|
trq = trq.filter(lambda t: t.root_folder == root)
|
||||||
|
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"starred",
|
"starred",
|
||||||
{
|
{
|
||||||
"artist": [
|
"artist": [sf.as_subsonic_artist(request.user) for sf in arq],
|
||||||
sf.as_subsonic_artist(request.user)
|
"album": [sf.as_subsonic_child(request.user) for sf in alq],
|
||||||
for sf in folders.filter(lambda f: count(f.tracks) == 0)
|
"song": [st.as_subsonic_child(request.user, request.client) for st in trq],
|
||||||
],
|
|
||||||
"album": [
|
|
||||||
sf.as_subsonic_child(request.user)
|
|
||||||
for sf in folders.filter(lambda f: count(f.tracks) > 0)
|
|
||||||
],
|
|
||||||
"song": [
|
|
||||||
st.as_subsonic_child(request.user, request.client)
|
|
||||||
for st in select(
|
|
||||||
s.starred for s in StarredTrack if s.user.id == request.user.id
|
|
||||||
)
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_routing("/getStarred2")
|
@api_routing("/getStarred2")
|
||||||
def get_starred_id3():
|
def get_starred_id3():
|
||||||
|
mfid = request.values.get("musicFolderId")
|
||||||
|
root = get_root_folder(mfid)
|
||||||
|
|
||||||
|
arq = select(s.starred for s in StarredArtist if s.user.id == request.user.id)
|
||||||
|
alq = select(s.starred for s in StarredAlbum if s.user.id == request.user.id)
|
||||||
|
trq = select(s.starred for s in StarredTrack if s.user.id == request.user.id)
|
||||||
|
|
||||||
|
if root is not None:
|
||||||
|
arq = arq.filter(lambda a: root in a.tracks.root_folder)
|
||||||
|
alq = alq.filter(lambda a: root in a.tracks.root_folder)
|
||||||
|
trq = trq.filter(lambda t: t.root_folder == root)
|
||||||
|
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"starred2",
|
"starred2",
|
||||||
{
|
{
|
||||||
"artist": [
|
"artist": [sa.as_subsonic_artist(request.user) for sa in arq],
|
||||||
sa.as_subsonic_artist(request.user)
|
"album": [sa.as_subsonic_album(request.user) for sa in alq],
|
||||||
for sa in select(
|
"song": [st.as_subsonic_child(request.user, request.client) for st in trq],
|
||||||
s.starred for s in StarredArtist if s.user.id == request.user.id
|
|
||||||
)
|
|
||||||
],
|
|
||||||
"album": [
|
|
||||||
sa.as_subsonic_album(request.user)
|
|
||||||
for sa in select(
|
|
||||||
s.starred for s in StarredAlbum if s.user.id == request.user.id
|
|
||||||
)
|
|
||||||
],
|
|
||||||
"song": [
|
|
||||||
st.as_subsonic_child(request.user, request.client)
|
|
||||||
for st in select(
|
|
||||||
s.starred for s in StarredTrack if s.user.id == request.user.id
|
|
||||||
)
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
#
|
#
|
||||||
# Copyright (C) 2013-2020 Alban 'spl0k' Féron
|
# Copyright (C) 2013-2022 Alban 'spl0k' Féron
|
||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
@ -9,11 +9,11 @@ import re
|
|||||||
import string
|
import string
|
||||||
|
|
||||||
from flask import current_app, request
|
from flask import current_app, request
|
||||||
from pony.orm import ObjectNotFound, select, count
|
from pony.orm import select, count
|
||||||
|
|
||||||
from ..db import Folder, Artist, Album, Track
|
from ..db import Folder, Artist, Album, Track
|
||||||
|
|
||||||
from . import get_entity, get_entity_id, api_routing
|
from . import get_entity, get_root_folder, api_routing
|
||||||
|
|
||||||
|
|
||||||
@api_routing("/getMusicFolders")
|
@api_routing("/getMusicFolders")
|
||||||
@ -80,12 +80,7 @@ def list_indexes():
|
|||||||
if musicFolderId is None:
|
if musicFolderId is None:
|
||||||
folders = Folder.select(lambda f: f.root)[:]
|
folders = Folder.select(lambda f: f.root)[:]
|
||||||
else:
|
else:
|
||||||
mfid = get_entity_id(Folder, musicFolderId)
|
folders = [get_root_folder(musicFolderId)]
|
||||||
folder = Folder[mfid]
|
|
||||||
if not folder.root:
|
|
||||||
raise ObjectNotFound(Folder, mfid)
|
|
||||||
|
|
||||||
folders = [folder]
|
|
||||||
|
|
||||||
last_modif = max(f.last_scan for f in folders)
|
last_modif = max(f.last_scan for f in folders)
|
||||||
if ifModifiedSince is not None and last_modif < ifModifiedSince:
|
if ifModifiedSince is not None and last_modif < ifModifiedSince:
|
||||||
@ -153,8 +148,14 @@ def list_genres():
|
|||||||
|
|
||||||
@api_routing("/getArtists")
|
@api_routing("/getArtists")
|
||||||
def list_artists():
|
def list_artists():
|
||||||
# According to the API page, there are no parameters?
|
mfid = request.values.get("musicFolderId")
|
||||||
indexes = build_indexes(Artist.select())
|
|
||||||
|
query = Artist.select()
|
||||||
|
if mfid is not None:
|
||||||
|
folder = get_root_folder(mfid)
|
||||||
|
query = Artist.select(lambda a: folder in a.tracks.root_folder)
|
||||||
|
|
||||||
|
indexes = build_indexes(query)
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"artists",
|
"artists",
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
#
|
#
|
||||||
# Copyright (C) 2013-2020 Alban 'spl0k' Féron
|
# Copyright (C) 2013-2022 Alban 'spl0k' Féron
|
||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ from pony.orm import select
|
|||||||
|
|
||||||
from ..db import Folder, Track, Artist, Album
|
from ..db import Folder, Track, Artist, Album
|
||||||
|
|
||||||
from . import api_routing
|
from . import api_routing, get_root_folder
|
||||||
from .exceptions import MissingParameter
|
from .exceptions import MissingParameter
|
||||||
|
|
||||||
|
|
||||||
@ -93,6 +93,7 @@ def new_search():
|
|||||||
album_offset,
|
album_offset,
|
||||||
song_count,
|
song_count,
|
||||||
song_offset,
|
song_offset,
|
||||||
|
mfid,
|
||||||
) = map(
|
) = map(
|
||||||
request.values.get,
|
request.values.get,
|
||||||
(
|
(
|
||||||
@ -102,6 +103,7 @@ def new_search():
|
|||||||
"albumOffset",
|
"albumOffset",
|
||||||
"songCount",
|
"songCount",
|
||||||
"songOffset",
|
"songOffset",
|
||||||
|
"musicFolderId",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -111,14 +113,20 @@ def new_search():
|
|||||||
album_offset = int(album_offset) if album_offset else 0
|
album_offset = int(album_offset) if album_offset else 0
|
||||||
song_count = int(song_count) if song_count else 20
|
song_count = int(song_count) if song_count else 20
|
||||||
song_offset = int(song_offset) if song_offset else 0
|
song_offset = int(song_offset) if song_offset else 0
|
||||||
|
root = get_root_folder(mfid)
|
||||||
|
|
||||||
artists = select(
|
artists = select(t.folder.parent for t in Track if query in t.folder.parent.name)
|
||||||
t.folder.parent for t in Track if query in t.folder.parent.name
|
albums = select(t.folder for t in Track if query in t.folder.name)
|
||||||
).limit(artist_count, artist_offset)
|
songs = Track.select(lambda t: query in t.title)
|
||||||
albums = select(t.folder for t in Track if query in t.folder.name).limit(
|
|
||||||
album_count, album_offset
|
if root is not None:
|
||||||
)
|
artists = artists.where(lambda t: t.root_folder == root)
|
||||||
songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
|
albums = albums.where(lambda t: t.root_folder == root)
|
||||||
|
songs = songs.where(lambda t: t.root_folder == root)
|
||||||
|
|
||||||
|
artists = artists.limit(artist_count, artist_offset)
|
||||||
|
albums = albums.limit(album_count, album_offset)
|
||||||
|
songs = songs.limit(song_count, song_offset)
|
||||||
|
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"searchResult2",
|
"searchResult2",
|
||||||
@ -145,6 +153,7 @@ def search_id3():
|
|||||||
album_offset,
|
album_offset,
|
||||||
song_count,
|
song_count,
|
||||||
song_offset,
|
song_offset,
|
||||||
|
mfid,
|
||||||
) = map(
|
) = map(
|
||||||
request.values.get,
|
request.values.get,
|
||||||
(
|
(
|
||||||
@ -154,6 +163,7 @@ def search_id3():
|
|||||||
"albumOffset",
|
"albumOffset",
|
||||||
"songCount",
|
"songCount",
|
||||||
"songOffset",
|
"songOffset",
|
||||||
|
"musicFolderId",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -163,12 +173,20 @@ def search_id3():
|
|||||||
album_offset = int(album_offset) if album_offset else 0
|
album_offset = int(album_offset) if album_offset else 0
|
||||||
song_count = int(song_count) if song_count else 20
|
song_count = int(song_count) if song_count else 20
|
||||||
song_offset = int(song_offset) if song_offset else 0
|
song_offset = int(song_offset) if song_offset else 0
|
||||||
|
root = get_root_folder(mfid)
|
||||||
|
|
||||||
artists = Artist.select(lambda a: query in a.name).limit(
|
artists = Artist.select(lambda a: query in a.name)
|
||||||
artist_count, artist_offset
|
albums = Album.select(lambda a: query in a.name)
|
||||||
)
|
songs = Track.select(lambda t: query in t.title)
|
||||||
albums = Album.select(lambda a: query in a.name).limit(album_count, album_offset)
|
|
||||||
songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
|
if root is not None:
|
||||||
|
artists = artists.where(lambda a: root in a.tracks.root_folder)
|
||||||
|
albums = albums.where(lambda a: root in a.tracks.root_folder)
|
||||||
|
songs = songs.where(lambda t: t.root_folder == root)
|
||||||
|
|
||||||
|
artists = artists.limit(artist_count, artist_offset)
|
||||||
|
albums = albums.limit(album_count, album_offset)
|
||||||
|
songs = songs.limit(song_count, song_offset)
|
||||||
|
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"searchResult3",
|
"searchResult3",
|
||||||
|
@ -20,7 +20,7 @@ NSMAP = {"sub": NS}
|
|||||||
class ApiTestBase(TestBase):
|
class ApiTestBase(TestBase):
|
||||||
__with_api__ = True
|
__with_api__ = True
|
||||||
|
|
||||||
def setUp(self, apiVersion="1.10.2"):
|
def setUp(self, apiVersion="1.12.0"):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.apiVersion = apiVersion
|
self.apiVersion = apiVersion
|
||||||
xsd = etree.parse(
|
xsd = etree.parse(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
#
|
#
|
||||||
# Copyright (C) 2017-2020 Alban 'spl0k' Féron
|
# Copyright (C) 2017-2022 Alban 'spl0k' Féron
|
||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ class AlbumSongsTestCase(ApiTestBase):
|
|||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
folder = Folder(name="Root", root=True, path="tests/assets")
|
folder = Folder(name="Root", root=True, path="tests/assets")
|
||||||
|
empty = Folder(name="Root", root=True, path="/tmp")
|
||||||
artist = Artist(name="Artist")
|
artist = Artist(name="Artist")
|
||||||
album = Album(name="Album", artist=artist)
|
album = Album(name="Album", artist=artist)
|
||||||
|
|
||||||
@ -70,6 +71,12 @@ class AlbumSongsTestCase(ApiTestBase):
|
|||||||
error=0,
|
error=0,
|
||||||
)
|
)
|
||||||
self._make_request("getAlbumList", {"type": "byGenre"}, error=10)
|
self._make_request("getAlbumList", {"type": "byGenre"}, error=10)
|
||||||
|
self._make_request(
|
||||||
|
"getAlbumList", {"type": "random", "musicFolderId": "id"}, error=0
|
||||||
|
)
|
||||||
|
self._make_request(
|
||||||
|
"getAlbumList", {"type": "random", "musicFolderId": 12}, error=70
|
||||||
|
)
|
||||||
|
|
||||||
types_and_count = [
|
types_and_count = [
|
||||||
("random", 1),
|
("random", 1),
|
||||||
@ -120,8 +127,21 @@ class AlbumSongsTestCase(ApiTestBase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(len(child), 1)
|
self.assertEqual(len(child), 1)
|
||||||
|
|
||||||
|
_, child = self._make_request(
|
||||||
|
"getAlbumList",
|
||||||
|
{"musicFolderId": 1, "type": "alphabeticalByName"},
|
||||||
|
tag="albumList",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(child), 1)
|
||||||
|
_, child = self._make_request(
|
||||||
|
"getAlbumList",
|
||||||
|
{"musicFolderId": 2, "type": "alphabeticalByName"},
|
||||||
|
tag="albumList",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(child), 0)
|
||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
Folder.get().delete()
|
Folder[1].delete()
|
||||||
rv, child = self._make_request(
|
rv, child = self._make_request(
|
||||||
"getAlbumList", {"type": "random"}, tag="albumList"
|
"getAlbumList", {"type": "random"}, tag="albumList"
|
||||||
)
|
)
|
||||||
@ -143,6 +163,12 @@ class AlbumSongsTestCase(ApiTestBase):
|
|||||||
error=0,
|
error=0,
|
||||||
)
|
)
|
||||||
self._make_request("getAlbumList2", {"type": "byGenre"}, error=10)
|
self._make_request("getAlbumList2", {"type": "byGenre"}, error=10)
|
||||||
|
self._make_request(
|
||||||
|
"getAlbumList2", {"type": "random", "musicFolderId": "id"}, error=0
|
||||||
|
)
|
||||||
|
self._make_request(
|
||||||
|
"getAlbumList2", {"type": "random", "musicFolderId": 12}, error=70
|
||||||
|
)
|
||||||
|
|
||||||
types = [
|
types = [
|
||||||
"random",
|
"random",
|
||||||
@ -192,6 +218,19 @@ class AlbumSongsTestCase(ApiTestBase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(len(child), 1)
|
self.assertEqual(len(child), 1)
|
||||||
|
|
||||||
|
_, child = self._make_request(
|
||||||
|
"getAlbumList2",
|
||||||
|
{"musicFolderId": 1, "type": "alphabeticalByName"},
|
||||||
|
tag="albumList2",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(child), 1)
|
||||||
|
_, child = self._make_request(
|
||||||
|
"getAlbumList2",
|
||||||
|
{"musicFolderId": 2, "type": "alphabeticalByName"},
|
||||||
|
tag="albumList2",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(child), 0)
|
||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
Track.select().delete()
|
Track.select().delete()
|
||||||
Album.get().delete()
|
Album.get().delete()
|
||||||
@ -211,15 +250,13 @@ class AlbumSongsTestCase(ApiTestBase):
|
|||||||
"getRandomSongs", tag="randomSongs", skip_post=True
|
"getRandomSongs", tag="randomSongs", skip_post=True
|
||||||
)
|
)
|
||||||
|
|
||||||
with db_session:
|
|
||||||
fid = Folder.get().id
|
|
||||||
self._make_request(
|
self._make_request(
|
||||||
"getRandomSongs",
|
"getRandomSongs",
|
||||||
{
|
{
|
||||||
"fromYear": -52,
|
"fromYear": -52,
|
||||||
"toYear": "1984",
|
"toYear": "1984",
|
||||||
"genre": "some cryptic subgenre youve never heard of",
|
"genre": "some cryptic subgenre youve never heard of",
|
||||||
"musicFolderId": fid,
|
"musicFolderId": 1,
|
||||||
},
|
},
|
||||||
tag="randomSongs",
|
tag="randomSongs",
|
||||||
)
|
)
|
||||||
@ -229,9 +266,11 @@ class AlbumSongsTestCase(ApiTestBase):
|
|||||||
|
|
||||||
def test_get_starred(self):
|
def test_get_starred(self):
|
||||||
self._make_request("getStarred", tag="starred")
|
self._make_request("getStarred", tag="starred")
|
||||||
|
self._make_request("getStarred", {"musicFolderId": 1}, tag="starred")
|
||||||
|
|
||||||
def test_get_starred2(self):
|
def test_get_starred2(self):
|
||||||
self._make_request("getStarred2", tag="starred2")
|
self._make_request("getStarred2", tag="starred2")
|
||||||
|
self._make_request("getStarred2", {"musicFolderId": 1}, tag="starred2")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
#
|
#
|
||||||
# Copyright (C) 2017-2020 Alban 'spl0k' Féron
|
# Copyright (C) 2017-2022 Alban 'spl0k' Féron
|
||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
@ -21,14 +21,14 @@ class BrowseTestCase(ApiTestBase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
Folder(root=True, name="Empty root", path="/tmp")
|
self.empty_root = Folder(root=True, name="Empty root", path="/tmp")
|
||||||
root = Folder(root=True, name="Root folder", path="tests/assets")
|
self.root = Folder(root=True, name="Root folder", path="tests/assets")
|
||||||
|
|
||||||
for letter in "ABC":
|
for letter in "ABC":
|
||||||
folder = Folder(
|
folder = Folder(
|
||||||
name=letter + "rtist",
|
name=letter + "rtist",
|
||||||
path="tests/assets/{}rtist".format(letter),
|
path="tests/assets/{}rtist".format(letter),
|
||||||
parent=root,
|
parent=self.root,
|
||||||
)
|
)
|
||||||
|
|
||||||
artist = Artist(name=letter + "rtist")
|
artist = Artist(name=letter + "rtist")
|
||||||
@ -56,7 +56,7 @@ class BrowseTestCase(ApiTestBase):
|
|||||||
letter, lether, song
|
letter, lether, song
|
||||||
),
|
),
|
||||||
last_modification=0,
|
last_modification=0,
|
||||||
root_folder=root,
|
root_folder=self.root,
|
||||||
folder=afolder,
|
folder=afolder,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -132,13 +132,26 @@ class BrowseTestCase(ApiTestBase):
|
|||||||
# same as getIndexes standard case
|
# same as getIndexes standard case
|
||||||
# dataset should be improved to have a different directory structure than /root/Artist/Album/Track
|
# dataset should be improved to have a different directory structure than /root/Artist/Album/Track
|
||||||
|
|
||||||
rv, child = self._make_request("getArtists", tag="artists")
|
_, child = self._make_request("getArtists", tag="artists")
|
||||||
self.assertEqual(len(child), 3)
|
self.assertEqual(len(child), 3)
|
||||||
for i, letter in enumerate(["A", "B", "C"]):
|
for i, letter in enumerate(["A", "B", "C"]):
|
||||||
self.assertEqual(child[i].get("name"), letter)
|
self.assertEqual(child[i].get("name"), letter)
|
||||||
self.assertEqual(len(child[i]), 1)
|
self.assertEqual(len(child[i]), 1)
|
||||||
self.assertEqual(child[i][0].get("name"), letter + "rtist")
|
self.assertEqual(child[i][0].get("name"), letter + "rtist")
|
||||||
|
|
||||||
|
self._make_request("getArtists", {"musicFolderId": "id"}, error=0)
|
||||||
|
self._make_request("getArtists", {"musicFolderId": -3}, error=70)
|
||||||
|
|
||||||
|
_, child = self._make_request(
|
||||||
|
"getArtists", {"musicFolderId": str(self.empty_root.id)}, tag="artists"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(child), 0)
|
||||||
|
|
||||||
|
_, child = self._make_request(
|
||||||
|
"getArtists", {"musicFolderId": str(self.root.id)}, tag="artists"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(child), 3)
|
||||||
|
|
||||||
def test_get_artist(self):
|
def test_get_artist(self):
|
||||||
# dataset should be improved to have tracks by a different artist than the album's artist
|
# dataset should be improved to have tracks by a different artist than the album's artist
|
||||||
self._make_request("getArtist", error=10)
|
self._make_request("getArtist", error=10)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# This file is part of Supysonic.
|
# This file is part of Supysonic.
|
||||||
# Supysonic is a Python implementation of the Subsonic server API.
|
# Supysonic is a Python implementation of the Subsonic server API.
|
||||||
#
|
#
|
||||||
# Copyright (C) 2017 Alban 'spl0k' Féron
|
# Copyright (C) 2017-2022 Alban 'spl0k' Féron
|
||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
@ -21,6 +21,7 @@ class SearchTestCase(ApiTestBase):
|
|||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
root = Folder(root=True, name="Root folder", path="tests/assets")
|
root = Folder(root=True, name="Root folder", path="tests/assets")
|
||||||
|
Folder(root=True, name="Empty", path="/tmp")
|
||||||
|
|
||||||
for letter in "ABC":
|
for letter in "ABC":
|
||||||
folder = Folder(
|
folder = Folder(
|
||||||
@ -58,7 +59,7 @@ class SearchTestCase(ApiTestBase):
|
|||||||
|
|
||||||
commit()
|
commit()
|
||||||
|
|
||||||
self.assertEqual(Folder.select().count(), 10)
|
self.assertEqual(Folder.select().count(), 11)
|
||||||
self.assertEqual(Artist.select().count(), 3)
|
self.assertEqual(Artist.select().count(), 3)
|
||||||
self.assertEqual(Album.select().count(), 6)
|
self.assertEqual(Album.select().count(), 6)
|
||||||
self.assertEqual(Track.select().count(), 18)
|
self.assertEqual(Track.select().count(), 18)
|
||||||
@ -196,6 +197,10 @@ class SearchTestCase(ApiTestBase):
|
|||||||
self._make_request("search2", {"query": "a", "albumOffset": "sstring"}, error=0)
|
self._make_request("search2", {"query": "a", "albumOffset": "sstring"}, error=0)
|
||||||
self._make_request("search2", {"query": "a", "songCount": "string"}, error=0)
|
self._make_request("search2", {"query": "a", "songCount": "string"}, error=0)
|
||||||
self._make_request("search2", {"query": "a", "songOffset": "sstring"}, error=0)
|
self._make_request("search2", {"query": "a", "songOffset": "sstring"}, error=0)
|
||||||
|
self._make_request(
|
||||||
|
"search2", {"query": "a", "musicFolderId": "sstring"}, error=0
|
||||||
|
)
|
||||||
|
self._make_request("search2", {"query": "a", "musicFolderId": -2}, error=70)
|
||||||
|
|
||||||
# no search
|
# no search
|
||||||
self._make_request("search2", error=10)
|
self._make_request("search2", error=10)
|
||||||
@ -288,6 +293,17 @@ class SearchTestCase(ApiTestBase):
|
|||||||
self.assertNotIn(song, songs)
|
self.assertNotIn(song, songs)
|
||||||
songs.append(song)
|
songs.append(song)
|
||||||
|
|
||||||
|
# root filtering
|
||||||
|
_, child = self._make_request(
|
||||||
|
"search2", {"query": "One", "musicFolderId": 1}, tag="searchResult2"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self._xpath(child, "./song")), 6)
|
||||||
|
|
||||||
|
_, child = self._make_request(
|
||||||
|
"search2", {"query": "One", "musicFolderId": 2}, tag="searchResult2"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self._xpath(child, "./song")), 0)
|
||||||
|
|
||||||
# Almost identical as above. Test dataset (and tests) should probably be changed
|
# Almost identical as above. Test dataset (and tests) should probably be changed
|
||||||
# to have folders that don't share names with artists or albums
|
# to have folders that don't share names with artists or albums
|
||||||
def test_search3(self):
|
def test_search3(self):
|
||||||
@ -300,6 +316,10 @@ class SearchTestCase(ApiTestBase):
|
|||||||
self._make_request("search3", {"query": "a", "albumOffset": "sstring"}, error=0)
|
self._make_request("search3", {"query": "a", "albumOffset": "sstring"}, error=0)
|
||||||
self._make_request("search3", {"query": "a", "songCount": "string"}, error=0)
|
self._make_request("search3", {"query": "a", "songCount": "string"}, error=0)
|
||||||
self._make_request("search3", {"query": "a", "songOffset": "sstring"}, error=0)
|
self._make_request("search3", {"query": "a", "songOffset": "sstring"}, error=0)
|
||||||
|
self._make_request(
|
||||||
|
"search3", {"query": "a", "musicFolderId": "sstring"}, error=0
|
||||||
|
)
|
||||||
|
self._make_request("search3", {"query": "a", "musicFolderId": -2}, error=70)
|
||||||
|
|
||||||
# no search
|
# no search
|
||||||
self._make_request("search3", error=10)
|
self._make_request("search3", error=10)
|
||||||
@ -392,6 +412,17 @@ class SearchTestCase(ApiTestBase):
|
|||||||
self.assertNotIn(song, songs)
|
self.assertNotIn(song, songs)
|
||||||
songs.append(song)
|
songs.append(song)
|
||||||
|
|
||||||
|
# root filtering
|
||||||
|
_, child = self._make_request(
|
||||||
|
"search3", {"query": "One", "musicFolderId": 1}, tag="searchResult3"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self._xpath(child, "./song")), 6)
|
||||||
|
|
||||||
|
_, child = self._make_request(
|
||||||
|
"search3", {"query": "One", "musicFolderId": 2}, tag="searchResult3"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self._xpath(child, "./song")), 0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
targetNamespace="http://subsonic.org/restapi"
|
targetNamespace="http://subsonic.org/restapi"
|
||||||
attributeFormDefault="unqualified"
|
attributeFormDefault="unqualified"
|
||||||
elementFormDefault="qualified"
|
elementFormDefault="qualified"
|
||||||
version="1.10.2">
|
version="1.12.0">
|
||||||
|
|
||||||
<xs:element name="subsonic-response" type="sub:Response"/>
|
<xs:element name="subsonic-response" type="sub:Response"/>
|
||||||
|
|
||||||
@ -39,9 +39,14 @@
|
|||||||
<xs:element name="podcasts" type="sub:Podcasts" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="podcasts" type="sub:Podcasts" minOccurs="1" maxOccurs="1"/>
|
||||||
<xs:element name="internetRadioStations" type="sub:InternetRadioStations" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="internetRadioStations" type="sub:InternetRadioStations" minOccurs="1" maxOccurs="1"/>
|
||||||
<xs:element name="bookmarks" type="sub:Bookmarks" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="bookmarks" type="sub:Bookmarks" minOccurs="1" maxOccurs="1"/>
|
||||||
|
<xs:element name="playQueue" type="sub:PlayQueue" minOccurs="1" maxOccurs="1"/>
|
||||||
<xs:element name="shares" type="sub:Shares" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="shares" type="sub:Shares" minOccurs="1" maxOccurs="1"/>
|
||||||
<xs:element name="starred" type="sub:Starred" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="starred" type="sub:Starred" minOccurs="1" maxOccurs="1"/>
|
||||||
<xs:element name="starred2" type="sub:Starred2" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="starred2" type="sub:Starred2" minOccurs="1" maxOccurs="1"/>
|
||||||
|
<xs:element name="artistInfo" type="sub:ArtistInfo" minOccurs="1" maxOccurs="1"/>
|
||||||
|
<xs:element name="artistInfo2" type="sub:ArtistInfo2" minOccurs="1" maxOccurs="1"/>
|
||||||
|
<xs:element name="similarSongs" type="sub:SimilarSongs" minOccurs="1" maxOccurs="1"/>
|
||||||
|
<xs:element name="similarSongs2" type="sub:SimilarSongs2" minOccurs="1" maxOccurs="1"/>
|
||||||
<xs:element name="error" type="sub:Error" minOccurs="1" maxOccurs="1"/>
|
<xs:element name="error" type="sub:Error" minOccurs="1" maxOccurs="1"/>
|
||||||
</xs:choice>
|
</xs:choice>
|
||||||
<xs:attribute name="status" type="sub:ResponseStatus" use="required"/>
|
<xs:attribute name="status" type="sub:ResponseStatus" use="required"/>
|
||||||
@ -287,11 +292,12 @@
|
|||||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||||
<xs:attribute name="comment" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
|
<xs:attribute name="comment" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
|
||||||
<xs:attribute name="owner" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
|
<xs:attribute name="owner" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
|
||||||
<xs:attribute name="public" type="xs:boolean" use="optional"/> <!--Added in 1.8.0-->
|
<xs:attribute name="public" type="xs:boolean" use="optional"/> <!--Added in 1.8.0-->
|
||||||
<xs:attribute name="songCount" type="xs:int" use="required"/> <!--Added in 1.8.0-->
|
<xs:attribute name="songCount" type="xs:int" use="required"/> <!--Added in 1.8.0-->
|
||||||
<xs:attribute name="duration" type="xs:int" use="required"/> <!--Added in 1.8.0-->
|
<xs:attribute name="duration" type="xs:int" use="required"/> <!--Added in 1.8.0-->
|
||||||
<xs:attribute name="created" type="xs:dateTime" use="required"/> <!--Added in 1.8.0-->
|
<xs:attribute name="created" type="xs:dateTime" use="required"/> <!--Added in 1.8.0-->
|
||||||
|
<xs:attribute name="coverArt" type="xs:string" use="optional"/> <!--Added in 1.11.0-->
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="PlaylistWithSongs">
|
<xs:complexType name="PlaylistWithSongs">
|
||||||
@ -426,6 +432,17 @@
|
|||||||
<xs:attribute name="changed" type="xs:dateTime" use="required"/>
|
<xs:attribute name="changed" type="xs:dateTime" use="required"/>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="PlayQueue">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="current" type="xs:int" use="optional"/> <!-- ID of currently playing track -->
|
||||||
|
<xs:attribute name="position" type="xs:long" use="optional"/> <!-- Position in milliseconds of currently playing track -->
|
||||||
|
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||||
|
<xs:attribute name="changed" type="xs:dateTime" use="required"/>
|
||||||
|
<xs:attribute name="changedBy" type="xs:string" use="required"/> <!-- Name of client app -->
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="Shares">
|
<xs:complexType name="Shares">
|
||||||
<xs:sequence>
|
<xs:sequence>
|
||||||
<xs:element name="share" type="sub:Share" minOccurs="0" maxOccurs="unbounded"/>
|
<xs:element name="share" type="sub:Share" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
@ -454,6 +471,49 @@
|
|||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="ArtistInfoBase">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="biography" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="musicBrainzId" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="lastFmUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="smallImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="mediumImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="largeImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="ArtistInfo">
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="sub:ArtistInfoBase">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="similarArtist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="ArtistInfo2">
|
||||||
|
<xs:complexContent>
|
||||||
|
<xs:extension base="sub:ArtistInfoBase">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="similarArtist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="SimilarSongs">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
|
<xs:complexType name="SimilarSongs2">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="Starred2">
|
<xs:complexType name="Starred2">
|
||||||
<xs:sequence>
|
<xs:sequence>
|
||||||
<xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
<xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
@ -476,8 +536,11 @@
|
|||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="User">
|
<xs:complexType name="User">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="folder" type="xs:int" minOccurs="0" maxOccurs="unbounded"/> <!-- Added in 1.12.0 -->
|
||||||
|
</xs:sequence>
|
||||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||||
<xs:attribute name="email" type="xs:string" use="optional"/> <!-- Added in 1.6.0 -->
|
<xs:attribute name="email" type="xs:string" use="optional"/> <!-- Added in 1.6.0 -->
|
||||||
<xs:attribute name="scrobblingEnabled" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
|
<xs:attribute name="scrobblingEnabled" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
|
||||||
<xs:attribute name="adminRole" type="xs:boolean" use="required"/>
|
<xs:attribute name="adminRole" type="xs:boolean" use="required"/>
|
||||||
<xs:attribute name="settingsRole" type="xs:boolean" use="required"/>
|
<xs:attribute name="settingsRole" type="xs:boolean" use="required"/>
|
Loading…
Reference in New Issue
Block a user