diff --git a/README.md b/README.md
index 7cfcc06..f7f2e49 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Current supported features are:
* [Last.fm][lastfm] scrobbling
* 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].
[subsonic]: http://www.subsonic.org/
diff --git a/docs/api.rst b/docs/api.rst
index dd3e40e..a1c51e9 100644
--- a/docs/api.rst
+++ b/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
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
`Subsonic API page`__.
@@ -227,7 +227,7 @@ Browsing
================= ====== =
Parameter Vers.
================= ====== =
- ``musicFolderId`` 1.14.0 📅
+ ``musicFolderId`` 1.14.0 ✔️
================= ====== =
.. _getArtist:
@@ -418,7 +418,7 @@ Album/song lists
``fromYear`` ✔️
``toYear`` ✔️
``genre`` ✔️
- ``musicFolderId`` 1.12.0 📅
+ ``musicFolderId`` 1.12.0 ✔️
================= ====== =
.. versionadded:: 1.10.1
@@ -441,7 +441,7 @@ Album/song lists
``fromYear`` ✔️
``toYear`` ✔️
``genre`` ✔️
- ``musicFolderId`` 1.12.0 📅
+ ``musicFolderId`` 1.12.0 ✔️
================= ====== =
.. versionadded:: 1.10.1
@@ -479,7 +479,7 @@ Album/song lists
``genre`` 1.9.0 ✔️
``count`` 1.9.0 ✔️
``offset`` 1.9.0 ✔️
- ``musicFolderId`` 1.12.0 📅
+ ``musicFolderId`` 1.12.0 ✔️
================= ====== =
.. _getNowPlaying:
@@ -500,7 +500,7 @@ Album/song lists
================= ====== =
Parameter Vers.
================= ====== =
- ``musicFolderId`` 1.12.0 📅
+ ``musicFolderId`` 1.12.0 ✔️
================= ====== =
.. _getStarred2:
@@ -514,7 +514,7 @@ Album/song lists
================= ====== =
Parameter Vers.
================= ====== =
- ``musicFolderId`` 1.12.0 📅
+ ``musicFolderId`` 1.12.0 ✔️
================= ====== =
Searching
@@ -558,7 +558,7 @@ Searching
``albumOffset`` ✔️
``songCount`` ✔️
``songOffset`` ✔️
- ``musicFolderId`` 1.12.0 📅
+ ``musicFolderId`` 1.12.0 ✔️
================= ====== =
.. _search3:
@@ -579,7 +579,7 @@ Searching
``albumOffset`` ✔️
``songCount`` ✔️
``songOffset`` ✔️
- ``musicFolderId`` 1.12.0 📅
+ ``musicFolderId`` 1.12.0 ✔️
================= ====== =
Playlists
diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py
index ddbdf9e..e40e65b 100644
--- a/supysonic/api/__init__.py
+++ b/supysonic/api/__init__.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# 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.
@@ -17,7 +17,7 @@ from pony.orm import commit
from ..db import ClientPrefs, Folder
from ..managers.user import UserManager
-from .exceptions import GenericError, Unauthorized
+from .exceptions import GenericError, Unauthorized, NotFound
from .formatters import JSONFormatter, JSONPFormatter, XMLFormatter
api = Blueprint("api", __name__)
@@ -119,6 +119,22 @@ def get_entity_id(cls, eid):
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 .system import *
diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py
index f71295b..25e9ed3 100644
--- a/supysonic/api/albums_songs.py
+++ b/supysonic/api/albums_songs.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# 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.
@@ -21,8 +21,8 @@ from ..db import (
)
from ..db import now
-from . import api_routing
-from .exceptions import GenericError, NotFound
+from . import api_routing, get_root_folder
+from .exceptions import GenericError
@api_routing("/getRandomSongs")
@@ -35,12 +35,7 @@ def rand_songs():
size = int(size) if size else 10
fromYear = int(fromYear) if fromYear else None
toYear = int(toYear) if toYear else None
- fid = None
- if musicFolderId:
- try:
- fid = int(musicFolderId)
- except ValueError:
- raise ValueError("Invalid folder ID")
+ root = get_root_folder(musicFolderId)
query = Track.select()
if fromYear:
@@ -49,11 +44,8 @@ def rand_songs():
query = query.filter(lambda t: t.year <= toYear)
if genre:
query = query.filter(lambda t: t.genre == genre)
- if fid:
- if not Folder.exists(id=fid, root=True):
- raise NotFound("Folder")
-
- query = query.filter(lambda t: t.root_folder.id == fid)
+ if root:
+ query = query.filter(lambda t: t.root_folder == root)
return request.formatter(
"randomSongs",
@@ -70,11 +62,15 @@ def rand_songs():
def album_list():
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
offset = int(offset) if offset else 0
+ root = get_root_folder(mfid)
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":
return request.formatter(
"albumList",
@@ -94,13 +90,18 @@ def album_list():
elif ltype == "recent":
query = select(
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":
query = select(
s.starred
for s in StarredFolder
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":
query = query.sort_by(Folder.name).distinct()
elif ltype == "alphabeticalByArtist":
@@ -135,11 +136,15 @@ def album_list():
def album_list_id3():
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
offset = int(offset) if offset else 0
+ root = get_root_folder(mfid)
query = Album.select()
+ if root is not None:
+ query = query.where(lambda a: root in a.tracks.root_folder)
+
if ltype == "random":
return request.formatter(
"albumList2",
@@ -150,11 +155,13 @@ def album_list_id3():
elif ltype == "frequent":
query = query.order_by(lambda a: desc(avg(a.tracks.play_count)))
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))
)
elif ltype == "starred":
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":
query = query.order_by(Album.name)
elif ltype == "alphabeticalByArtist":
@@ -191,14 +198,22 @@ def album_list_id3():
def songs_by_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
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(
"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")
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)
+ 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(
"starred",
{
- "artist": [
- sf.as_subsonic_artist(request.user)
- for sf in folders.filter(lambda f: count(f.tracks) == 0)
- ],
- "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
- )
- ],
+ "artist": [sf.as_subsonic_artist(request.user) for sf in arq],
+ "album": [sf.as_subsonic_child(request.user) for sf in alq],
+ "song": [st.as_subsonic_child(request.user, request.client) for st in trq],
},
)
@api_routing("/getStarred2")
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(
"starred2",
{
- "artist": [
- sa.as_subsonic_artist(request.user)
- for sa in select(
- 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
- )
- ],
+ "artist": [sa.as_subsonic_artist(request.user) for sa in arq],
+ "album": [sa.as_subsonic_album(request.user) for sa in alq],
+ "song": [st.as_subsonic_child(request.user, request.client) for st in trq],
},
)
diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py
index e278a39..c25746b 100644
--- a/supysonic/api/browse.py
+++ b/supysonic/api/browse.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# 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.
@@ -9,11 +9,11 @@ import re
import string
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 . import get_entity, get_entity_id, api_routing
+from . import get_entity, get_root_folder, api_routing
@api_routing("/getMusicFolders")
@@ -80,12 +80,7 @@ def list_indexes():
if musicFolderId is None:
folders = Folder.select(lambda f: f.root)[:]
else:
- mfid = get_entity_id(Folder, musicFolderId)
- folder = Folder[mfid]
- if not folder.root:
- raise ObjectNotFound(Folder, mfid)
-
- folders = [folder]
+ folders = [get_root_folder(musicFolderId)]
last_modif = max(f.last_scan for f in folders)
if ifModifiedSince is not None and last_modif < ifModifiedSince:
@@ -153,8 +148,14 @@ def list_genres():
@api_routing("/getArtists")
def list_artists():
- # According to the API page, there are no parameters?
- indexes = build_indexes(Artist.select())
+ mfid = request.values.get("musicFolderId")
+
+ 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(
"artists",
{
diff --git a/supysonic/api/search.py b/supysonic/api/search.py
index 4091e75..605acda 100644
--- a/supysonic/api/search.py
+++ b/supysonic/api/search.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# 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.
@@ -12,7 +12,7 @@ from pony.orm import select
from ..db import Folder, Track, Artist, Album
-from . import api_routing
+from . import api_routing, get_root_folder
from .exceptions import MissingParameter
@@ -93,6 +93,7 @@ def new_search():
album_offset,
song_count,
song_offset,
+ mfid,
) = map(
request.values.get,
(
@@ -102,6 +103,7 @@ def new_search():
"albumOffset",
"songCount",
"songOffset",
+ "musicFolderId",
),
)
@@ -111,14 +113,20 @@ def new_search():
album_offset = int(album_offset) if album_offset else 0
song_count = int(song_count) if song_count else 20
song_offset = int(song_offset) if song_offset else 0
+ root = get_root_folder(mfid)
- artists = select(
- t.folder.parent for t in Track if query in t.folder.parent.name
- ).limit(artist_count, artist_offset)
- albums = select(t.folder for t in Track if query in t.folder.name).limit(
- album_count, album_offset
- )
- songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
+ artists = select(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)
+ songs = Track.select(lambda t: query in t.title)
+
+ if root is not None:
+ artists = artists.where(lambda t: t.root_folder == root)
+ 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(
"searchResult2",
@@ -145,6 +153,7 @@ def search_id3():
album_offset,
song_count,
song_offset,
+ mfid,
) = map(
request.values.get,
(
@@ -154,6 +163,7 @@ def search_id3():
"albumOffset",
"songCount",
"songOffset",
+ "musicFolderId",
),
)
@@ -163,12 +173,20 @@ def search_id3():
album_offset = int(album_offset) if album_offset else 0
song_count = int(song_count) if song_count else 20
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(
- artist_count, artist_offset
- )
- 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)
+ artists = Artist.select(lambda a: query in a.name)
+ albums = Album.select(lambda a: query in a.name)
+ songs = Track.select(lambda t: query in t.title)
+
+ 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(
"searchResult3",
diff --git a/tests/api/apitestbase.py b/tests/api/apitestbase.py
index 6f9ab50..7cca930 100644
--- a/tests/api/apitestbase.py
+++ b/tests/api/apitestbase.py
@@ -20,7 +20,7 @@ NSMAP = {"sub": NS}
class ApiTestBase(TestBase):
__with_api__ = True
- def setUp(self, apiVersion="1.10.2"):
+ def setUp(self, apiVersion="1.12.0"):
super().setUp()
self.apiVersion = apiVersion
xsd = etree.parse(
diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py
index c5da99b..6e0302c 100644
--- a/tests/api/test_album_songs.py
+++ b/tests/api/test_album_songs.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# 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.
@@ -23,6 +23,7 @@ class AlbumSongsTestCase(ApiTestBase):
with db_session:
folder = Folder(name="Root", root=True, path="tests/assets")
+ empty = Folder(name="Root", root=True, path="/tmp")
artist = Artist(name="Artist")
album = Album(name="Album", artist=artist)
@@ -70,6 +71,12 @@ class AlbumSongsTestCase(ApiTestBase):
error=0,
)
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 = [
("random", 1),
@@ -120,8 +127,21 @@ class AlbumSongsTestCase(ApiTestBase):
)
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:
- Folder.get().delete()
+ Folder[1].delete()
rv, child = self._make_request(
"getAlbumList", {"type": "random"}, tag="albumList"
)
@@ -143,6 +163,12 @@ class AlbumSongsTestCase(ApiTestBase):
error=0,
)
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 = [
"random",
@@ -192,6 +218,19 @@ class AlbumSongsTestCase(ApiTestBase):
)
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:
Track.select().delete()
Album.get().delete()
@@ -211,15 +250,13 @@ class AlbumSongsTestCase(ApiTestBase):
"getRandomSongs", tag="randomSongs", skip_post=True
)
- with db_session:
- fid = Folder.get().id
self._make_request(
"getRandomSongs",
{
"fromYear": -52,
"toYear": "1984",
"genre": "some cryptic subgenre youve never heard of",
- "musicFolderId": fid,
+ "musicFolderId": 1,
},
tag="randomSongs",
)
@@ -229,9 +266,11 @@ class AlbumSongsTestCase(ApiTestBase):
def test_get_starred(self):
self._make_request("getStarred", tag="starred")
+ self._make_request("getStarred", {"musicFolderId": 1}, tag="starred")
def test_get_starred2(self):
self._make_request("getStarred2", tag="starred2")
+ self._make_request("getStarred2", {"musicFolderId": 1}, tag="starred2")
if __name__ == "__main__":
diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py
index c976f6f..bacb5bb 100644
--- a/tests/api/test_browse.py
+++ b/tests/api/test_browse.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# 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.
@@ -21,14 +21,14 @@ class BrowseTestCase(ApiTestBase):
super().setUp()
with db_session:
- Folder(root=True, name="Empty root", path="/tmp")
- root = Folder(root=True, name="Root folder", path="tests/assets")
+ self.empty_root = Folder(root=True, name="Empty root", path="/tmp")
+ self.root = Folder(root=True, name="Root folder", path="tests/assets")
for letter in "ABC":
folder = Folder(
name=letter + "rtist",
path="tests/assets/{}rtist".format(letter),
- parent=root,
+ parent=self.root,
)
artist = Artist(name=letter + "rtist")
@@ -56,7 +56,7 @@ class BrowseTestCase(ApiTestBase):
letter, lether, song
),
last_modification=0,
- root_folder=root,
+ root_folder=self.root,
folder=afolder,
)
@@ -132,13 +132,26 @@ class BrowseTestCase(ApiTestBase):
# same as getIndexes standard case
# 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)
for i, letter in enumerate(["A", "B", "C"]):
self.assertEqual(child[i].get("name"), letter)
self.assertEqual(len(child[i]), 1)
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):
# dataset should be improved to have tracks by a different artist than the album's artist
self._make_request("getArtist", error=10)
diff --git a/tests/api/test_search.py b/tests/api/test_search.py
index 77bece2..af63391 100644
--- a/tests/api/test_search.py
+++ b/tests/api/test_search.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# 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.
@@ -21,6 +21,7 @@ class SearchTestCase(ApiTestBase):
with db_session:
root = Folder(root=True, name="Root folder", path="tests/assets")
+ Folder(root=True, name="Empty", path="/tmp")
for letter in "ABC":
folder = Folder(
@@ -58,7 +59,7 @@ class SearchTestCase(ApiTestBase):
commit()
- self.assertEqual(Folder.select().count(), 10)
+ self.assertEqual(Folder.select().count(), 11)
self.assertEqual(Artist.select().count(), 3)
self.assertEqual(Album.select().count(), 6)
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", "songCount": "string"}, 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
self._make_request("search2", error=10)
@@ -288,6 +293,17 @@ class SearchTestCase(ApiTestBase):
self.assertNotIn(song, songs)
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
# to have folders that don't share names with artists or albums
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", "songCount": "string"}, 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
self._make_request("search3", error=10)
@@ -392,6 +412,17 @@ class SearchTestCase(ApiTestBase):
self.assertNotIn(song, songs)
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__":
unittest.main()
diff --git a/tests/assets/subsonic-rest-api-1.10.2.xsd b/tests/assets/subsonic-rest-api-1.12.0.xsd
similarity index 86%
rename from tests/assets/subsonic-rest-api-1.10.2.xsd
rename to tests/assets/subsonic-rest-api-1.12.0.xsd
index a409c45..6104348 100644
--- a/tests/assets/subsonic-rest-api-1.10.2.xsd
+++ b/tests/assets/subsonic-rest-api-1.12.0.xsd
@@ -4,7 +4,7 @@
targetNamespace="http://subsonic.org/restapi"
attributeFormDefault="unqualified"
elementFormDefault="qualified"
- version="1.10.2">
+ version="1.12.0">
@@ -39,9 +39,14 @@
+
+
+
+
+
@@ -287,11 +292,12 @@
-
+
+
@@ -426,6 +432,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -454,6 +471,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -476,8 +536,11 @@
+
+
+
-
+