From 14a5944ae1d8e0167ef6a2f4956df1ec877235f9 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Sun, 28 Dec 2025 22:24:23 -0800 Subject: [PATCH] Add enhanced search mode UI and retry logic for downloads Introduces a search mode toggle in the downloads UI, allowing users to switch between basic and enhanced search modes. Adds new HTML structure, JavaScript logic, and CSS styles for the enhanced search interface (currently placeholder functionality). In the backend, implements a retry system for file discovery after download completion to handle race conditions, with cleanup of stale retry attempts to prevent memory leaks. --- web_server.py | 56 +++++++- webui/index.html | 61 +++++++- webui/static/script.js | 67 +++++++++ webui/static/style.css | 314 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 492 insertions(+), 6 deletions(-) diff --git a/web_server.py b/web_server.py index 636e9409..d1e354f2 100644 --- a/web_server.py +++ b/web_server.py @@ -3476,10 +3476,39 @@ def get_download_status(): print(f"๐ŸŽฏ Found completed matched file on disk: {found_path}") completed_matched_downloads.append((context_key, context, found_path)) # Don't add to _processed_download_ids yet - wait until thread starts successfully + + # Clean up retry tracking if file was found after retries + with _download_retry_lock: + if context_key in _download_retry_attempts: + retry_count = _download_retry_attempts[context_key]['count'] + elapsed = time.time() - _download_retry_attempts[context_key]['first_attempt'] + print(f"โœ… File found after {retry_count} retry attempt(s) ({elapsed:.1f}s): {os.path.basename(filename_from_api)}") + del _download_retry_attempts[context_key] else: - print(f"โŒ CRITICAL: Could not find '{os.path.basename(filename_from_api)}' on disk. Post-processing skipped.") - # Mark as processed to prevent endless retries - _processed_download_ids.add(context_key) + # File not found yet - implement retry logic instead of immediate give-up + # This fixes race condition where slskd reports completion before file is written to disk + with _download_retry_lock: + if context_key not in _download_retry_attempts: + # First retry attempt + _download_retry_attempts[context_key] = { + 'count': 1, + 'first_attempt': time.time() + } + print(f"โณ File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt 1/{_download_retry_max})") + else: + # Increment retry count + _download_retry_attempts[context_key]['count'] += 1 + retry_count = _download_retry_attempts[context_key]['count'] + elapsed = time.time() - _download_retry_attempts[context_key]['first_attempt'] + + if retry_count >= _download_retry_max: + # Max retries reached, give up + print(f"โŒ CRITICAL: Could not find '{os.path.basename(filename_from_api)}' after {retry_count} attempts over {elapsed:.1f}s. Giving up.") + _processed_download_ids.add(context_key) + # Clean up retry tracking + del _download_retry_attempts[context_key] + else: + print(f"โณ File not found yet: '{os.path.basename(filename_from_api)}' - Will retry (attempt {retry_count}/{_download_retry_max}, elapsed: {elapsed:.1f}s)") # If we found completed matched downloads, start processing them in background threads if completed_matched_downloads: @@ -7747,6 +7776,13 @@ def _post_process_matched_download(context_key, context, file_path): # Keep track of processed downloads to avoid re-processing _processed_download_ids = set() +# --- File Discovery Retry System --- +# Prevents race condition where slskd reports completion before file is written to disk +# Tracks retry attempts per download to give files time to finish writing +_download_retry_attempts = {} # {context_key: {'count': N, 'first_attempt': timestamp}} +_download_retry_max = 10 # Max retries before giving up (10 seconds with 1s poll interval) +_download_retry_lock = threading.Lock() + def _check_and_remove_from_wishlist(context): """ Check if a successfully downloaded track should be removed from wishlist. @@ -8057,7 +8093,19 @@ def _simple_monitor_task(): # Use app_context to safely call endpoint logic from a thread with app.app_context(): get_download_status() - + + # Cleanup stale retry attempts (older than 60 seconds) + # This prevents memory leaks from stuck/failed downloads + with _download_retry_lock: + current_time = time.time() + stale_keys = [ + key for key, data in _download_retry_attempts.items() + if current_time - data['first_attempt'] > 60 + ] + for key in stale_keys: + print(f"๐Ÿงน Cleaning up stale retry attempt: {key}") + del _download_retry_attempts[key] + # Automatic search cleanup every hour (or initial cleanup) current_time = time.time() should_cleanup = (current_time - last_search_cleanup > search_cleanup_interval) or not initial_cleanup_done diff --git a/webui/index.html b/webui/index.html index 84935967..f77b35cf 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1176,8 +1176,25 @@

Search, discover, and download high-quality music

