Two things in this commit. Functional download / matched-download behaviour is untouched — same JS handlers, same routes for the download actions, same album-expand interaction. VISUAL REDESIGN - Glass search-bar card with accent radial wash + focus ring + pill primary search button - Source chip row above the search bar (see below) - Always-visible compact filter pill row (Type / Format / Sort) — pills carry both ``bs-filter-pill`` (new visual) and ``filter-btn`` (legacy class for ``resetFilters`` + ``applyFiltersAndSort`` in wishlist-tools.js to keep working) - Accent-tinted status pill matching the dashboard / auto-sync look - Album result cards: glass card with accent left-edge stripe, 52px brand-tinted cover icon, chevron expand indicator, pill action buttons (Download / Matched Album), accent glow on hover - Track result cards: glass row with accent stripe, 44px icon, pill action buttons (Stream / Download / Matched Download) - Multi-disc separators inside expanded album track lists styled with the accent treatment - Responsive: action button columns stack vertically below 900px New CSS lives in a self-contained ``webui/static/basic-search-v2.css`` sheet linked from index.html. Selectors are scoped to ``#basic-search-section`` for any class that already exists in style.css (``.album-result-card``, ``.album-icon``, ``.track-*``, etc.); the new ``bs-*`` prefixed classes for the search bar / filters / source row / status are unscoped because they only exist in the new markup. ``!important`` is used on the card-level rules to defeat the original unscoped ``.album-result-card`` etc. rules in style.css that would otherwise leak heavyweight padding / box-shadow / 56px icon styles into the new design. Also removed ``overflow: hidden`` from the original ``.album-result-card`` and ``.track-result-card`` rules in style.css — those two classes only render in ``downloads.js`` basic search results (verified via grep, two render sites only), so the removal can't impact any other UI. SOURCE PICKER (hybrid mode) - New ``GET /api/search/sources`` endpoint returns the list of active sources from the orchestrator's chain (or the single active source in single-source mode). - Frontend renders a chip row above the search bar. Click a chip to target that source for the next search; the chip's brand accent fills. - In single-source mode the lone chip is rendered as a dashed- border label so the user always knows what they're searching but can't accidentally try to switch to sources that aren't configured. - ``/api/search`` accepts an optional ``source`` body param. When set, ``core/search/basic.py:run_basic_search`` resolves the client directly via ``orchestrator.client(source)`` and calls its ``.search()`` instead of going through the hybrid chain. - Backwards compatible: omitting ``source`` falls through to the original ``orchestrator.search()`` call exactly as before. Unknown source names also fall back to the default — typo protection. TESTS (5 new + 6 pre-existing = 11 total in test_search_basic.py) - source param routes to specific client, NOT orchestrator chain - no source param preserves original orchestrator-default behaviour - unknown source name falls back to orchestrator default - ``run_basic_soulseek_search`` backwards-compat alias preserved - source-targeted path serialises albums + tracks correctly 101 search-suite tests pass.
79 lines
2.8 KiB
Python
79 lines
2.8 KiB
Python
"""Basic download-source file search — flat list of file results sorted by quality.
|
|
|
|
Used by the basic search UI on the Search page and by ``/api/search``.
|
|
|
|
``run_basic_search`` replaced ``run_basic_soulseek_search`` so the caller
|
|
can target any active download source (not just slskd). The old name is
|
|
kept as a thin alias for backwards compat with any callers outside this
|
|
module.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Callable, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def run_basic_search(
|
|
query: str,
|
|
download_orchestrator,
|
|
run_async: Callable,
|
|
*,
|
|
source: Optional[str] = None,
|
|
) -> list[dict]:
|
|
"""Search ``source`` (or the active/first hybrid source) for ``query``.
|
|
|
|
Returns dicts with ``result_type`` set to ``"album"`` or ``"track"``
|
|
and sorted by ``quality_score`` descending. Empty list on any failure.
|
|
|
|
Parameters
|
|
----------
|
|
source:
|
|
Optional source name to override the orchestrator's default selection.
|
|
Must be a canonical name from ``DownloadPluginRegistry`` (e.g.
|
|
``"soulseek"``, ``"tidal"``, ``"qobuz"``). When ``None``, behaviour
|
|
is unchanged from before: orchestrator.search() picks the active
|
|
source (single mode) or the first in chain (hybrid).
|
|
"""
|
|
if source and download_orchestrator:
|
|
# Target a specific source: resolve the client and call search()
|
|
# directly instead of going through the orchestrator chain.
|
|
try:
|
|
client = download_orchestrator.client(source)
|
|
except Exception as exc:
|
|
logger.warning("basic search: could not resolve client for %r: %s", source, exc)
|
|
client = None
|
|
|
|
if client is None:
|
|
logger.warning("basic search: no client for source %r — falling back to orchestrator", source)
|
|
tracks, albums = run_async(download_orchestrator.search(query))
|
|
else:
|
|
logger.info("basic search: targeting %r for %r", source, query)
|
|
tracks, albums = run_async(client.search(query))
|
|
else:
|
|
tracks, albums = run_async(download_orchestrator.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,
|
|
)
|
|
|
|
|
|
# Backwards-compat alias for any callers that haven't been updated yet.
|
|
run_basic_soulseek_search = run_basic_search
|