diff --git a/web_server.py b/web_server.py
index d2e879b4..40052fc3 100644
--- a/web_server.py
+++ b/web_server.py
@@ -37,7 +37,7 @@ _log_dir = Path(_log_path).parent
logger = setup_logging(_log_level, _log_path)
# App version — single source of truth for backup metadata, version-info endpoint, etc.
-_SOULSYNC_BASE_VERSION = "2.41"
+_SOULSYNC_BASE_VERSION = "2.42"
def _build_version_string():
"""Append short commit hash to version when available (e.g. 2.35+abc1234)."""
@@ -22809,6 +22809,19 @@ def get_version_info():
"title": "What's New in SoulSync",
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"sections": [
+ {
+ "title": "Search Source Picker — Pick Where You're Searching",
+ "description": "The Search page's Enhanced/Basic toggle is replaced by a single 'Search from' dropdown so you can explicitly pick which source to query instead of fanning out to every provider",
+ "features": [
+ "• Choose from: All sources (Auto), Spotify, Apple Music, Deezer, Discogs, Hydrabase, MusicBrainz, or Soulseek (raw files)",
+ "• Auto keeps today's multi-source fan-out behavior — no change if you want the current results",
+ "• Picking a specific source hits only that provider — no more surprise Spotify rate-limit hits from flows that didn't need Spotify",
+ "• 'Soulseek' routes to the raw-file search (what 'Basic' used to do) — one picker now covers both modes",
+ "• Loading text shows the selected source (e.g., 'Searching across Apple Music and your library...')",
+ "• Phase 3 of the Search/Artists unification project — builds on the shared fetch helper from 2.41",
+ ],
+ "usage_note": "Sidebar → Search → pick a source in the 'Search from' dropdown above the search bar."
+ },
{
"title": "Shared Enhanced-Search Fetch Helper",
"description": "Internal refactor — the Search page and the global search widget now route through one shared fetch helper, so future source-picker work only needs wiring in one place",
diff --git a/webui/index.html b/webui/index.html
index 10c63f68..08f4b85c 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -1869,18 +1869,21 @@
-
-
-
-
-
-
+
+
+
+
+
+ ▾
diff --git a/webui/static/helper.js b/webui/static/helper.js
index eff92aec..9ee59750 100644
--- a/webui/static/helper.js
+++ b/webui/static/helper.js
@@ -3599,6 +3599,11 @@ function closeHelperSearch() {
// ═══════════════════════════════════════════════════════════════════════════
const WHATS_NEW = {
+ '2.42': [
+ // --- April 23, 2026 ---
+ { date: 'April 23, 2026' },
+ { title: 'Search Source Picker — Pick Where You\'re Searching', desc: 'The Search page\'s Enhanced/Basic toggle is replaced by a single "Search from" dropdown at the top. Choose All sources (Auto — keeps today\'s multi-source fan-out), Spotify, Apple Music, Deezer, Discogs, Hydrabase, MusicBrainz, or Soulseek (raw files). Picking a specific source hits only that provider — no more surprise Spotify rate-limit hits from flows that didn\'t need Spotify. "Soulseek" routes to the raw-file search (what "Basic" used to do), so one picker now covers both modes. Loading text reflects the selected source (e.g., "Searching across Apple Music..."). Phase 3 of the Search/Artists unification project', page: 'downloads' },
+ ],
'2.41': [
// --- April 22, 2026 (late night) ---
{ date: 'April 22, 2026 (late night)' },
diff --git a/webui/static/search.js b/webui/static/search.js
index cbf19504..c6b38573 100644
--- a/webui/static/search.js
+++ b/webui/static/search.js
@@ -56,41 +56,38 @@ function initializeSearchModeToggle() {
return;
}
- const toggleContainer = document.querySelector('.search-mode-toggle');
- const modeBtns = document.querySelectorAll('.search-mode-btn');
+ const sourceSelect = document.getElementById('search-source-select');
const basicSection = document.getElementById('basic-search-section');
const enhancedSection = document.getElementById('enhanced-search-section');
- if (!toggleContainer || !modeBtns.length || !basicSection || !enhancedSection) {
- console.warn('Search mode toggle elements not found');
+ if (!sourceSelect || !basicSection || !enhancedSection) {
+ console.warn('Search source picker elements not found');
return;
}
searchModeToggleInitialized = true;
- console.log('✅ Initializing search mode toggle (first time only)');
+ console.log('✅ Initializing search source picker (first time only)');
- modeBtns.forEach(btn => {
- btn.addEventListener('click', () => {
- const mode = btn.dataset.mode;
+ // Current source selection — 'auto' (fan-out) by default. Soulseek routes
+ // to the raw-file basic search; everything else routes to enhanced.
+ let currentSearchSource = sourceSelect.value || 'auto';
- // Update button active states
- modeBtns.forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
+ const applySourceSelection = (value) => {
+ currentSearchSource = value;
+ if (value === 'soulseek') {
+ basicSection.classList.add('active');
+ enhancedSection.classList.remove('active');
+ } else {
+ basicSection.classList.remove('active');
+ enhancedSection.classList.add('active');
+ }
+ };
- // Update toggle slider position
- toggleContainer.setAttribute('data-active', mode);
+ applySourceSelection(currentSearchSource);
- // Toggle sections
- if (mode === 'basic') {
- basicSection.classList.add('active');
- enhancedSection.classList.remove('active');
- console.log('Switched to basic search mode');
- } else {
- basicSection.classList.remove('active');
- enhancedSection.classList.add('active');
- console.log('Switched to enhanced search mode');
- }
- });
+ sourceSelect.addEventListener('change', (e) => {
+ applySourceSelection(e.target.value);
+ console.log('Search source →', currentSearchSource);
});
// Initialize enhanced search
@@ -221,7 +218,14 @@ function initializeSearchModeToggle() {
showDropdown();
const loadingText = document.getElementById('enhanced-loading-text');
if (loadingText) {
- loadingText.textContent = `Searching across ${currentMusicSourceName} and your library...`;
+ const _sourceLabelMap = {
+ spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer',
+ discogs: 'Discogs', hydrabase: 'Hydrabase', musicbrainz: 'MusicBrainz',
+ };
+ const _sourceName = currentSearchSource && currentSearchSource !== 'auto'
+ ? (_sourceLabelMap[currentSearchSource] || currentSearchSource)
+ : currentMusicSourceName;
+ loadingText.textContent = `Searching across ${_sourceName} and your library...`;
}
loadingState.classList.remove('hidden');
emptyState.classList.add('hidden');
@@ -241,7 +245,10 @@ function initializeSearchModeToggle() {
_enhancedSearchData = { db_artists: [], primary_source: null, sources: {}, searchId, query };
try {
- const data = await enhancedSearchFetch(query, { signal: abortController.signal });
+ const data = await enhancedSearchFetch(query, {
+ source: currentSearchSource,
+ signal: abortController.signal,
+ });
console.log('Enhanced results:', data);
// Store multi-source state
diff --git a/webui/static/style.css b/webui/static/style.css
index d71b03ef..5cc7d083 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -32832,10 +32832,76 @@ div.artist-hero-badge {
}
/* ========================================= */
-/* SEARCH MODE TOGGLE SYSTEM */
+/* ========================================= */
+/* SEARCH SOURCE PICKER (replaces Enhanced/Basic toggle) */
/* ========================================= */
-/* Toggle Container */
+.search-source-picker-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 12px;
+ margin: 20px 0;
+}
+
+.search-source-picker-label {
+ color: rgba(255, 255, 255, 0.7);
+ font-family: 'Segoe UI', sans-serif;
+ font-size: 13px;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.search-source-picker-wrapper {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+}
+
+.search-source-picker-select {
+ appearance: none;
+ -webkit-appearance: none;
+ background: rgba(30, 30, 30, 0.8);
+ border: 1px solid rgba(64, 64, 64, 0.4);
+ border-radius: 10px;
+ color: #ffffff;
+ padding: 10px 40px 10px 16px;
+ font-family: 'Segoe UI', sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ min-width: 220px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.search-source-picker-select:hover {
+ border-color: rgba(138, 43, 226, 0.6);
+}
+
+.search-source-picker-select:focus {
+ outline: none;
+ border-color: rgba(138, 43, 226, 0.9);
+ box-shadow: 0 2px 10px rgba(138, 43, 226, 0.35);
+}
+
+.search-source-picker-select option {
+ background: #1e1e1e;
+ color: #ffffff;
+}
+
+.search-source-picker-caret {
+ position: absolute;
+ right: 14px;
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: none;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 12px;
+}
+
+/* Legacy toggle (kept for any other references, but unused by the Search page) */
.search-mode-toggle-container {
display: flex;
justify-content: center;