[bug] Fix issue with search
Some checks failed
CI / Build (push) Failing after 4m59s
CI / pyright (push) Failing after 7s
CI / pytest (3.13) (push) Failing after 6s
CI / ruff check (push) Failing after 5s
CI / ruff format (push) Failing after 8s
CI / pytest (3.14) (push) Failing after 4s

This commit is contained in:
2026-06-24 20:49:33 -04:00
parent 5e6fe9df15
commit c35e5a4f44
4 changed files with 85 additions and 29 deletions

View File

@ -21,6 +21,7 @@ class Extension(ext.Extension):
schema["artists"] = config.String(optional=True)
schema["playlist_prefix"] = config.String(optional=True)
schema["refresh_interval"] = config.Integer(optional=True, minimum=0)
schema["search_uris"] = config.List(optional=True)
return schema
def validate_environment(self) -> None:

View File

@ -15,3 +15,7 @@ artists =
playlist_prefix = [Smart]
# Refresh interval in hours (0 = only on startup)
refresh_interval = 0
# Comma-separated list of URI schemes to restrict searches to.
# Example: local:, file:
# Leave empty to search all backends.
search_uris =

View File

@ -19,20 +19,24 @@ def parse_decade(decade_str: str) -> str:
return decade_str
def _search(core: CoreProxy, field: str, value: str) -> list[Track]:
def _search(
core: CoreProxy, field: str, value: str, uris: list[Uri] | None = None,
) -> list[Track]:
query = cast("Query[SearchField]", {field: [value]})
try:
result = core.library.search(query).get()
result = core.library.search(query, uris=uris).get()
except Exception:
logger.exception("Search failed for %s=%s", field, value)
return []
return _extract_tracks(result)
def build_decade_mix(core: CoreProxy, decade: str) -> list[Track]:
def build_decade_mix(
core: CoreProxy, decade: str, uris: list[Uri] | None = None,
) -> list[Track]:
query = cast("Query[SearchField]", {"date": [parse_decade(decade)]})
try:
result = core.library.search(query).get()
result = core.library.search(query, uris=uris).get()
except Exception:
logger.exception("Decade search failed for %s", decade)
return []
@ -41,12 +45,16 @@ def build_decade_mix(core: CoreProxy, decade: str) -> list[Track]:
return tracks
def build_genre_mix(core: CoreProxy, genre: str) -> list[Track]:
return _search(core, "genre", genre)
def build_genre_mix(
core: CoreProxy, genre: str, uris: list[Uri] | None = None,
) -> list[Track]:
return _search(core, "genre", genre, uris=uris)
def build_artist_mix(core: CoreProxy, artist: str) -> list[Track]:
return _search(core, "artist", artist)
def build_artist_mix(
core: CoreProxy, artist: str, uris: list[Uri] | None = None,
) -> list[Track]:
return _search(core, "artist", artist, uris=uris)
def build_album_mix(core: CoreProxy, album_uri: str) -> list[Track]:
@ -60,7 +68,9 @@ def build_album_mix(core: CoreProxy, album_uri: str) -> list[Track]:
return flat
def build_instant_mix(core: CoreProxy, track_uri: str, limit: int = 50) -> list[Track]:
def build_instant_mix(
core: CoreProxy, track_uri: str, limit: int = 50, uris: list[Uri] | None = None,
) -> list[Track]:
try:
lookup_result = core.library.lookup(cast("list[Uri]", [track_uri])).get()
except Exception:
@ -85,7 +95,7 @@ def build_instant_mix(core: CoreProxy, track_uri: str, limit: int = 50) -> list[
for genre in genres:
g_query = cast("Query[SearchField]", {"genre": [genre]})
try:
genre_result = core.library.search(g_query).get()
genre_result = core.library.search(g_query, uris=uris).get()
except Exception:
logger.exception("Genre search failed for %s", genre)
continue
@ -99,7 +109,7 @@ def build_instant_mix(core: CoreProxy, track_uri: str, limit: int = 50) -> list[
break
a_query = cast("Query[SearchField]", {"artist": [artist]})
try:
artist_result = core.library.search(a_query).get()
artist_result = core.library.search(a_query, uris=uris).get()
except Exception:
logger.exception("Artist search failed for %s", artist)
continue
@ -149,13 +159,17 @@ def save_smart_playlist(
def refresh_smart_playlists(core: CoreProxy, config_dict: dict) -> None:
prefix = config_dict.get("playlist_prefix", "[Smart]")
raw = config_dict.get("search_uris", "")
uris: list[Uri] | None = None
if raw:
uris = cast("list[Uri]", [u.strip() for u in raw.split(",") if u.strip()])
decades_raw = config_dict.get("decades", "")
if decades_raw:
decades = [d.strip() for d in decades_raw.split(",") if d.strip()]
for decade in decades:
try:
tracks = build_decade_mix(core, decade)
tracks = build_decade_mix(core, decade, uris=uris)
except Exception:
logger.exception("Failed to build decade mix for %s", decade)
continue
@ -167,7 +181,7 @@ def refresh_smart_playlists(core: CoreProxy, config_dict: dict) -> None:
genres = [d.strip() for d in genres_raw.split(",") if d.strip()]
for genre in genres:
try:
tracks = build_genre_mix(core, genre)
tracks = build_genre_mix(core, genre, uris=uris)
except Exception:
logger.exception("Failed to build genre mix for %s", genre)
continue
@ -179,7 +193,7 @@ def refresh_smart_playlists(core: CoreProxy, config_dict: dict) -> None:
artists = [a.strip() for a in artists_raw.split(",") if a.strip()]
for artist in artists:
try:
tracks = build_artist_mix(core, artist)
tracks = build_artist_mix(core, artist, uris=uris)
except Exception:
logger.exception("Failed to build artist mix for %s", artist)
continue

View File

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, cast
import tornado.web
if TYPE_CHECKING:
from mopidy_smartplaylists.compat import Config, CoreProxy
from mopidy_smartplaylists.compat import Config, CoreProxy, Uri
if TYPE_CHECKING:
from mopidy.models import Playlist
@ -26,9 +26,12 @@ logger = logging.getLogger(__name__)
class DecadeMixHandler(tornado.web.RequestHandler):
def initialize(self, core: CoreProxy, prefix: str) -> None:
def initialize(
self, core: CoreProxy, prefix: str, uris: list[Uri] | None = None,
) -> None:
self.core = core
self.prefix = prefix
self.uris = uris
def post(self) -> None:
data = json.loads(self.request.body)
@ -37,7 +40,7 @@ class DecadeMixHandler(tornado.web.RequestHandler):
self.set_status(400)
self.write({"error": "Missing 'decade' in request body"})
return
tracks = build_decade_mix(self.core, decade)
tracks = build_decade_mix(self.core, decade, uris=self.uris)
if not tracks:
self.write({"playlist": None, "tracks": 0})
return
@ -54,9 +57,12 @@ class DecadeMixHandler(tornado.web.RequestHandler):
class GenreMixHandler(tornado.web.RequestHandler):
def initialize(self, core: CoreProxy, prefix: str) -> None:
def initialize(
self, core: CoreProxy, prefix: str, uris: list[Uri] | None = None,
) -> None:
self.core = core
self.prefix = prefix
self.uris = uris
def post(self) -> None:
data = json.loads(self.request.body)
@ -65,7 +71,7 @@ class GenreMixHandler(tornado.web.RequestHandler):
self.set_status(400)
self.write({"error": "Missing 'genre' in request body"})
return
tracks = build_genre_mix(self.core, genre)
tracks = build_genre_mix(self.core, genre, uris=self.uris)
if not tracks:
self.write({"playlist": None, "tracks": 0})
return
@ -82,9 +88,12 @@ class GenreMixHandler(tornado.web.RequestHandler):
class ArtistMixHandler(tornado.web.RequestHandler):
def initialize(self, core: CoreProxy, prefix: str) -> None:
def initialize(
self, core: CoreProxy, prefix: str, uris: list[Uri] | None = None,
) -> None:
self.core = core
self.prefix = prefix
self.uris = uris
def post(self) -> None:
data = json.loads(self.request.body)
@ -93,7 +102,7 @@ class ArtistMixHandler(tornado.web.RequestHandler):
self.set_status(400)
self.write({"error": "Missing 'artist' in request body"})
return
tracks = build_artist_mix(self.core, artist)
tracks = build_artist_mix(self.core, artist, uris=self.uris)
if not tracks:
self.write({"playlist": None, "tracks": 0})
return
@ -143,9 +152,12 @@ class AlbumMixHandler(tornado.web.RequestHandler):
class InstantMixHandler(tornado.web.RequestHandler):
def initialize(self, core: CoreProxy, prefix: str) -> None:
def initialize(
self, core: CoreProxy, prefix: str, uris: list[Uri] | None = None,
) -> None:
self.core = core
self.prefix = prefix
self.uris = uris
def post(self) -> None:
data = json.loads(self.request.body)
@ -155,12 +167,17 @@ class InstantMixHandler(tornado.web.RequestHandler):
self.set_status(400)
self.write({"error": "Missing 'uri' in request body"})
return
tracks = build_instant_mix(self.core, track_uri, limit)
tracks = build_instant_mix(self.core, track_uri, limit, uris=self.uris)
if not tracks:
self.write({"playlist": None, "tracks": 0})
return
lookup_result = self.core.library.lookup([track_uri]).get()
try:
lookup_result = self.core.library.lookup([track_uri]).get()
except Exception:
logger.exception("Track lookup failed for %s", track_uri)
self.write({"playlist": None, "tracks": 0})
return
seed_name = "Instant Mix"
for uri_tracks in lookup_result.values():
for t in uri_tracks:
@ -204,7 +221,17 @@ class StatusHandler(tornado.web.RequestHandler):
self.core = core
def get(self) -> None:
result = self.core.playlists.as_list().get()
try:
result = self.core.playlists.as_list().get()
except Exception:
logger.exception("Failed to list playlists")
self.set_status(500)
self.write({
"error": "Failed to list playlists",
"smart_playlists": [],
"count": 0,
})
return
result_typed = cast("list[Playlist]", result)
smart = [p for p in result_typed if p.uri and "smartplaylists" in p.uri]
self.write(
@ -222,15 +249,25 @@ class StatusHandler(tornado.web.RequestHandler):
)
def _parse_search_uris(config: Config) -> list[Uri] | None:
raw = config.get("smartplaylists", {}).get("search_uris", "")
if raw:
uris = cast("list[Uri]", [u.strip() for u in raw.split(",") if u.strip()])
return uris or None
return None
def app_factory(config: Config, core: CoreProxy) -> list[tuple]:
prefix = config.get("smartplaylists", {}).get("playlist_prefix", "[Smart]")
uris = _parse_search_uris(config)
return [
(r"/decade", DecadeMixHandler, {"core": core, "prefix": prefix}),
(r"/genre", GenreMixHandler, {"core": core, "prefix": prefix}),
(r"/artist", ArtistMixHandler, {"core": core, "prefix": prefix}),
(r"/decade", DecadeMixHandler, {"core": core, "prefix": prefix, "uris": uris}),
(r"/genre", GenreMixHandler, {"core": core, "prefix": prefix, "uris": uris}),
(r"/artist", ArtistMixHandler, {"core": core, "prefix": prefix, "uris": uris}),
(r"/album", AlbumMixHandler, {"core": core, "prefix": prefix}),
(r"/instant-mix", InstantMixHandler, {"core": core, "prefix": prefix}),
(r"/instant-mix", InstantMixHandler,
{"core": core, "prefix": prefix, "uris": uris}),
(r"/refresh", RefreshHandler, {"core": core, "config": config}),
(r"/status", StatusHandler, {"core": core}),
]