Routes moved to thin parse-args/jsonify handlers; logic now lives in six focused modules under core/search/. 720 lines deleted from web_server.py; 109 added back as wrappers; ~700 lines of new core code plus ~700 lines of tests. Module split: - core/search/cache.py — TTL+LRU cache for enhanced-search responses, keyed by (query, active_server, fallback_source, hydrabase_active, source_tag) so config changes don't poison stale entries. - core/search/sources.py — per-kind metadata search (artists/albums/ tracks) and the multi-kind ThreadPoolExecutor that fans them out. - core/search/library_check.py — library + wishlist presence check with Plex thumb URL resolution; profile-aware wishlist with legacy fallback for older DBs missing the profile_id column. - core/search/stream.py — single-track preview search; effective stream mode resolution, query-variant generation, retry walk, matching engine integration. - core/search/basic.py — flat Soulseek file search, quality-sorted. - core/search/orchestrator.py — main enhanced-search dispatch (short-query fast path, single-source bypass, hydrabase-primary fan out, alternate source list builder), NDJSON streaming generator for /source/<src>, and the SearchDeps dataclass that bundles the cross-cutting deps. Routes pass clients (spotify, hydrabase, hydrabase_worker, soulseek) and helpers (config_manager, fix_artist_image_url, _is_hydrabase_active, _get_metadata_fallback_*, _run_background_ comparison, run_async, dev_mode_enabled_provider) into core/search via a SearchDeps bundle built per-request. fix_artist_image_url stays in web_server.py because it touches 31 other call sites. Behavior preserved 1:1: - Same response shapes (db_artists, spotify_artists, spotify_albums, spotify_tracks, primary_source, metadata_source, alternate_sources, source_available) - Same NDJSON line ordering (artists/albums/tracks as they finish, plus done marker) - Same per-kind exception swallowing - Same hydrabase-worker mirror on dev mode - Same cache key shape (5-tuple) and TTL/LRU semantics - Same stream-track effective-mode resolution including the Soulseek-coerce-to-YouTube edge case - Same library-check Plex thumb URL rewriting and wishlist fallback for older DBs Tests: 94 new (cache TTL/LRU/key, sources happy/partial/all-fail, library presence with library + wishlist + thumbs, stream effective mode + query gen + retry, orchestrator client resolution + short query + single source + fan-out alternates + hydrabase primary + NDJSON drain). Full suite: 788 passing (was 694). Ruff clean.
44 lines
1.3 KiB
Python
44 lines
1.3 KiB
Python
"""Basic Soulseek file search — flat list of file results sorted by quality.
|
|
|
|
Used by the Soulseek source icon in the unified search UI and by direct
|
|
/api/search calls. Synchronous wrapper around the async soulseek client.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Callable
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def run_basic_soulseek_search(
|
|
query: str,
|
|
soulseek_client,
|
|
run_async: Callable,
|
|
) -> list[dict]:
|
|
"""Search Soulseek for `query`, normalize albums + tracks to one sorted list.
|
|
|
|
Returns dicts with `result_type` set to "album" or "track" and sorted by
|
|
`quality_score` descending. Empty list on any failure (caller logs).
|
|
"""
|
|
tracks, albums = run_async(soulseek_client.search(query))
|
|
|
|
processed_albums = []
|
|
for album in albums:
|
|
album_dict = album.__dict__.copy()
|
|
album_dict['tracks'] = [track.__dict__ for track in album.tracks]
|
|
album_dict['result_type'] = 'album'
|
|
processed_albums.append(album_dict)
|
|
|
|
processed_tracks = []
|
|
for track in tracks:
|
|
track_dict = track.__dict__.copy()
|
|
track_dict['result_type'] = 'track'
|
|
processed_tracks.append(track_dict)
|
|
|
|
return sorted(
|
|
processed_albums + processed_tracks,
|
|
key=lambda x: x.get('quality_score', 0),
|
|
reverse=True,
|
|
)
|