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; +} +