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 @@ + + + - +