- -
+ +
+
+ + +
+
+
+ + +
+ +
@@ -1251,6 +1268,46 @@
+ + + + +
+ +
+
+
โœจ
+ + +
+ +
+ + +
+
+

Ready โ€ข Enhanced search provides intelligent filtering and ranking

+
+ + +
+
+

Enhanced Results

+
0 results
+
+
+
+
โœจ
+

Enhanced Search Ready

+

Intelligent filtering โ€ข Smart ranking โ€ข Better results

+
+
+
+
+ diff --git a/webui/static/script.js b/webui/static/script.js index eff8eefd..1f9bf1c6 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -431,6 +431,7 @@ async function loadPageData(pageId) { break; case 'downloads': initializeSearch(); + initializeSearchModeToggle(); initializeFilters(); await loadDownloadsData(); break; @@ -2333,6 +2334,72 @@ function initializeSearch() { } } +// =============================== +// SEARCH MODE TOGGLE +// =============================== + +function initializeSearchModeToggle() { + const toggleContainer = document.querySelector('.search-mode-toggle'); + const modeBtns = document.querySelectorAll('.search-mode-btn'); + 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'); + return; + } + + modeBtns.forEach(btn => { + btn.addEventListener('click', () => { + const mode = btn.dataset.mode; + + // Update button active states + modeBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + // Update toggle slider position + toggleContainer.setAttribute('data-active', mode); + + // 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'); + } + }); + }); + + // Initialize enhanced search input handlers + const enhancedInput = document.getElementById('enhanced-search-input'); + const enhancedSearchBtn = document.getElementById('enhanced-search-btn'); + const enhancedCancelBtn = document.getElementById('enhanced-cancel-btn'); + + if (enhancedSearchBtn && enhancedInput) { + enhancedSearchBtn.addEventListener('click', () => { + console.log('Enhanced search clicked - functionality to be implemented'); + showToast('Enhanced search coming soon!', 'info'); + }); + + enhancedInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + console.log('Enhanced search Enter pressed - functionality to be implemented'); + showToast('Enhanced search coming soon!', 'info'); + } + }); + } + + if (enhancedCancelBtn) { + enhancedCancelBtn.addEventListener('click', () => { + console.log('Enhanced search cancelled'); + // Cancel logic will be added when functionality is implemented + }); + } +} + async function performSearch() { const query = document.getElementById('search-input').value.trim(); if (!query) { diff --git a/webui/static/style.css b/webui/static/style.css index 85c30cc9..fde88b5c 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -18796,3 +18796,317 @@ body { } } +/* ========================================= */ +/* SEARCH MODE TOGGLE SYSTEM */ +/* ========================================= */ + +/* Toggle Container */ +.search-mode-toggle-container { + display: flex; + justify-content: center; + margin: 20px 0; +} + +.search-mode-toggle { + position: relative; + background: rgba(30, 30, 30, 0.8); + border-radius: 12px; + padding: 4px; + display: inline-flex; + gap: 4px; + border: 1px solid rgba(64, 64, 64, 0.4); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.search-mode-btn { + position: relative; + z-index: 2; + background: transparent; + border: none; + padding: 10px 24px; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + gap: 8px; + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + font-weight: 500; + color: rgba(255, 255, 255, 0.6); +} + +.search-mode-btn:hover { + color: rgba(255, 255, 255, 0.9); +} + +.search-mode-btn.active { + color: #ffffff; +} + +.mode-icon { + font-size: 16px; + transition: transform 0.3s ease; +} + +.search-mode-btn.active .mode-icon { + transform: scale(1.1); +} + +.toggle-slider { + position: absolute; + top: 4px; + left: 4px; + width: calc(50% - 4px); + height: calc(100% - 8px); + background: linear-gradient(135deg, rgba(29, 185, 84, 0.9), rgba(24, 156, 71, 0.9)); + border-radius: 8px; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 1; + box-shadow: 0 2px 8px rgba(29, 185, 84, 0.4); +} + +.search-mode-toggle[data-active="enhanced"] .toggle-slider { + transform: translateX(100%); + background: linear-gradient(135deg, rgba(138, 43, 226, 0.9), rgba(123, 31, 162, 0.9)); + box-shadow: 0 2px 8px rgba(138, 43, 226, 0.4); +} + +/* Search Section Visibility */ +.search-section { + display: none; +} + +.search-section.active { + display: block; +} + +/* ========================================= */ +/* ENHANCED SEARCH STYLING */ +/* ========================================= */ + +/* Enhanced Search Bar */ +.enhanced-search-bar-container { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.15), rgba(75, 0, 130, 0.15)); + border-radius: 16px; + border: 2px solid rgba(138, 43, 226, 0.3); + padding: 20px; + display: flex; + gap: 16px; + align-items: center; + box-shadow: 0 8px 24px rgba(138, 43, 226, 0.2); + backdrop-filter: blur(10px); +} + +.enhanced-search-wrapper { + flex-grow: 1; + position: relative; + display: flex; + align-items: center; + background: rgba(20, 20, 20, 0.6); + border-radius: 12px; + border: 1px solid rgba(138, 43, 226, 0.4); + padding: 0 16px; + transition: all 0.3s ease; +} + +.enhanced-search-wrapper:focus-within { + border-color: rgba(138, 43, 226, 0.8); + box-shadow: 0 0 0 3px rgba(138, 43, 226, 0.2); +} + +.enhanced-search-icon { + font-size: 20px; + margin-right: 12px; + animation: sparkle 2s ease-in-out infinite; +} + +@keyframes sparkle { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.7; transform: scale(1.1); } +} + +#enhanced-search-input { + flex-grow: 1; + background: transparent; + border: none; + outline: none; + font-family: 'Segoe UI', sans-serif; + font-size: 15px; + color: #ffffff; + padding: 14px 0; +} + +#enhanced-search-input::placeholder { + color: rgba(255, 255, 255, 0.5); +} + +.enhanced-cancel-btn { + background: rgba(138, 43, 226, 0.3); + border: none; + border-radius: 6px; + padding: 6px 12px; + color: #ffffff; + cursor: pointer; + font-size: 14px; + transition: all 0.2s ease; +} + +.enhanced-cancel-btn:hover { + background: rgba(138, 43, 226, 0.5); +} + +.enhanced-search-btn { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.9), rgba(123, 31, 162, 0.9)); + border: none; + border-radius: 12px; + padding: 14px 28px; + color: #ffffff; + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(138, 43, 226, 0.3); +} + +.enhanced-search-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(138, 43, 226, 0.4); +} + +.enhanced-search-btn:active { + transform: translateY(0); +} + +.btn-icon { + font-size: 16px; +} + +/* Enhanced Search Status */ +.enhanced-search-status { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 20px; + margin-top: 16px; + background: rgba(20, 20, 20, 0.4); + border-radius: 10px; + border-left: 3px solid rgba(138, 43, 226, 0.8); +} + +.enhanced-status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: rgba(138, 43, 226, 0.8); + animation: pulse-glow 2s ease-in-out infinite; +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 4px rgba(138, 43, 226, 0.6); } + 50% { box-shadow: 0 0 12px rgba(138, 43, 226, 0.9); } +} + +#enhanced-search-status-text { + font-family: 'Segoe UI', sans-serif; + font-size: 13px; + color: rgba(255, 255, 255, 0.8); + margin: 0; +} + +/* Enhanced Search Results */ +.enhanced-search-results-container { + background: rgba(20, 20, 20, 0.3); + border-radius: 16px; + border: 2px solid rgba(138, 43, 226, 0.2); + padding: 20px; + margin-top: 20px; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: hidden; +} + +.enhanced-results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(138, 43, 226, 0.2); +} + +.enhanced-results-header h3 { + color: rgba(255, 255, 255, 0.95); + font-size: 16px; + font-weight: 600; + margin: 0; +} + +.results-count-badge { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.3), rgba(123, 31, 162, 0.3)); + border: 1px solid rgba(138, 43, 226, 0.5); + border-radius: 20px; + padding: 6px 14px; + font-size: 12px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); +} + +.enhanced-results-scroll-area { + overflow-y: auto; + flex-grow: 1; + padding-right: 8px; +} + +.enhanced-results-scroll-area::-webkit-scrollbar { + width: 8px; +} + +.enhanced-results-scroll-area::-webkit-scrollbar-track { + background: rgba(20, 20, 20, 0.3); + border-radius: 4px; +} + +.enhanced-results-scroll-area::-webkit-scrollbar-thumb { + background: linear-gradient(to bottom, rgba(138, 43, 226, 0.6), rgba(123, 31, 162, 0.6)); + border-radius: 4px; +} + +.enhanced-results-scroll-area::-webkit-scrollbar-thumb:hover { + background: linear-gradient(to bottom, rgba(138, 43, 226, 0.8), rgba(123, 31, 162, 0.8)); +} + +/* Enhanced Results Placeholder */ +.enhanced-results-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; +} + +.placeholder-icon { + font-size: 48px; + margin-bottom: 16px; + animation: sparkle 2s ease-in-out infinite; +} + +.placeholder-title { + font-size: 18px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + margin: 0 0 8px 0; +} + +.placeholder-subtitle { + font-size: 13px; + color: rgba(255, 255, 255, 0.6); + margin: 0; +} +