From f203b3e46dc9c614420c00a3027e5cdcb53a8bd0 Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Wed, 22 Apr 2026 13:31:42 -0700
Subject: [PATCH] Remove embedded Download Manager from Search page, bump to
2.44
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Phase 3c of the Search/Artists unification. The Search page carried
a second copy of the Download Manager (active + finished queues,
clear/cancel-all buttons) that was hidden by default and duplicated
the dedicated Downloads page. That duplicate is now gone.
Removed:
- Side-panel HTML block and the toggle button that showed/hid it
- ~290 lines of polling + render infra in downloads.js: loadDownloads-
Data, startDownloadPolling/stopDownloadPolling, updateDownload-
Queues, renderQueue, updateTabCounts/updateDownloadStats,
initializeDownloadTabs/switchDownloadTab, cancelDownloadItem,
clearFinishedDownloads, cancelAllDownloads, and the
activeDownloads/finishedDownloads globals
- initializeDownloadManagerToggle and its call from init.js
- Stopped hitting /api/downloads/status every second on the Search
page (the dedicated Downloads page already polls its own view)
CSS grid for the Search page collapsed from '1fr 370px' to '1fr' now
that the right panel is gone. Unused .controls-panel__* / .download-
manager__* / .downloads-side-panel CSS rules kept in place — harmless,
can be pruned later.
---
web_server.py | 12 +-
webui/index.html | 50 +-----
webui/static/downloads.js | 330 +-------------------------------------
webui/static/helper.js | 5 +
webui/static/init.js | 33 ----
webui/static/style.css | 3 +-
6 files changed, 22 insertions(+), 411 deletions(-)
diff --git a/web_server.py b/web_server.py
index 3c1d1b35..7b01e306 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.43"
+_SOULSYNC_BASE_VERSION = "2.44"
def _build_version_string():
"""Append short commit hash to version when available (e.g. 2.35+abc1234)."""
@@ -22809,6 +22809,16 @@ def get_version_info():
"title": "What's New in SoulSync",
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"sections": [
+ {
+ "title": "Remove Embedded Download Manager from Search Page",
+ "description": "The Search page used to carry a second copy of the Download Manager (active + finished queues, clear/cancel-all buttons) that was hidden by default and duplicated the dedicated Downloads page. That duplicate is gone",
+ "features": [
+ "• Toggle button, side-panel HTML, and its 1-second polling loop removed — Downloads page is now the single downloads UI",
+ "• About 330 lines of dead code gone across downloads.js and init.js",
+ "• CSS grid for the Search page collapsed to a single-column layout now that the right panel is gone",
+ "• Phase 3c of the Search/Artists unification project",
+ ],
+ },
{
"title": "Search Page Renamed to /search",
"description": "The Search page's internal id is now 'search' instead of 'downloads', which no longer conflicts with the real Downloads page. URL /downloads still works for bookmarks and external links",
diff --git a/webui/index.html b/webui/index.html
index 8e196335..053245a9 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -1848,10 +1848,10 @@
This top-level container replicates the QSplitter from downloads.py,
creating the two-panel layout for the page.
-->
-
+
-
+
@@ -1859,13 +1859,9 @@
@@ -2107,44 +2103,6 @@
-
-
-
-
-
-
-
-
-
-
• Active Downloads: 0
-
• Finished Downloads: 0
-
-
- 🗑️ Clear Completed
- ⛔ Clear Current
-
-
-
-
-
-
- Download Queue (0)
- Finished (0)
-
-
-
-
-
-
-
-
-
No finished downloads.
-
-
-
-
diff --git a/webui/static/downloads.js b/webui/static/downloads.js
index 9fdd9bef..7348f4ca 100644
--- a/webui/static/downloads.js
+++ b/webui/static/downloads.js
@@ -4267,335 +4267,7 @@ function updateModalSyncProgress(playlistId, progress) {
}
-// Download tracking state management - matching GUI functionality
-let activeDownloads = {};
-let finishedDownloads = {};
-let downloadStatusInterval = null;
-let isDownloadPollingActive = false;
-
-async function loadDownloadsData() {
- // Downloads page loads search results dynamically
- console.log('Downloads page loaded');
-
- // Event listeners are already set up in initializeSearch() - don't duplicate them
- const clearButton = document.querySelector('.controls-panel__clear-btn');
- const cancelAllButton = document.querySelector('.controls-panel__cancel-all-btn');
-
- if (clearButton) {
- clearButton.addEventListener('click', clearFinishedDownloads);
- }
- if (cancelAllButton) {
- cancelAllButton.addEventListener('click', cancelAllDownloads);
- }
-
- // Start sophisticated polling system (1-second interval like GUI)
- startDownloadPolling();
-
- // Initialize tab management
- initializeDownloadTabs();
-}
-
-function startDownloadPolling() {
- if (isDownloadPollingActive) return;
-
- console.log('Starting download status polling (1-second interval)');
- isDownloadPollingActive = true;
-
- // Initial call
- updateDownloadQueues();
-
- // Start 1-second polling (matching GUI's 1000ms timer)
- downloadStatusInterval = setInterval(updateDownloadQueues, 1000);
-}
-
-function stopDownloadPolling() {
- if (downloadStatusInterval) {
- clearInterval(downloadStatusInterval);
- downloadStatusInterval = null;
- }
- isDownloadPollingActive = false;
- console.log('Stopped download status polling');
-}
-
-async function updateDownloadQueues() {
- if (document.hidden) return; // Skip polling when tab is not visible
- try {
- const response = await fetch('/api/downloads/status');
- const data = await response.json();
-
- if (data.error) {
- console.error("Error fetching download status:", data.error);
- return;
- }
-
- const newActive = {};
- const newFinished = {};
-
- // Terminal states matching GUI logic
- const terminalStates = ['Completed', 'Succeeded', 'Cancelled', 'Canceled', 'Failed', 'Errored'];
-
- // Process transfers exactly like GUI
- data.transfers.forEach(item => {
- const isTerminal = terminalStates.some(state =>
- item.state && item.state.includes(state)
- );
-
- if (isTerminal) {
- newFinished[item.id] = item;
- } else {
- newActive[item.id] = item;
- }
- });
-
- // Update global state
- activeDownloads = newActive;
- finishedDownloads = newFinished;
-
- // Render both queues
- renderQueue('active-queue', activeDownloads, true);
- renderQueue('finished-queue', finishedDownloads, false);
-
- // Update tab counts
- updateTabCounts();
-
- // Update stats in the side panel
- updateDownloadStats();
-
- } catch (error) {
- // Only log errors occasionally to avoid console spam
- if (Math.random() < 0.1) {
- console.error("Failed to update download queues:", error);
- }
- }
-}
-
-function renderQueue(containerId, downloads, isActiveQueue) {
- const container = document.getElementById(containerId);
- if (!container) return;
-
- const downloadIds = Object.keys(downloads);
-
- if (downloadIds.length === 0) {
- container.innerHTML = `${isActiveQueue ? 'No active downloads.' : 'No finished downloads.'}
`;
- return;
- }
-
- let html = '';
- for (const id of downloadIds) {
- const item = downloads[id];
-
- // Extract display title from filename
- let title = 'Unknown File';
- if (item.filename) {
- // YouTube/Tidal filenames are encoded as "id||title"
- if ((item.username === 'youtube' || item.username === 'tidal' || item.username === 'qobuz' || item.username === 'hifi') && item.filename.includes('||')) {
- const parts = item.filename.split('||');
- title = parts[1] || parts[0]; // Use title part, fallback to id
- } else {
- // Regular Soulseek filename - extract last part of path
- title = item.filename.split(/[\\/]/).pop();
- }
- }
-
- const progress = item.percentComplete || 0;
- const bytesTransferred = item.bytesTransferred || 0;
- const totalBytes = item.size || 0;
- const speed = item.averageSpeed || 0;
-
- // Format file size
- const formatSize = (bytes) => {
- if (!bytes) return 'Unknown size';
- const units = ['B', 'KB', 'MB', 'GB'];
- let size = bytes;
- let unitIndex = 0;
- while (size >= 1024 && unitIndex < units.length - 1) {
- size /= 1024;
- unitIndex++;
- }
- return `${size.toFixed(1)} ${units[unitIndex]}`;
- };
-
- // Format speed
- const formatSpeed = (bytesPerSecond) => {
- if (!bytesPerSecond || bytesPerSecond <= 0) return '';
- return `${formatSize(bytesPerSecond)}/s`;
- };
-
- let actionButtonHTML = '';
- if (isActiveQueue) {
- // Active items get progress bar and cancel button
- actionButtonHTML = `
-
-
-
- ${item.state} - ${progress.toFixed(1)}%
- ${speed > 0 ? `• ${formatSpeed(speed)}` : ''}
- ${totalBytes > 0 ? `• ${formatSize(bytesTransferred)} / ${formatSize(totalBytes)}` : ''}
-
-
- ✕ Cancel
- `;
- } else {
- // Finished items get status and open button
- let statusClass = '';
- if (item.state.includes('Cancelled')) statusClass = 'status--cancelled';
- else if (item.state.includes('Failed') || item.state.includes('Errored')) statusClass = 'status--failed';
- else if (item.state.includes('Completed') || item.state.includes('Succeeded')) statusClass = 'status--completed';
-
- actionButtonHTML = `
-
- ${item.state}
-
- 📁 Open
- `;
- }
-
- // Enrich with metadata from backend context (artist, album, artwork)
- const meta = item._meta || {};
- const sourceLabels = { youtube: 'YouTube', tidal: 'Tidal', qobuz: 'Qobuz', hifi: 'HiFi', deezer_dl: 'Deezer', lidarr: 'Lidarr' };
- const sourceBadge = sourceLabels[item.username] || item.username;
-
- html += `
-
-
- ${meta.artwork_url
- ? `
`
- : '
♫
'}
-
-
-
${title}
- ${meta.artist || meta.album ? `
-
- ${meta.artist ? `${escapeHtml(meta.artist)} ` : ''}
- ${meta.artist && meta.album ? '· ' : ''}
- ${meta.album ? `${escapeHtml(meta.album)} ` : ''}
-
- ` : ''}
-
- ${sourceBadge}
- ${meta.quality ? `${escapeHtml(meta.quality)} ` : ''}
-
-
-
- ${actionButtonHTML}
-
-
- `;
- }
- container.innerHTML = html;
-}
-
-function updateTabCounts() {
- const activeCount = Object.keys(activeDownloads).length;
- const finishedCount = Object.keys(finishedDownloads).length;
-
- const activeTabBtn = document.querySelector('.tab-btn[data-tab="active-queue"]');
- const finishedTabBtn = document.querySelector('.tab-btn[data-tab="finished-queue"]');
-
- if (activeTabBtn) activeTabBtn.textContent = `Download Queue (${activeCount})`;
- if (finishedTabBtn) finishedTabBtn.textContent = `Finished (${finishedCount})`;
-}
-
-function updateDownloadStats() {
- const activeCount = Object.keys(activeDownloads).length;
- const finishedCount = Object.keys(finishedDownloads).length;
-
- const activeLabel = document.getElementById('active-downloads-label');
- const finishedLabel = document.getElementById('finished-downloads-label');
-
- if (activeLabel) activeLabel.textContent = `• Active Downloads: ${activeCount}`;
- if (finishedLabel) finishedLabel.textContent = `• Finished Downloads: ${finishedCount}`;
-}
-
-function initializeDownloadTabs() {
- const tabButtons = document.querySelectorAll('.tab-btn');
- tabButtons.forEach(btn => {
- btn.addEventListener('click', () => switchDownloadTab(btn));
- });
-}
-
-function switchDownloadTab(button) {
- const targetTabId = button.getAttribute('data-tab');
-
- // Update buttons
- document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
- button.classList.add('active');
-
- // Update content panes
- document.querySelectorAll('.download-queue').forEach(queue => queue.classList.remove('active'));
- const targetQueue = document.getElementById(targetTabId);
- if (targetQueue) targetQueue.classList.add('active');
-}
-
-async function cancelDownloadItem(downloadId, username) {
- try {
- const response = await fetch('/api/downloads/cancel', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ download_id: downloadId, username: username })
- });
- const result = await response.json();
-
- if (result.success) {
- showToast('Download cancelled', 'success');
- } else {
- showToast(`Failed to cancel: ${result.error}`, 'error');
- }
- } catch (error) {
- console.error('Error cancelling download:', error);
- showToast('Error sending cancel request', 'error');
- }
-}
-
-async function clearFinishedDownloads() {
- const finishedCount = Object.keys(finishedDownloads).length;
- if (finishedCount === 0) {
- showToast('No finished downloads to clear', 'error');
- return;
- }
-
- try {
- const response = await fetch('/api/downloads/clear-finished', {
- method: 'POST'
- });
- const result = await response.json();
-
- if (result.success) {
- showToast('Finished downloads cleared', 'success');
- } else {
- showToast(`Failed to clear: ${result.error}`, 'error');
- }
- } catch (error) {
- console.error('Error clearing finished downloads:', error);
- showToast('Error sending clear request', 'error');
- }
-}
-
-async function cancelAllDownloads() {
- if (!await showConfirmDialog({ title: 'Cancel All Downloads', message: 'Cancel ALL active downloads and clear the transfer list? This cannot be undone.', confirmText: 'Cancel All', destructive: true })) {
- return;
- }
-
- try {
- const response = await fetch('/api/downloads/cancel-all', {
- method: 'POST'
- });
- const result = await response.json();
-
- if (result.success) {
- showToast('All downloads cancelled and cleared', 'success');
- } else {
- showToast(`Failed to cancel: ${result.error}`, 'error');
- }
- } catch (error) {
- console.error('Error cancelling all downloads:', error);
- showToast('Error cancelling downloads', 'error');
- }
-}
-
-// REPLACE the old performDownloadsSearch function with this new one.
+// Raw Soulseek file search (used by the 'Soulseek (raw files)' source picker option).
async function performDownloadsSearch() {
const query = document.getElementById('downloads-search-input').value.trim();
if (!query) {
diff --git a/webui/static/helper.js b/webui/static/helper.js
index 2c4fd0e5..fe201416 100644
--- a/webui/static/helper.js
+++ b/webui/static/helper.js
@@ -3599,6 +3599,11 @@ function closeHelperSearch() {
// ═══════════════════════════════════════════════════════════════════════════
const WHATS_NEW = {
+ '2.44': [
+ // --- April 23, 2026 (evening) ---
+ { date: 'April 23, 2026 (evening)' },
+ { title: 'Remove Embedded Download Manager from Search Page', desc: 'The Search page used to carry a second copy of the Download Manager (active + finished queues, clear/cancel-all buttons) that was hidden by default and duplicated the dedicated Downloads page. That duplicate is gone — toggle button, side-panel HTML, and its 1-second polling loop all removed. About 330 lines of dead code cleaned up across downloads.js and init.js. CSS grid for the Search page collapsed to single-column now that the right panel is gone. The dedicated Downloads sidebar page is now the single downloads UI. Phase 3c of the Search/Artists unification project', page: 'search' },
+ ],
'2.43': [
// --- April 23, 2026 (later) ---
{ date: 'April 23, 2026 (later)' },
diff --git a/webui/static/init.js b/webui/static/init.js
index 8f1870f6..e8e38463 100644
--- a/webui/static/init.js
+++ b/webui/static/init.js
@@ -1925,7 +1925,6 @@ function initApp() {
initExpandedPlayer();
initializeSyncPage();
initializeWatchlist();
- initializeDownloadManagerToggle();
// Initialize WebSocket connection (falls back to HTTP polling if unavailable)
@@ -2115,37 +2114,6 @@ function initializeWatchlist() {
console.log('Watchlist system initialized');
}
-function initializeDownloadManagerToggle() {
- const toggleButton = document.getElementById('toggle-download-manager-btn');
- const downloadsContent = document.querySelector('.downloads-content');
-
- if (!toggleButton || !downloadsContent) {
- console.log('Download manager toggle not found on this page');
- return;
- }
-
- // Load saved state from localStorage (hidden by default for more search space)
- const isHidden = localStorage.getItem('downloadManagerHidden') !== 'false';
- if (isHidden) {
- downloadsContent.classList.add('manager-hidden');
- }
-
- // Add click handler
- toggleButton.addEventListener('click', () => {
- const isCurrentlyHidden = downloadsContent.classList.contains('manager-hidden');
-
- if (isCurrentlyHidden) {
- downloadsContent.classList.remove('manager-hidden');
- localStorage.setItem('downloadManagerHidden', 'false');
- } else {
- downloadsContent.classList.add('manager-hidden');
- localStorage.setItem('downloadManagerHidden', 'true');
- }
- });
-
- console.log('Download manager toggle initialized');
-}
-
function navigateToPage(pageId, options = {}) {
// Backwards-compat alias — the Search page used to live under id 'downloads'.
if (pageId === 'downloads') pageId = 'search';
@@ -2249,7 +2217,6 @@ async function loadPageData(pageId) {
initializeSearch();
initializeSearchModeToggle();
initializeFilters();
- await loadDownloadsData();
break;
case 'artists':
// Only fully initialize if not already initialized
diff --git a/webui/static/style.css b/webui/static/style.css
index 5cc7d083..7298bc44 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -4255,8 +4255,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
/* Main Layout: Replicates QSplitter */
.downloads-content {
display: grid;
- grid-template-columns: 1fr 370px;
- /* Left panel is flexible, right is fixed */
+ grid-template-columns: 1fr;
gap: 24px;
height: 100%;
/* Fill parent .page content area */