diff --git a/webui/src/routes/import/-ui/import-page.module.css b/webui/src/routes/import/-ui/import-page.module.css
index 2f7e71b9..db15a907 100644
--- a/webui/src/routes/import/-ui/import-page.module.css
+++ b/webui/src/routes/import/-ui/import-page.module.css
@@ -969,19 +969,12 @@
color: #ef4444;
}
-/* ========================================
- END IMPORT PAGE
- .importPageFileChip {
- cursor: pointer;
- }
-
- .importPageMatchRow {
- cursor: pointer;
- }
-}
-
-/* Import Page — small screen */
@media (max-width: 768px) {
+ .importPageFileChip,
+ .importPageMatchRow {
+ cursor: pointer;
+ }
+
.importPageContainer {
padding: 16px;
}
diff --git a/webui/static/init.js b/webui/static/init.js
index 6b760b0c..10a047b4 100644
--- a/webui/static/init.js
+++ b/webui/static/init.js
@@ -2420,9 +2420,6 @@ async function loadPageData(pageId) {
loadApiKeys();
loadBlacklistCount();
break;
- case 'import':
- initializeImportPage();
- break;
case 'hydrabase':
// Check connection status and pre-fill saved credentials
try {
diff --git a/webui/static/mobile.css b/webui/static/mobile.css
index 043ea773..bd81ae64 100644
--- a/webui/static/mobile.css
+++ b/webui/static/mobile.css
@@ -2312,86 +2312,9 @@
opacity: 0.7;
transition: opacity 0.1s ease;
}
-
- /* Import Page - Touch fallback */
- .import-page-file-chip {
- cursor: pointer;
- }
-
- .import-page-match-row {
- cursor: pointer;
- }
}
-/* Import Page — small screen */
@media (max-width: 768px) {
- .import-page-container {
- padding: 16px;
- }
-
- .import-page-title {
- font-size: 22px;
- }
-
- .import-page-album-grid {
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
- gap: 10px;
- }
-
- .import-page-album-hero {
- flex-direction: column;
- text-align: center;
- }
-
- .import-page-album-hero img {
- width: 100px;
- height: 100px;
- }
-
- .import-page-match-row {
- grid-template-columns: 28px 1fr;
- gap: 8px;
- }
-
- .import-page-match-file {
- grid-column: 1 / -1;
- padding-left: 28px;
- }
-
- .import-page-match-unmatch {
- grid-column: 1 / -1;
- justify-self: end;
- }
-
- .import-page-singles-header {
- flex-direction: column;
- align-items: flex-start;
- }
-
- .import-page-singles-actions {
- width: 100%;
- flex-wrap: wrap;
- }
-
- .import-page-single-item {
- grid-template-columns: 28px 1fr;
- }
-
- .import-page-single-actions {
- grid-column: 1 / -1;
- justify-self: end;
- }
-
- .import-page-single-search-panel {
- padding-left: 0;
- }
-
- .import-page-staging-bar {
- flex-direction: column;
- gap: 4px;
- align-items: flex-start;
- }
-
/* Profile Picker - Mobile */
.profile-picker-grid {
gap: 20px;
diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js
index 68772b35..9723c549 100644
--- a/webui/static/stats-automations.js
+++ b/webui/static/stats-automations.js
@@ -1,1377 +1,3 @@
-// IMPORT PAGE (full page, replaces old modal)
-// ===================================================================
-
-let importJobIdCounter = 0;
-
-const importPageState = {
- stagingFiles: [],
- selectedSingles: new Set(),
- albumData: null, // response from /api/import/album/match
- matchOverrides: {}, // { trackIndex: stagingFileIndex } — manual drag-drop overrides
- singlesManualMatches: {}, // { stagingFileIndex: { id, name, artist, album, ... } }
- initialized: false,
- activeTab: 'album',
- tapSelectedChip: null, // for mobile tap-to-assign fallback
- // Album lookup cache for click handlers. Populated by suggestions /
- // search renderers; read by importPageSelectAlbum so the match POST
- // can include `source` + `name` + `artist` (without these, backend
- // can't look up cross-source IDs and falls back to a broken
- // "Unknown Artist / album_id as title / 0 tracks" placeholder —
- // github issue #524).
- _albumLookup: {}, // { albumId: { id, name, artist, source } }
-};
-
-// --- Initialization ---
-
-function initializeImportPage() {
- if (!importPageState.initialized) {
- importPageState.initialized = true;
- importPageRefreshStaging();
- importPageLoadAutoGroups();
- importPageLoadSuggestions();
- }
-}
-
-async function importPageRefreshStaging() {
- // Clear finished jobs from the queue
- importPageClearFinishedJobs();
-
- try {
- const resp = await fetch('/api/import/staging/files');
- const data = await resp.json();
- if (!data.success) {
- document.getElementById('import-page-staging-path').textContent = `Import folder: error`;
- return;
- }
-
- importPageState.stagingFiles = data.files || [];
- document.getElementById('import-page-staging-path').textContent = `Import: ${data.staging_path || 'Not configured'}`;
-
- const totalSize = importPageState.stagingFiles.reduce((s, f) => s + (f.size || 0), 0);
- const sizeStr = totalSize > 1073741824 ? `${(totalSize / 1073741824).toFixed(1)} GB`
- : totalSize > 1048576 ? `${(totalSize / 1048576).toFixed(0)} MB`
- : `${(totalSize / 1024).toFixed(0)} KB`;
- document.getElementById('import-page-staging-stats').textContent =
- `${importPageState.stagingFiles.length} file${importPageState.stagingFiles.length !== 1 ? 's' : ''}${totalSize ? ' · ' + sizeStr : ''}`;
-
- // Refresh the current tab view after data is loaded
- if (importPageState.activeTab === 'singles') {
- importPageRenderSinglesList();
- } else if (importPageState.activeTab === 'album') {
- importPageLoadAutoGroups();
- }
- // Always refresh suggestions and groups in background
- importPageLoadSuggestions();
- } catch (err) {
- console.error('Failed to refresh staging:', err);
- }
-}
-
-function importPageSwitchTab(tab) {
- importPageState.activeTab = tab;
- document.getElementById('import-page-tab-album').classList.toggle('active', tab === 'album');
- document.getElementById('import-page-tab-singles').classList.toggle('active', tab === 'singles');
- document.getElementById('import-page-tab-auto')?.classList.toggle('active', tab === 'auto');
- document.getElementById('import-page-album-content').classList.toggle('active', tab === 'album');
- document.getElementById('import-page-singles-content')?.classList.toggle('active', tab === 'singles');
- document.getElementById('import-page-auto-content')?.classList.toggle('active', tab === 'auto');
-
- if (tab === 'singles' && importPageState.stagingFiles.length > 0) {
- importPageRenderSinglesList();
- }
- if (tab === 'auto') {
- _autoImportLoadStatus();
- _autoImportLoadResults();
- _autoImportStartPolling();
- } else {
- _autoImportStopPolling();
- }
-}
-
-// ── Auto-Import Tab ──
-let _autoImportPollInterval = null;
-let _autoImportFilter = 'all';
-let _autoImportLastStatus = null;
-
-function _autoImportStartPolling() {
- _autoImportStopPolling();
- _autoImportPollInterval = setInterval(async () => {
- if (importPageState.activeTab === 'auto') {
- await _autoImportLoadStatus();
- _autoImportLoadResults();
- }
- }, 5000);
-}
-
-function _autoImportStopPolling() {
- if (_autoImportPollInterval) { clearInterval(_autoImportPollInterval); _autoImportPollInterval = null; }
-}
-
-async function _autoImportToggle(enabled) {
- // Optimistically update toggle state so it doesn't flicker
- const toggle = document.getElementById('auto-import-enabled');
- if (toggle) toggle.checked = enabled;
- const statusText = document.getElementById('auto-import-status-text');
- if (statusText) statusText.textContent = enabled ? 'Starting...' : 'Stopping...';
-
- try {
- const res = await fetch('/api/auto-import/toggle', {
- method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ enabled })
- });
- const data = await res.json();
- if (data.success) {
- showToast(enabled ? 'Auto-import enabled' : 'Auto-import disabled', 'success');
- _autoImportLoadStatus();
- } else {
- // Revert on failure
- if (toggle) toggle.checked = !enabled;
- }
- } catch (e) {
- showToast('Error: ' + e.message, 'error');
- if (toggle) toggle.checked = !enabled;
- }
-}
-
-async function _autoImportLoadStatus() {
- try {
- const res = await fetch('/api/auto-import/status');
- const data = await res.json();
- if (!data.success) return;
- _autoImportLastStatus = data;
-
- const toggle = document.getElementById('auto-import-enabled');
- const statusText = document.getElementById('auto-import-status-text');
- const settingsRow = document.getElementById('auto-import-settings-row');
- const scanNowBtn = document.getElementById('auto-import-scan-now');
- const progressEl = document.getElementById('auto-import-progress');
- const progressText = document.getElementById('auto-import-progress-text');
-
- if (toggle) toggle.checked = data.running;
- if (settingsRow) settingsRow.style.display = data.running ? '' : 'none';
- if (scanNowBtn) scanNowBtn.style.display = data.running ? '' : 'none';
-
- // Live scan + per-track processing progress.
- // `active_imports` (added when the worker switched to a bounded
- // executor pool) is the source of truth; multiple albums can be
- // in flight at once. Render each one on its own line; fall back
- // to the legacy single-line summary for older backend payloads.
- if (progressEl) {
- const active = Array.isArray(data.active_imports) ? data.active_imports : [];
- if (active.length > 0) {
- progressEl.style.display = '';
- if (progressText) {
- const lines = active.map(a => {
- const folder = a.folder_name || '...';
- const idx = a.track_index || 0;
- const total = a.track_total || 0;
- const trackName = a.track_name || '';
- if (a.status === 'processing' && total > 0) {
- return `${folder} — track ${idx}/${total}: ${trackName}`;
- }
- if (a.status === 'matching') return `${folder} — matching tracks…`;
- if (a.status === 'identifying') return `${folder} — identifying…`;
- return `${folder} — queued`;
- });
- progressText.textContent = lines.length === 1
- ? `Processing ${lines[0]}`
- : `Processing ${lines.length} imports:\n${lines.join('\n')}`;
- }
- } else if (data.current_status === 'scanning') {
- progressEl.style.display = '';
- if (progressText) {
- const stats = data.stats || {};
- progressText.textContent = `Scanning… (${stats.scanned || 0} processed)`;
- }
- } else {
- progressEl.style.display = 'none';
- }
- }
-
- if (statusText) {
- if (data.paused) statusText.textContent = 'Paused';
- else if (data.current_status === 'processing') statusText.textContent = 'Processing...';
- else if (data.current_status === 'scanning') statusText.textContent = 'Scanning...';
- else if (data.running) {
- // Show last scan time
- let watchText = 'Watching';
- if (data.last_scan_time) {
- try {
- const lastScan = new Date(data.last_scan_time);
- const diffS = Math.floor((Date.now() - lastScan) / 1000);
- if (diffS < 60) watchText = `Watching (scanned ${diffS}s ago)`;
- else if (diffS < 3600) watchText = `Watching (scanned ${Math.floor(diffS / 60)}m ago)`;
- } catch (e) {}
- }
- statusText.textContent = watchText;
- } else statusText.textContent = 'Disabled';
- const _runningClass = data.current_status === 'scanning'
- ? 'scanning'
- : data.current_status === 'processing'
- ? 'processing'
- : 'active';
- statusText.className = 'auto-import-status ' + (data.running ? _runningClass : 'disabled');
- }
- } catch (e) {}
-}
-
-async function _autoImportLoadResults() {
- const container = document.getElementById('auto-import-results');
- if (!container) return;
- try {
- const res = await fetch('/api/auto-import/results?limit=100');
- const data = await res.json();
- if (!data.success || !data.results || data.results.length === 0) {
- if (!container.querySelector('.auto-import-card')) {
- container.innerHTML = `
-
No imports yet. Drop album folders or single tracks into your import folder.
-
`;
- }
- // Hide stats and filters
- const statsEl = document.getElementById('auto-import-stats');
- const filtersEl = document.getElementById('auto-import-filters');
- if (statsEl) statsEl.style.display = 'none';
- if (filtersEl) filtersEl.style.display = 'none';
- return;
- }
-
- // Compute stats
- const allResults = data.results;
- const importedCount = allResults.filter(r => r.status === 'completed' || r.status === 'approved').length;
- const reviewCount = allResults.filter(r => r.status === 'pending_review').length;
- const failedCount = allResults.filter(r => r.status === 'failed' || r.status === 'needs_identification').length;
-
- // Update stats
- const statsEl = document.getElementById('auto-import-stats');
- if (statsEl) {
- statsEl.style.display = '';
- document.getElementById('auto-import-stat-imported').textContent = `${importedCount} imported`;
- document.getElementById('auto-import-stat-review').textContent = `${reviewCount} review`;
- document.getElementById('auto-import-stat-failed').textContent = `${failedCount} failed`;
- }
-
- // Show filters
- const filtersEl = document.getElementById('auto-import-filters');
- if (filtersEl) {
- filtersEl.style.display = '';
- // Show batch action buttons when applicable
- const approveAllBtn = document.getElementById('auto-import-approve-all');
- const clearBtn = document.getElementById('auto-import-clear-completed');
- if (approveAllBtn) approveAllBtn.style.display = reviewCount > 0 ? '' : 'none';
- if (clearBtn) clearBtn.style.display = (importedCount + failedCount) > 0 ? '' : 'none';
- }
-
- // Apply filter
- let filtered = allResults;
- if (_autoImportFilter === 'pending') filtered = allResults.filter(r => r.status === 'pending_review');
- else if (_autoImportFilter === 'imported') filtered = allResults.filter(r => r.status === 'completed' || r.status === 'approved');
- else if (_autoImportFilter === 'failed') filtered = allResults.filter(r => r.status === 'failed' || r.status === 'needs_identification');
-
- if (filtered.length === 0) {
- const filterName = _autoImportFilter === 'pending' ? 'pending review' : _autoImportFilter;
- container.innerHTML = ``;
- return;
- }
-
- container.innerHTML = filtered.map((r, idx) => {
- const confPct = Math.round((r.confidence || 0) * 100);
- const confClass = confPct >= 90 ? 'high' : confPct >= 70 ? 'medium' : 'low';
- const statusLabels = {
- 'completed': 'Imported', 'pending_review': 'Needs Review',
- 'needs_identification': 'Unidentified', 'failed': 'Failed',
- 'scanning': 'Scanning...', 'matched': 'Matched',
- 'rejected': 'Dismissed', 'approved': 'Approved',
- 'processing': 'Processing',
- };
- const statusIcons = {
- 'completed': '\u2713', 'pending_review': '\u26A0',
- 'needs_identification': '\u2717', 'failed': '\u2717',
- 'scanning': '\u231B', 'matched': '\u2713',
- 'rejected': '\u2715', 'approved': '\u2713',
- 'processing': '\u29D7',
- };
- const statusLabel = statusLabels[r.status] || r.status;
- const statusIcon = statusIcons[r.status] || '';
- const statusClass = r.status === 'completed' ? 'completed' : r.status === 'pending_review' ? 'review' :
- r.status === 'failed' || r.status === 'needs_identification' ? 'failed' :
- r.status === 'processing' ? 'processing' : 'neutral';
-
- // Live per-track progress for the row currently being processed.
- // Match by folder_hash through the `active_imports` array
- // — the worker now runs multiple imports in parallel via a
- // bounded executor pool, so `current_folder` alone can't
- // identify a row's live state.
- const liveStatus = _autoImportLastStatus;
- const liveActive = (liveStatus && Array.isArray(liveStatus.active_imports))
- ? liveStatus.active_imports.find(a => a.folder_hash === r.folder_hash)
- : null;
- const isLiveProcessing = r.status === 'processing'
- && liveActive && liveActive.status === 'processing';
- const liveTrackIdx = isLiveProcessing ? (liveActive.track_index || 0) : 0;
- const liveTrackTotal = isLiveProcessing ? (liveActive.track_total || 0) : 0;
- const liveTrackName = isLiveProcessing ? (liveActive.track_name || '') : '';
-
- // Parse match data for track details
- let matchCount = 0, totalTracks = 0, trackDetails = [];
- if (r.match_data) {
- try {
- const md = typeof r.match_data === 'string' ? JSON.parse(r.match_data) : r.match_data;
- matchCount = md.matched_count || 0;
- totalTracks = md.total_tracks || 0;
- if (md.matches) {
- trackDetails = md.matches.map(m => ({
- name: m.track_name || m.track?.name || 'Unknown',
- file: m.file ? m.file.split(/[/\\]/).pop() : '?',
- confidence: Math.round((m.confidence || 0) * 100),
- }));
- }
- } catch (e) {}
- }
-
- let matchSummary = totalTracks > 0 ? `${matchCount}/${totalTracks} tracks` : `${r.total_files} files`;
- if (isLiveProcessing && liveTrackTotal > 0) {
- matchSummary = `track ${liveTrackIdx}/${liveTrackTotal}: ${liveTrackName}`;
- }
- const methodLabels = { tags: 'Tags', folder_name: 'Folder Name', acoustid: 'AcoustID', filename: 'Filename' };
- const methodLabel = methodLabels[r.identification_method] || r.identification_method || '';
-
- // Time ago
- let timeAgo = '';
- if (r.created_at) {
- try {
- const d = new Date(r.created_at);
- const diffM = Math.floor((Date.now() - d) / 60000);
- if (diffM < 1) timeAgo = 'just now';
- else if (diffM < 60) timeAgo = `${diffM}m ago`;
- else if (diffM < 1440) timeAgo = `${Math.floor(diffM / 60)}h ago`;
- else timeAgo = `${Math.floor(diffM / 1440)}d ago`;
- } catch (e) {}
- }
-
- let actions = '';
- if (r.status === 'pending_review') {
- actions = `
-
-
-
`;
- }
-
- // Expanded track list (hidden by default)
- let trackListHtml = '';
- if (trackDetails.length > 0) {
- trackListHtml = `
-
- ${trackDetails.map((t, tIdx) => {
- const tConfClass = t.confidence >= 90 ? 'high' : t.confidence >= 70 ? 'medium' : 'low';
- // 1-based liveTrackIdx — current row glows, prior rows dim as "done".
- let rowState = '';
- if (isLiveProcessing && liveTrackIdx > 0) {
- if (tIdx + 1 === liveTrackIdx) rowState = ' auto-import-track-row-active';
- else if (tIdx + 1 < liveTrackIdx) rowState = ' auto-import-track-row-done';
- }
- return `
- ${escapeHtml(t.name)}
- ${escapeHtml(t.file)}
- ${t.confidence}%
-
`;
- }).join('')}
-
`;
- }
-
- return `
-
-
- ${r.image_url ? `

\uD83D\uDCBF
` : `
\uD83D\uDCBF
`}
-
-
-
${escapeHtml(r.album_name || r.folder_name)}
-
${escapeHtml(r.artist_name || 'Unknown Artist')}
-
- ${matchSummary}
- ${methodLabel ? `${methodLabel}` : ''}
- ${timeAgo ? `${timeAgo}` : ''}
-
- ${r.error_message ? `
${escapeHtml(r.error_message)}
` : ''}
-
-
-
${statusIcon} ${statusLabel}
-
-
${confPct}% confidence
- ${actions}
-
-
-
${escapeHtml(r.folder_name)}
- ${trackListHtml}
-
`;
- }).join('');
-
- } catch (e) {}
-}
-
-async function _autoImportSaveSettings() {
- const confidence = (document.getElementById('auto-import-confidence')?.value || 90) / 100;
- const interval = parseInt(document.getElementById('auto-import-interval')?.value || 60);
- try {
- await fetch('/api/auto-import/settings', {
- method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ confidence_threshold: confidence, scan_interval: interval })
- });
- showToast('Settings saved', 'success');
- } catch (e) { showToast('Error', 'error'); }
-}
-
-function _autoImportSetFilter(filter) {
- _autoImportFilter = filter;
- document.querySelectorAll('#auto-import-filters .adl-pill').forEach(p =>
- p.classList.toggle('active', p.dataset.filter === filter));
- _autoImportLoadResults();
-}
-
-async function _autoImportScanNow() {
- try {
- const res = await fetch('/api/auto-import/scan-now', { method: 'POST' });
- const data = await res.json();
- if (data.success) {
- showToast('Scan triggered', 'success');
- _autoImportLoadStatus();
- } else {
- showToast(data.error || 'Failed to trigger scan', 'error');
- }
- } catch (e) { showToast('Error: ' + e.message, 'error'); }
-}
-
-async function _autoImportApproveAll() {
- const confirmed = await showConfirmDialog({
- title: 'Approve All',
- message: 'Approve and import all pending review items?',
- confirmText: 'Approve All',
- });
- if (!confirmed) return;
- try {
- const res = await fetch('/api/auto-import/approve-all', { method: 'POST' });
- const data = await res.json();
- if (data.success) {
- showToast(`Approved ${data.count || 0} items`, 'success');
- _autoImportLoadResults();
- } else {
- showToast(data.error || 'Failed', 'error');
- }
- } catch (e) { showToast('Error: ' + e.message, 'error'); }
-}
-
-async function _autoImportClearCompleted() {
- try {
- const res = await fetch('/api/auto-import/clear-completed', { method: 'POST' });
- const data = await res.json();
- if (data.success) {
- showToast(`Cleared ${data.count || 0} imported items`, 'success');
- _autoImportLoadResults();
- } else {
- showToast(data.error || 'Failed', 'error');
- }
- } catch (e) { showToast('Error: ' + e.message, 'error'); }
-}
-
-function _autoImportToggleDetail(idx) {
- const trackList = document.getElementById(`auto-import-tracks-${idx}`);
- if (trackList) {
- trackList.classList.toggle('expanded');
- }
-}
-window._autoImportToggleDetail = _autoImportToggleDetail;
-window._autoImportSetFilter = _autoImportSetFilter;
-window._autoImportScanNow = _autoImportScanNow;
-window._autoImportApproveAll = _autoImportApproveAll;
-window._autoImportClearCompleted = _autoImportClearCompleted;
-
-async function _autoImportApprove(id) {
- try {
- const res = await fetch(`/api/auto-import/approve/${id}`, { method: 'POST' });
- const data = await res.json();
- if (data.success) { showToast('Approved', 'success'); _autoImportLoadResults(); }
- else showToast(data.error || 'Failed', 'error');
- } catch (e) { showToast('Error', 'error'); }
-}
-
-async function _autoImportReject(id) {
- try {
- const res = await fetch(`/api/auto-import/reject/${id}`, { method: 'POST' });
- const data = await res.json();
- if (data.success) { showToast('Dismissed', 'success'); _autoImportLoadResults(); }
- else showToast(data.error || 'Failed', 'error');
- } catch (e) { showToast('Error', 'error'); }
-}
-
-// --- Album Tab: Auto-Detected Groups (from file tags) ---
-
-async function importPageLoadAutoGroups() {
- const grid = document.getElementById('import-page-suggestions-grid');
- if (!grid) return;
-
- try {
- const resp = await fetch('/api/import/staging/groups');
- if (!resp.ok) return;
- const data = await resp.json();
-
- if (!data.success || !data.groups || data.groups.length === 0) return;
-
- // Build auto-groups section above suggestions
- let groupsContainer = document.getElementById('import-page-auto-groups');
- if (!groupsContainer) {
- groupsContainer = document.createElement('div');
- groupsContainer.id = 'import-page-auto-groups';
- groupsContainer.style.marginBottom = '16px';
- const suggestionsSection = document.getElementById('import-page-suggestions');
- if (suggestionsSection) {
- suggestionsSection.parentNode.insertBefore(groupsContainer, suggestionsSection);
- } else {
- grid.parentNode.insertBefore(groupsContainer, grid);
- }
- }
-
- groupsContainer.innerHTML = `
-
- Auto-Detected Albums
-
-
- ${data.groups.map((g, idx) => `
-
-
- ${g.file_count}
-
-
-
${_esc(g.album)}
-
${_esc(g.artist)} · ${g.file_count} tracks
-
-
- `).join('')}
-
- `;
-
- // Store groups for click handler
- importPageState._autoGroups = data.groups;
- } catch (err) {
- console.warn('Failed to load auto-groups:', err);
- }
-}
-
-async function importPageMatchAutoGroup(groupIdx) {
- const group = importPageState._autoGroups?.[groupIdx];
- if (!group) return;
-
- // Search for the album by name + artist
- const query = `${group.artist} ${group.album}`;
- const searchInput = document.getElementById('import-page-album-search-input');
- if (searchInput) searchInput.value = query;
-
- // Hide suggestions/groups, show search results
- const suggestionsEl = document.getElementById('import-page-suggestions');
- const groupsEl = document.getElementById('import-page-auto-groups');
- if (suggestionsEl) suggestionsEl.style.display = 'none';
- if (groupsEl) groupsEl.style.display = 'none';
-
- const grid = document.getElementById('import-page-album-results');
- if (grid) grid.innerHTML = 'Searching...
';
-
- try {
- const resp = await fetch(`/api/import/search/albums?q=${encodeURIComponent(query)}&limit=12`);
- const data = await resp.json();
-
- if (data.success && data.albums && data.albums.length > 0) {
- // Store file_paths filter so match only includes this group's files
- importPageState._autoGroupFilePaths = group.file_paths;
-
- // Render results — user picks the right album
- const banner = _renderImportFallbackBanner(data.albums, data.primary_source);
- grid.innerHTML = banner + data.albums.map(a => _renderSuggestionCard(a, data.primary_source)).join('');
- } else {
- grid.innerHTML = 'No albums found — try searching manually
';
- }
- } catch (err) {
- console.error('Auto-group search failed:', err);
- if (grid) grid.innerHTML = 'Search failed
';
- }
-}
-
-// --- Album Tab: Suggestions (server-side cache, just fetch and render) ---
-
-async function importPageLoadSuggestions() {
- const section = document.getElementById('import-page-suggestions');
- const grid = document.getElementById('import-page-suggestions-grid');
- if (!section || !grid) return;
-
- try {
- const resp = await fetch('/api/import/staging/suggestions');
- if (!resp.ok) return;
- const data = await resp.json();
-
- if (!data.success || !data.suggestions || data.suggestions.length === 0) {
- if (!data.ready) {
- // Server is still building cache — show placeholder, retry shortly
- section.style.display = '';
- grid.innerHTML = 'Loading suggestions...
';
- setTimeout(() => importPageLoadSuggestions(), 3000);
- } else {
- section.style.display = 'none';
- grid.innerHTML = '';
- }
- return;
- }
-
- section.style.display = '';
- const banner = _renderImportFallbackBanner(data.suggestions, data.primary_source);
- grid.innerHTML = banner + data.suggestions.map(a => _renderSuggestionCard(a, data.primary_source)).join('');
- } catch (err) {
- // Network error or server not ready — fail silently
- console.warn('Failed to load import suggestions:', err);
- }
-}
-
-function _renderSuggestionCard(a, primarySource) {
- // Cache the album lookup so importPageSelectAlbum can pull source +
- // name + artist on click (the onclick can only carry the ID string
- // — see github issue #524 root cause).
- importPageState._albumLookup[a.id] = {
- id: a.id, name: a.name || '', artist: a.artist || '', source: a.source || '',
- };
- // Surface the served source when it differs from the user's configured
- // primary — the search route silently falls through to the next source
- // in METADATA_SOURCE_PRIORITY when the primary returns nothing
- // (intentional design, see core/auto_import_worker.py:1316). Without
- // this badge the user has no idea their MusicBrainz / Discogs choice
- // got bypassed (github issue #681).
- const sourceBadge = (a.source && primarySource && a.source !== primarySource)
- ? `via ${_esc((SOURCE_LABELS[a.source] || {}).text || a.source)}
`
- : '';
- const metaParts = [
- `${a.total_tracks || 0} tracks`,
- a.release_date ? a.release_date.substring(0, 4) : '',
- a.format || '',
- a.country || '',
- a.disambiguation || '',
- ].filter(Boolean);
- const details = [a.status || '', a.label || ''].filter(Boolean);
- const detailsLine = details.length
- ? `${_esc(details.join(' · '))}
`
- : '';
- return `
-

-
${_esc(a.name)}
-
${_esc(a.artist)}
-
${_esc(metaParts.join(' · '))}
- ${detailsLine}
- ${sourceBadge}
-
`;
-}
-
-function _renderImportFallbackBanner(albums, primarySource) {
- if (!primarySource || !albums || !albums.length) return '';
- const allFallback = albums.every(a => a.source && a.source !== primarySource);
- if (!allFallback) return '';
- const servedSource = albums[0].source;
- const primaryLabel = (SOURCE_LABELS[primarySource] || {}).text || primarySource;
- const servedLabel = (SOURCE_LABELS[servedSource] || {}).text || servedSource;
- // Neutral wording — covers both live-search fallback (primary returned 0)
- // and cache-stale suggestions (primary changed since cache was built).
- return `Showing ${_esc(servedLabel)} results — not from your primary source (${_esc(primaryLabel)}).
`;
-}
-
-// --- Album Tab: Search ---
-
-async function importPageSearchAlbum() {
- const query = document.getElementById('import-page-album-search-input').value.trim();
- if (!query) return;
-
- document.getElementById('import-page-suggestions').style.display = 'none';
- const groupsEl = document.getElementById('import-page-auto-groups');
- if (groupsEl) groupsEl.style.display = 'none';
- const grid = document.getElementById('import-page-album-results');
- grid.innerHTML = 'Searching...
';
-
- try {
- const resp = await fetch(`/api/import/search/albums?q=${encodeURIComponent(query)}&limit=12`);
- const data = await resp.json();
- if (!data.success || !data.albums.length) {
- grid.innerHTML = 'No albums found
';
- return;
- }
- const banner = _renderImportFallbackBanner(data.albums, data.primary_source);
- grid.innerHTML = banner + data.albums.map(a => _renderSuggestionCard(a, data.primary_source)).join('');
- document.getElementById('import-page-album-clear-btn').classList.remove('hidden');
- } catch (err) {
- grid.innerHTML = `Error: ${err.message}
`;
- }
-}
-
-// --- Album Tab: Select Album & Match ---
-
-async function importPageSelectAlbum(albumId) {
- document.getElementById('import-page-album-search-section').classList.add('hidden');
- document.getElementById('import-page-album-match-section').classList.remove('hidden');
-
- const matchList = document.getElementById('import-page-match-list');
- matchList.innerHTML = 'Matching files to tracklist...
';
-
- try {
- // Include file_paths filter if matching from an auto-group.
- // CRITICAL: include source + album_name + album_artist from the
- // search/suggestion result. Without `source`, the backend can't
- // route the lookup to the metadata source the album_id came
- // from — for example a Deezer album_id needs Deezer's get_album
- // call. Cross-source lookup fails silently, returns the
- // failure-fallback dict with album_id-as-name + Unknown Artist
- // + 0 tracks, then the import flow writes that broken metadata
- // to the library DB (github issue #524).
- const cached = importPageState._albumLookup[albumId] || {};
- const matchBody = {
- album_id: albumId,
- source: cached.source || '',
- album_name: cached.name || '',
- album_artist: cached.artist || '',
- };
- if (importPageState._autoGroupFilePaths) {
- matchBody.file_paths = importPageState._autoGroupFilePaths;
- importPageState._autoGroupFilePaths = null; // clear after use
- }
- const resp = await fetch('/api/import/album/match', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(matchBody)
- });
- const data = await resp.json();
- if (!data.success) {
- matchList.innerHTML = `Error: ${data.error}
`;
- return;
- }
-
- importPageState.albumData = data;
- importPageState.matchOverrides = {};
-
- // Render hero
- const album = data.album;
- const heroMetaParts = [
- `${album.total_tracks || 0} tracks`,
- album.release_date ? album.release_date.substring(0, 4) : '',
- album.format || '',
- album.country || '',
- album.disambiguation || '',
- ].filter(Boolean);
- document.getElementById('import-page-album-hero').innerHTML = `
-
-
-
${_esc(album.name)}
-
${_esc(album.artist)}
-
${_esc(heroMetaParts.join(' · '))}
-
- `;
-
- importPageRenderMatchList();
- } catch (err) {
- matchList.innerHTML = `Error: ${err.message}
`;
- }
-}
-
-function importPageRenderMatchList() {
- const data = importPageState.albumData;
- if (!data) return;
-
- const matchList = document.getElementById('import-page-match-list');
- const overrides = importPageState.matchOverrides;
-
- // Build effective matches: auto-match overridden by manual overrides
- // Also track which staging files are used (auto or override)
- const usedStagingFiles = new Set();
-
- // First pass: collect overridden indices
- Object.values(overrides).forEach(sfIdx => usedStagingFiles.add(sfIdx));
-
- // Build rows
- let matchedCount = 0;
- const rows = data.matches.map((m, idx) => {
- const trackInfo = _importPageGetTrackDisplayInfo(m, idx);
- let file = null;
- let confidence = m.confidence;
- let isOverride = false;
-
- if (overrides.hasOwnProperty(idx)) {
- const sfIdx = overrides[idx];
- if (sfIdx === -1) {
- // Forcibly unmatched — no file
- file = null;
- } else {
- // Manual override
- file = importPageState.stagingFiles[sfIdx] || null;
- confidence = 1.0;
- isOverride = true;
- usedStagingFiles.add(sfIdx);
- }
- } else if (m.staging_file) {
- file = m.staging_file;
- // Check if this file was reassigned to another track via override
- const autoFileName = m.staging_file.filename;
- const reassigned = Object.entries(overrides).some(([tIdx, sfIdx]) => {
- const sf = importPageState.stagingFiles[sfIdx];
- return sf && sf.filename === autoFileName && parseInt(tIdx) !== idx;
- });
- if (!reassigned) {
- usedStagingFiles.add(-1); // placeholder — auto-matched file
- } else {
- file = null; // file was reassigned elsewhere
- }
- }
-
- if (file) matchedCount++;
- const confPercent = Math.round(confidence * 100);
- const confClass = confidence >= 0.7 ? '' : 'low';
-
- return `
-
- ${trackInfo.displayTrackNumber}
- ${_esc(trackInfo.name)}
-
- ${file
- ? `${_esc(file.filename)}
- ${confPercent}%`
- : `Drop a file here`}
-
- ${file ? `` : ''}
-
- `;
- });
-
- matchList.innerHTML = rows.join('');
-
- // Unmatched file pool
- const unmatchedFiles = [];
- importPageState.stagingFiles.forEach((f, i) => {
- // Check if used by override
- if (Object.values(overrides).includes(i)) return;
- // Check if used by auto-match (not overridden away)
- const autoUsed = data.matches.some((m, mIdx) => {
- if (overrides.hasOwnProperty(mIdx)) return false;
- return m.staging_file && m.staging_file.filename === f.filename;
- });
- if (autoUsed) return;
- unmatchedFiles.push({ file: f, index: i });
- });
-
- const poolChips = document.getElementById('import-page-pool-chips');
- document.getElementById('import-page-unmatched-count').textContent = unmatchedFiles.length;
-
- if (unmatchedFiles.length === 0) {
- poolChips.innerHTML = 'All files matched';
- } else {
- poolChips.innerHTML = unmatchedFiles.map(({ file, index }) => `
-
- ${_esc(file.filename)}
-
- `).join('');
- }
-
- // Stats & button
- document.getElementById('import-page-match-stats').textContent = `${matchedCount} of ${data.matches.length} tracks matched`;
- const processBtn = document.getElementById('import-page-album-process-btn');
- processBtn.disabled = matchedCount === 0;
- processBtn.textContent = `Process ${matchedCount} Track${matchedCount !== 1 ? 's' : ''}`;
-}
-
-function _importPageGetTrackDisplayInfo(item, index) {
- const track = item?.track || item?.spotify_track || {};
- const rawTrackNumber = track.track_number ?? track.trackNumber ?? null;
- const trackNumber = rawTrackNumber === null || rawTrackNumber === undefined || rawTrackNumber === ''
- ? null
- : String(rawTrackNumber).split('/')[0].trim();
-
- return {
- track,
- name: track.name || track.title || `Track ${index + 1}`,
- trackNumber,
- displayTrackNumber: trackNumber || String(index + 1),
- };
-}
-
-// --- Album Tab: Drag and Drop ---
-
-function importPageStartDrag(event, stagingFileIndex) {
- event.dataTransfer.setData('text/plain', stagingFileIndex.toString());
- event.dataTransfer.effectAllowed = 'move';
-}
-
-function importPageHandleDragOver(event) {
- event.preventDefault();
- event.dataTransfer.dropEffect = 'move';
- event.currentTarget.classList.add('drag-over');
- // Remove drag-over from others
- document.querySelectorAll('.import-page-match-row.drag-over').forEach(el => {
- if (el !== event.currentTarget) el.classList.remove('drag-over');
- });
-}
-
-function importPageHandleDrop(event, trackIndex) {
- event.preventDefault();
- event.currentTarget.classList.remove('drag-over');
- const stagingFileIndex = parseInt(event.dataTransfer.getData('text/plain'));
- if (isNaN(stagingFileIndex)) return;
-
- // Remove this staging file from any other track it was assigned to
- Object.keys(importPageState.matchOverrides).forEach(k => {
- if (importPageState.matchOverrides[k] === stagingFileIndex) {
- delete importPageState.matchOverrides[k];
- }
- });
-
- importPageState.matchOverrides[trackIndex] = stagingFileIndex;
- importPageState.tapSelectedChip = null;
- importPageRenderMatchList();
-}
-
-// Mobile tap-to-assign fallback
-function importPageTapSelectChip(stagingFileIndex) {
- if (importPageState.tapSelectedChip === stagingFileIndex) {
- importPageState.tapSelectedChip = null;
- } else {
- importPageState.tapSelectedChip = stagingFileIndex;
- }
- importPageRenderMatchList();
-}
-
-function importPageTapAssign(trackIndex) {
- if (importPageState.tapSelectedChip === null) return;
- const stagingFileIndex = importPageState.tapSelectedChip;
-
- // Remove from any other track
- Object.keys(importPageState.matchOverrides).forEach(k => {
- if (importPageState.matchOverrides[k] === stagingFileIndex) {
- delete importPageState.matchOverrides[k];
- }
- });
-
- importPageState.matchOverrides[trackIndex] = stagingFileIndex;
- importPageState.tapSelectedChip = null;
- importPageRenderMatchList();
-}
-
-function importPageUnmatchTrack(trackIndex) {
- delete importPageState.matchOverrides[trackIndex];
- // Also remove auto-match by setting override to -1 special value? No — just delete override and let auto-match stay.
- // Actually, to truly unmatch: we need to suppress the auto-match too.
- // We'll use a sentinel: override = -1 means "forcibly unmatched"
- const m = importPageState.albumData?.matches[trackIndex];
- if (m && m.staging_file) {
- importPageState.matchOverrides[trackIndex] = -1; // sentinel: force no match
- }
- importPageRenderMatchList();
-}
-
-function importPageAutoRematch() {
- importPageState.matchOverrides = {};
- importPageState.tapSelectedChip = null;
- importPageRenderMatchList();
-}
-
-// --- Album Tab: Process ---
-
-function importPageProcessAlbum() {
- const data = importPageState.albumData;
- if (!data) return;
-
- // Build effective matches with overrides applied
- const overrides = importPageState.matchOverrides;
- const effectiveMatches = [];
- data.matches.forEach((m, idx) => {
- if (overrides.hasOwnProperty(idx)) {
- if (overrides[idx] === -1) return; // forcibly unmatched — skip
- const sf = importPageState.stagingFiles[overrides[idx]];
- effectiveMatches.push({ ...m, staging_file: sf, confidence: 1.0 });
- } else if (m.staging_file !== null) {
- effectiveMatches.push(m);
- }
- });
-
- if (effectiveMatches.length === 0) return;
-
- // Add to queue and reset search immediately so user can queue more
- const album = data.album;
- _importQueueAdd({
- type: 'album',
- label: album.name,
- sublabel: `${album.artist} · ${effectiveMatches.length} tracks`,
- imageUrl: album.image_url,
- items: effectiveMatches,
- albumData: album,
- });
-
- importPageResetAlbumSearch();
-}
-
-function importPageResetAlbumSearch() {
- importPageState.albumData = null;
- importPageState.matchOverrides = {};
- importPageState.tapSelectedChip = null;
- importPageState._autoGroupFilePaths = null;
-
- document.getElementById('import-page-album-search-section').classList.remove('hidden');
- document.getElementById('import-page-album-match-section').classList.add('hidden');
-
- // Clear search
- document.getElementById('import-page-album-results').innerHTML = '';
- document.getElementById('import-page-album-search-input').value = '';
- document.getElementById('import-page-album-clear-btn').classList.add('hidden');
-
- // Re-show auto-groups
- const groupsEl = document.getElementById('import-page-auto-groups');
- if (groupsEl) groupsEl.style.display = '';
-
- // Refresh suggestions & staging
- importPageLoadAutoGroups();
- importPageLoadSuggestions();
- importPageRefreshStaging();
-}
-
-// --- Singles Tab ---
-
-function importPageRenderSinglesList() {
- const list = document.getElementById('import-page-singles-list');
- const files = importPageState.stagingFiles;
-
- if (files.length === 0) {
- list.innerHTML = 'No audio files found in import folder
';
- return;
- }
-
- list.innerHTML = files.map((f, i) => {
- const isSelected = importPageState.selectedSingles.has(i);
- const manualMatch = importPageState.singlesManualMatches[i];
- const searchOpen = document.querySelector(`[data-singles-search="${i}"]`);
-
- let html = `
-
-
-
-
${_esc(f.filename)}
-
- ${f.title ? `${_esc(f.title)}` : ''}
- ${f.artist ? `${_esc(f.artist)}` : ''}
- ${f.extension ? `${f.extension}` : ''}
-
- ${manualMatch ? `
-
- ✓ ${_esc(manualMatch.name)} - ${_esc(manualMatch.artist)}
- change
-
- ` : ''}
-
-
-
-
-
- `;
- return html;
- }).join('');
-
- importPageUpdateSinglesProcessButton();
-}
-
-function importPageToggleSingle(idx) {
- if (importPageState.selectedSingles.has(idx)) {
- importPageState.selectedSingles.delete(idx);
- } else {
- importPageState.selectedSingles.add(idx);
- }
- // Update checkbox UI without full re-render
- const item = document.querySelector(`[data-single-idx="${idx}"]`);
- if (item) {
- const cb = item.querySelector('.import-page-single-checkbox');
- if (cb) cb.classList.toggle('checked', importPageState.selectedSingles.has(idx));
- }
- importPageUpdateSinglesProcessButton();
-}
-
-function importPageSelectAllSingles() {
- const allSelected = importPageState.selectedSingles.size === importPageState.stagingFiles.length;
- if (allSelected) {
- importPageState.selectedSingles.clear();
- } else {
- importPageState.stagingFiles.forEach((_, i) => importPageState.selectedSingles.add(i));
- }
- document.getElementById('import-page-select-all-text').textContent = allSelected ? 'Select All' : 'Deselect All';
- // Update all checkboxes
- document.querySelectorAll('.import-page-single-checkbox').forEach((cb, i) => {
- cb.classList.toggle('checked', importPageState.selectedSingles.has(i));
- });
- importPageUpdateSinglesProcessButton();
-}
-
-function importPageUpdateSinglesProcessButton() {
- const btn = document.getElementById('import-page-singles-process-btn');
- const count = importPageState.selectedSingles.size;
- btn.textContent = `Process Selected (${count})`;
- btn.disabled = count === 0;
-}
-
-function importPageOpenSingleSearch(fileIdx) {
- const item = document.querySelector(`[data-single-idx="${fileIdx}"]`);
- if (!item) return;
-
- // Remove any existing search panel
- const existing = item.querySelector('.import-page-single-search-panel');
- if (existing) {
- existing.remove();
- return;
- }
-
- // Close other open panels
- document.querySelectorAll('.import-page-single-search-panel').forEach(p => p.remove());
-
- const f = importPageState.stagingFiles[fileIdx];
- const defaultQuery = [f.artist, f.title].filter(Boolean).join(' ') || f.filename.replace(/\.[^.]+$/, '');
-
- const panel = document.createElement('div');
- panel.className = 'import-page-single-search-panel';
- panel.innerHTML = `
-
-
-
-
-
- `;
- item.appendChild(panel);
-
- // Auto-search
- const input = panel.querySelector('input');
- input.focus();
- if (defaultQuery) {
- importPageSearchSingleTrack(fileIdx, defaultQuery);
- }
-}
-
-async function importPageSearchSingleTrack(fileIdx, query) {
- if (!query || !query.trim()) return;
-
- const resultsDiv = document.getElementById(`import-single-results-${fileIdx}`);
- if (!resultsDiv) return;
- resultsDiv.innerHTML = 'Searching...
';
-
- try {
- const resp = await fetch(`/api/import/search/tracks?q=${encodeURIComponent(query.trim())}&limit=6`);
- const data = await resp.json();
- if (!data.success || !data.tracks.length) {
- resultsDiv.innerHTML = 'No results found
';
- return;
- }
- // Store results in a temp cache so we can reference by index
- window._importSingleSearchResults = window._importSingleSearchResults || {};
- window._importSingleSearchResults[fileIdx] = data.tracks;
-
- resultsDiv.innerHTML = data.tracks.map((t, tIdx) => {
- const dur = t.duration_ms ? `${Math.floor(t.duration_ms / 60000)}:${String(Math.floor((t.duration_ms % 60000) / 1000)).padStart(2, '0')}` : '';
- return `
-
- ${t.image_url ? `

` : ''}
-
-
${_esc(t.name)} - ${_esc(t.artist)}
-
${_esc(t.album)}${dur ? ' · ' + dur : ''}
-
-
-
- `;
- }).join('');
- } catch (err) {
- resultsDiv.innerHTML = `Error: ${err.message}
`;
- }
-}
-
-function importPageSelectSingleMatch(fileIdx, trackIdx) {
- const trackData = window._importSingleSearchResults?.[fileIdx]?.[trackIdx];
- if (!trackData) return;
- importPageState.singlesManualMatches[fileIdx] = trackData;
-
- // Auto-select this file
- importPageState.selectedSingles.add(fileIdx);
-
- // Close search panel and re-render this item
- importPageRenderSinglesList();
-}
-
-// --- Singles Tab: Process ---
-
-function importPageProcessSingles() {
- if (importPageState.selectedSingles.size === 0) return;
-
- const filesToProcess = Array.from(importPageState.selectedSingles).map(i => {
- const f = importPageState.stagingFiles[i];
- const manualMatch = importPageState.singlesManualMatches[i];
- if (manualMatch) {
- return { ...f, manual_match: manualMatch };
- }
- return f;
- });
-
- // Add to queue and reset immediately
- _importQueueAdd({
- type: 'singles',
- label: `${filesToProcess.length} Single${filesToProcess.length !== 1 ? 's' : ''}`,
- sublabel: filesToProcess.map(f => f.title || f.filename).slice(0, 3).join(', ') + (filesToProcess.length > 3 ? '...' : ''),
- imageUrl: null,
- items: filesToProcess,
- });
-
- importPageState.selectedSingles.clear();
- importPageState.singlesManualMatches = {};
- importPageUpdateSinglesProcessButton();
- importPageRefreshStaging();
-}
-
-// --- Processing Queue ---
-
-const _importQueue = []; // { id, type, label, sublabel, imageUrl, status, processed, total, errors }
-
-function _importQueueAdd(job) {
- const id = ++importJobIdCounter;
- const entry = {
- id,
- type: job.type,
- label: job.label,
- sublabel: job.sublabel,
- imageUrl: job.imageUrl,
- status: 'running', // running | done | error
- processed: 0,
- total: job.items.length,
- errors: [],
- };
- _importQueue.push(entry);
- _importQueueRender();
-
- // Fire and forget — runs in background
- _importQueueRunJob(entry, job);
-}
-
-async function _importQueueRunJob(entry, job) {
- for (let i = 0; i < job.items.length; i++) {
- const itemName = job.type === 'album'
- ? _importPageGetTrackDisplayInfo(job.items[i], i).name
- : (job.items[i].title || job.items[i].filename || `File ${i + 1}`);
-
- // Update status with current track info
- entry.sublabel = `Processing ${i + 1}/${job.items.length}: ${itemName}`;
- _importQueueRender();
-
- try {
- let resp;
- if (job.type === 'album') {
- resp = await fetch('/api/import/album/process', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- album: job.albumData,
- matches: [job.items[i]]
- })
- });
- } else {
- resp = await fetch('/api/import/singles/process', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ files: [job.items[i]] })
- });
- }
-
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- const data = await resp.json();
- if (data.success) entry.processed += (data.processed || 0);
- if (data.errors && data.errors.length > 0) entry.errors.push(...data.errors);
- } catch (err) {
- entry.errors.push(`${itemName}: ${err.message}`);
- }
-
- _importQueueRender();
- }
-
- entry.status = entry.errors.length > 0 && entry.processed === 0 ? 'error' : 'done';
- _importQueueRender();
-
- // Refresh staging and suggestions since files moved
- importPageRefreshStaging();
- importPageLoadSuggestions();
-}
-
-function _importQueueRender() {
- const container = document.getElementById('import-page-queue');
- const list = document.getElementById('import-page-queue-list');
- const clearBtn = document.getElementById('import-page-queue-clear');
- if (!container || !list) return;
-
- if (_importQueue.length === 0) {
- container.classList.add('hidden');
- return;
- }
-
- container.classList.remove('hidden');
-
- // Show clear button only if there are finished jobs
- const hasFinished = _importQueue.some(j => j.status !== 'running');
- clearBtn.style.display = hasFinished ? '' : 'none';
-
- list.innerHTML = _importQueue.map(j => {
- const pct = j.total > 0 ? Math.round((j.processed / j.total) * 100) : 0;
- const fillClass = j.status === 'error' ? 'error' : '';
- let statusText, statusClass;
- if (j.status === 'running') {
- statusText = `${j.processed}/${j.total}`;
- statusClass = '';
- } else if (j.status === 'done') {
- statusText = j.errors.length > 0 ? `${j.processed}/${j.total} (${j.errors.length} err)` : 'Done';
- statusClass = j.errors.length > 0 ? 'error' : 'done';
- } else {
- statusText = 'Failed';
- statusClass = 'error';
- }
-
- return `
-
- ${j.imageUrl
- ? `

`
- : `
♪
`}
-
-
${_esc(j.label)}
-
${_esc(j.sublabel)}
-
-
-
- `;
- }).join('');
-}
-
-function importPageClearFinishedJobs() {
- for (let i = _importQueue.length - 1; i >= 0; i--) {
- if (_importQueue[i].status !== 'running') {
- _importQueue.splice(i, 1);
- }
- }
- _importQueueRender();
-}
-
// ── Import File Tab ──────────────────────────────────────────────────
let _importFileState = {
@@ -6056,14 +4682,15 @@ async function playArtistRadio() {
const albumArt = random.album.thumb_url || data.artist?.thumb_url || null;
// Clear existing queue and disable radio before starting fresh
- npSetRadioMode(false, { toast: false });
+ npRadioMode = false;
clearQueue();
if (audioPlayer && !audioPlayer.paused) {
audioPlayer.pause();
}
- // Play the track first so currentTrack is populated before radio seeds the queue.
- await playLibraryTrack({
+ // Play the track first, then enable radio mode after a short delay
+ // so currentTrack is set and the radio queue fill triggers
+ playLibraryTrack({
id: random.track.id,
title: random.track.title,
file_path: random.track.file_path,
@@ -6072,9 +4699,14 @@ async function playArtistRadio() {
album_id: random.album.id,
}, random.album.title || '', artistName);
- npSetRadioMode(true, { toast: false, fetchIfNeeded: true });
+ // Enable radio mode after track starts loading
+ setTimeout(() => {
+ npRadioMode = true;
+ const radioBtn = document.querySelector('.np-radio-btn');
+ if (radioBtn) radioBtn.classList.add('active');
+ }, 1000);
- showToast(`Playing ${artistName} radio - similar tracks will auto-queue`, 'success');
+ showToast(`Playing ${artistName} radio — similar tracks will auto-queue`, 'success');
} catch (e) {
showToast(`Failed to start artist radio: ${e.message}`, 'error');
}
diff --git a/webui/static/style.css b/webui/static/style.css
index ea65ca7f..7706efc0 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -10376,8 +10376,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
border-color: rgba(167, 139, 250, 0.25);
}
-.library-history-badge.download.source-staging,
-.library-history-badge.download.source-auto-import {
+.library-history-badge.download.source-staging {
color: rgba(255, 255, 255, 0.55);
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.12);
@@ -40072,956 +40071,6 @@ div.artist-hero-badge {
opacity: 1;
}
-/* ========================================
- IMPORT PAGE (full page, replaces modal)
- ======================================== */
-
-/* ============================================================================
- IMPORT PAGE
- ============================================================================ */
-
-.import-page-container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 24px 32px;
-}
-
-.import-page-header {
- margin-bottom: 20px;
-}
-
-.import-page-title-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
-}
-
-.import-page-title {
- font-size: 28px;
- font-weight: 700;
- margin: 0;
- letter-spacing: -0.3px;
- font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
- display: flex;
- align-items: center;
- gap: 14px;
-}
-
-.import-page-title > span {
- background: linear-gradient(90deg, #ffffff 0%, rgb(var(--accent-light-rgb)) 50%, rgb(var(--accent-rgb)) 100%);
- background-size: 200% 100%;
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- animation: page-title-shimmer 6s ease-in-out infinite;
-}
-
-.import-page-refresh-btn {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 16px;
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.12);
- border-radius: 8px;
- color: #ccc;
- font-size: 13px;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.import-page-refresh-btn:hover {
- background: rgba(255, 255, 255, 0.14);
- color: #fff;
-}
-
-.import-page-staging-bar {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 10px 16px;
- background: rgba(255, 255, 255, 0.04);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 10px;
- font-size: 13px;
- color: rgba(255, 255, 255, 0.5);
-}
-
-.import-staging-path {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.import-staging-stats {
- color: rgb(var(--accent-light-rgb));
- font-weight: 500;
- white-space: nowrap;
-}
-
-/* Tab Bar */
-.import-page-tab-bar {
- display: flex;
- gap: 4px;
- margin-bottom: 24px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
- padding-bottom: 0;
-}
-
-.import-page-tab {
- padding: 10px 24px;
- background: none;
- border: none;
- border-bottom: 2px solid transparent;
- color: rgba(255, 255, 255, 0.5);
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s;
- margin-bottom: -1px;
-}
-
-.import-page-tab:hover {
- color: rgba(255, 255, 255, 0.8);
-}
-
-.import-page-tab.active {
- color: rgb(var(--accent-light-rgb));
- border-bottom-color: rgb(var(--accent-light-rgb));
-}
-
-.import-page-tab-content {
- display: none;
-}
-
-.import-page-tab-content.active {
- display: block;
-}
-
-/* Section labels */
-.import-page-section-label {
- font-size: 13px;
- color: rgba(255, 255, 255, 0.4);
- margin-bottom: 12px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- font-weight: 500;
-}
-
-/* Search bar */
-.import-page-search-bar {
- display: flex;
- gap: 8px;
- margin-bottom: 20px;
- position: relative;
-}
-
-.import-page-search-input {
- flex: 1;
- padding: 10px 16px;
- background: rgba(255, 255, 255, 0.06);
- border: 1px solid rgba(255, 255, 255, 0.12);
- border-radius: 10px;
- color: #fff;
- font-size: 14px;
- outline: none;
- transition: border-color 0.2s;
-}
-
-.import-page-search-input:focus {
- border-color: rgba(var(--accent-light-rgb), 0.5);
-}
-
-.import-page-search-btn {
- padding: 10px 20px;
- background: rgb(var(--accent-light-rgb));
- border: none;
- border-radius: 10px;
- color: #000;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- transition: background 0.2s;
- white-space: nowrap;
-}
-
-.import-page-search-btn:hover {
- background: #1fdf64;
-}
-
-.import-page-clear-btn {
- position: absolute;
- right: 100px;
- top: 50%;
- transform: translateY(-50%);
- background: none;
- border: none;
- color: rgba(255, 255, 255, 0.4);
- font-size: 16px;
- cursor: pointer;
- padding: 4px 8px;
-}
-
-.import-page-clear-btn:hover {
- color: #fff;
-}
-
-/* Album grid */
-.import-page-album-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
- gap: 16px;
- margin-bottom: 24px;
-}
-
-.import-page-album-card {
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 12px;
- padding: 12px;
- cursor: pointer;
- transition: all 0.2s;
- text-align: center;
-}
-
-.import-page-album-card:hover {
- background: rgba(255, 255, 255, 0.1);
- border-color: rgba(var(--accent-light-rgb), 0.3);
- transform: translateY(-2px);
-}
-
-.import-page-album-card img {
- width: 100%;
- aspect-ratio: 1;
- object-fit: cover;
- border-radius: 8px;
- margin-bottom: 8px;
-}
-
-.import-page-album-card-title {
- font-size: 13px;
- font-weight: 600;
- color: #fff;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- margin-bottom: 2px;
-}
-
-.import-page-album-card-artist {
- font-size: 11px;
- color: rgba(255, 255, 255, 0.5);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.import-page-album-card-meta {
- font-size: 10px;
- color: rgba(255, 255, 255, 0.3);
- margin-top: 4px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.import-page-album-card-detail {
- font-size: 10px;
- color: rgba(255, 255, 255, 0.36);
- margin-top: 2px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.import-page-album-card-source {
- font-size: 10px;
- color: rgba(255, 200, 100, 0.75);
- margin-top: 2px;
- font-style: italic;
-}
-
-.import-page-fallback-banner {
- grid-column: 1 / -1;
- padding: 10px 14px;
- margin-bottom: 12px;
- background: rgba(255, 200, 100, 0.08);
- border: 1px solid rgba(255, 200, 100, 0.25);
- border-radius: 8px;
- color: rgba(255, 220, 170, 0.9);
- font-size: 12px;
- line-height: 1.4;
-}
-
-/* Album hero (selected album) */
-.import-page-album-hero {
- display: flex;
- gap: 20px;
- padding: 20px;
- background: rgba(255, 255, 255, 0.04);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 14px;
- margin-bottom: 20px;
- align-items: center;
-}
-
-.import-page-album-hero img {
- width: 120px;
- height: 120px;
- border-radius: 10px;
- object-fit: cover;
- flex-shrink: 0;
-}
-
-.import-page-album-hero-info {
- flex: 1;
- min-width: 0;
-}
-
-.import-page-album-hero-title {
- font-size: 22px;
- font-weight: 700;
- color: #fff;
- margin-bottom: 4px;
-}
-
-.import-page-album-hero-artist {
- font-size: 15px;
- color: rgba(255, 255, 255, 0.6);
- margin-bottom: 4px;
-}
-
-.import-page-album-hero-meta {
- font-size: 12px;
- color: rgba(255, 255, 255, 0.35);
-}
-
-/* Match header */
-.import-page-match-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 16px;
-}
-
-.import-page-match-header h3 {
- font-size: 16px;
- font-weight: 600;
- color: #fff;
- margin: 0;
-}
-
-.import-page-match-actions {
- display: flex;
- gap: 8px;
-}
-
-/* Match list */
-.import-page-match-list {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-bottom: 16px;
-}
-
-.import-page-match-row {
- display: grid;
- grid-template-columns: 36px 1fr 1fr 36px;
- align-items: center;
- gap: 12px;
- padding: 10px 14px;
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid rgba(255, 255, 255, 0.06);
- border-radius: 10px;
- min-height: 44px;
- transition: all 0.2s;
-}
-
-.import-page-match-row.drag-over {
- background: rgba(var(--accent-light-rgb), 0.08);
- border-color: rgba(var(--accent-light-rgb), 0.4);
- box-shadow: 0 0 12px rgba(var(--accent-light-rgb), 0.15);
-}
-
-.import-page-match-row.matched {
- border-color: rgba(var(--accent-light-rgb), 0.2);
-}
-
-.import-page-match-num {
- font-size: 13px;
- color: rgba(255, 255, 255, 0.3);
- text-align: center;
- font-weight: 500;
-}
-
-.import-page-match-track {
- font-size: 13px;
- color: #fff;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.import-page-match-file {
- font-size: 12px;
- color: rgba(255, 255, 255, 0.4);
- display: flex;
- align-items: center;
- gap: 6px;
- overflow: hidden;
-}
-
-.import-page-match-file.has-file {
- color: rgb(var(--accent-light-rgb));
-}
-
-.import-page-match-file-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.import-page-match-confidence {
- font-size: 10px;
- padding: 2px 6px;
- border-radius: 4px;
- background: rgba(var(--accent-light-rgb), 0.15);
- color: rgb(var(--accent-light-rgb));
- font-weight: 500;
- white-space: nowrap;
-}
-
-.import-page-match-confidence.low {
- background: rgba(255, 165, 0, 0.15);
- color: #ffa500;
-}
-
-.import-page-match-drop-zone {
- font-size: 11px;
- color: rgba(255, 255, 255, 0.25);
- font-style: italic;
-}
-
-.import-page-match-unmatch {
- background: none;
- border: none;
- color: rgba(255, 255, 255, 0.3);
- font-size: 14px;
- cursor: pointer;
- padding: 4px;
- border-radius: 4px;
- transition: all 0.2s;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.import-page-match-unmatch:hover {
- color: #ff4444;
- background: rgba(255, 68, 68, 0.1);
-}
-
-/* Unmatched file pool */
-.import-page-unmatched-pool {
- padding: 16px;
- background: rgba(255, 255, 255, 0.02);
- border: 1px dashed rgba(255, 255, 255, 0.1);
- border-radius: 12px;
- margin-bottom: 16px;
-}
-
-.import-page-pool-label {
- font-size: 12px;
- color: rgba(255, 255, 255, 0.4);
- margin-bottom: 10px;
- font-weight: 500;
-}
-
-.import-page-pool-chips {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-}
-
-.import-page-file-chip {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 12px;
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.12);
- border-radius: 20px;
- font-size: 12px;
- color: #ccc;
- cursor: grab;
- transition: all 0.2s;
- user-select: none;
-}
-
-.import-page-file-chip:active {
- cursor: grabbing;
-}
-
-.import-page-file-chip:hover {
- background: rgba(255, 255, 255, 0.14);
- border-color: rgba(var(--accent-light-rgb), 0.3);
- color: #fff;
-}
-
-.import-page-file-chip.selected {
- background: rgba(var(--accent-light-rgb), 0.15);
- border-color: rgba(var(--accent-light-rgb), 0.4);
- color: rgb(var(--accent-light-rgb));
-}
-
-.import-page-pool-empty {
- font-size: 12px;
- color: rgba(255, 255, 255, 0.2);
- font-style: italic;
-}
-
-/* Match footer */
-.import-page-match-footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding-top: 16px;
- border-top: 1px solid rgba(255, 255, 255, 0.06);
-}
-
-.import-page-match-stats {
- font-size: 13px;
- color: rgba(255, 255, 255, 0.5);
-}
-
-/* Buttons */
-.import-page-process-btn {
- padding: 10px 24px;
- background: rgb(var(--accent-light-rgb));
- border: none;
- border-radius: 10px;
- color: #000;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.import-page-process-btn:hover {
- background: #1fdf64;
-}
-
-.import-page-process-btn:disabled {
- background: rgba(255, 255, 255, 0.1);
- color: rgba(255, 255, 255, 0.3);
- cursor: not-allowed;
-}
-
-.import-page-secondary-btn {
- padding: 8px 16px;
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.12);
- border-radius: 8px;
- color: #ccc;
- font-size: 13px;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.import-page-secondary-btn:hover {
- background: rgba(255, 255, 255, 0.14);
- color: #fff;
-}
-
-.import-page-back-btn {
- padding: 8px 16px;
- background: none;
- border: 1px solid rgba(255, 255, 255, 0.1);
- border-radius: 8px;
- color: rgba(255, 255, 255, 0.5);
- font-size: 13px;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.import-page-back-btn:hover {
- color: #fff;
- border-color: rgba(255, 255, 255, 0.2);
-}
-
-/* Singles */
-.import-page-singles-header {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- margin-bottom: 16px;
-}
-
-.import-page-singles-actions {
- display: flex;
- gap: 8px;
- align-items: center;
-}
-
-.import-page-singles-list {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.import-page-single-item {
- display: grid;
- grid-template-columns: 32px 1fr auto;
- align-items: center;
- gap: 12px;
- padding: 10px 14px;
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid rgba(255, 255, 255, 0.06);
- border-radius: 10px;
- transition: all 0.2s;
-}
-
-.import-page-single-item.matched {
- border-color: rgba(var(--accent-light-rgb), 0.2);
-}
-
-.import-page-single-checkbox {
- width: 18px;
- height: 18px;
- border: 2px solid rgba(255, 255, 255, 0.2);
- border-radius: 4px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- flex-shrink: 0;
-}
-
-.import-page-single-checkbox.checked {
- background: rgb(var(--accent-light-rgb));
- border-color: rgb(var(--accent-light-rgb));
-}
-
-.import-page-single-checkbox.checked::after {
- content: '✓';
- color: #000;
- font-size: 12px;
- font-weight: 700;
-}
-
-.import-page-single-info {
- min-width: 0;
-}
-
-.import-page-single-filename {
- font-size: 13px;
- color: #fff;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.import-page-single-meta {
- font-size: 11px;
- color: rgba(255, 255, 255, 0.35);
- margin-top: 2px;
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
-}
-
-.import-page-single-matched-info {
- font-size: 12px;
- color: rgb(var(--accent-light-rgb));
- margin-top: 4px;
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-.import-page-single-matched-change {
- color: rgba(255, 255, 255, 0.4);
- cursor: pointer;
- font-size: 11px;
- text-decoration: underline;
-}
-
-.import-page-single-matched-change:hover {
- color: #fff;
-}
-
-.import-page-single-actions {
- display: flex;
- gap: 6px;
- align-items: center;
- flex-shrink: 0;
-}
-
-.import-page-identify-btn {
- padding: 6px 12px;
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.12);
- border-radius: 6px;
- color: #ccc;
- font-size: 12px;
- cursor: pointer;
- transition: all 0.2s;
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.import-page-identify-btn:hover {
- background: rgba(255, 255, 255, 0.14);
- color: #fff;
-}
-
-/* Inline search panel for singles */
-.import-page-single-search-panel {
- grid-column: 1 / -1;
- padding: 12px 0 4px 44px;
-}
-
-.import-page-single-search-bar {
- display: flex;
- gap: 8px;
- margin-bottom: 10px;
-}
-
-.import-page-single-search-input {
- flex: 1;
- padding: 8px 12px;
- background: rgba(255, 255, 255, 0.06);
- border: 1px solid rgba(255, 255, 255, 0.12);
- border-radius: 8px;
- color: #fff;
- font-size: 13px;
- outline: none;
-}
-
-.import-page-single-search-input:focus {
- border-color: rgba(var(--accent-light-rgb), 0.4);
-}
-
-.import-page-single-search-go {
- padding: 8px 14px;
- background: rgb(var(--accent-light-rgb));
- border: none;
- border-radius: 8px;
- color: #000;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
-}
-
-.import-page-single-search-results {
- display: flex;
- flex-direction: column;
- gap: 4px;
- max-height: 200px;
- overflow-y: auto;
-}
-
-.import-page-single-result-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 10px;
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid rgba(255, 255, 255, 0.06);
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.15s;
-}
-
-.import-page-single-result-item:hover {
- background: rgba(var(--accent-light-rgb), 0.08);
- border-color: rgba(var(--accent-light-rgb), 0.25);
-}
-
-.import-page-single-result-img {
- width: 36px;
- height: 36px;
- border-radius: 4px;
- object-fit: cover;
- flex-shrink: 0;
-}
-
-.import-page-single-result-info {
- flex: 1;
- min-width: 0;
-}
-
-.import-page-single-result-name {
- font-size: 13px;
- color: #fff;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.import-page-single-result-detail {
- font-size: 11px;
- color: rgba(255, 255, 255, 0.4);
-}
-
-.import-page-single-result-select {
- padding: 4px 12px;
- background: rgba(var(--accent-light-rgb), 0.15);
- border: 1px solid rgba(var(--accent-light-rgb), 0.3);
- border-radius: 6px;
- color: rgb(var(--accent-light-rgb));
- font-size: 11px;
- font-weight: 600;
- cursor: pointer;
- flex-shrink: 0;
-}
-
-.import-page-single-result-select:hover {
- background: rgba(var(--accent-light-rgb), 0.25);
-}
-
-/* Empty state */
-.import-page-empty-state {
- text-align: center;
- padding: 60px 20px;
- color: rgba(255, 255, 255, 0.3);
- font-size: 14px;
-}
-
-/* Suggestions */
-.import-page-suggestions {
- margin-bottom: 24px;
-}
-
-/* Processing Queue */
-.import-page-queue {
- margin-bottom: 20px;
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 12px;
- background: rgba(255, 255, 255, 0.02);
- overflow: hidden;
-}
-
-.import-page-queue-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 16px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.06);
-}
-
-.import-page-queue-title {
- font-size: 13px;
- font-weight: 600;
- color: rgba(255, 255, 255, 0.6);
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.import-page-queue-clear {
- background: none;
- border: none;
- color: rgba(255, 255, 255, 0.3);
- font-size: 12px;
- cursor: pointer;
- padding: 2px 8px;
-}
-
-.import-page-queue-clear:hover {
- color: #fff;
-}
-
-.import-page-queue-list {
- display: flex;
- flex-direction: column;
-}
-
-.import-page-queue-item {
- display: grid;
- grid-template-columns: 44px 1fr auto;
- gap: 12px;
- align-items: center;
- padding: 10px 16px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.04);
-}
-
-.import-page-queue-item:last-child {
- border-bottom: none;
-}
-
-.import-page-queue-art {
- width: 44px;
- height: 44px;
- border-radius: 6px;
- object-fit: cover;
-}
-
-.import-page-queue-info {
- min-width: 0;
-}
-
-.import-page-queue-name {
- font-size: 13px;
- font-weight: 600;
- color: #fff;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.import-page-queue-detail {
- font-size: 11px;
- color: rgba(255, 255, 255, 0.4);
- margin-top: 2px;
-}
-
-.import-page-queue-progress {
- width: 120px;
- flex-shrink: 0;
-}
-
-.import-page-queue-bar {
- height: 4px;
- background: rgba(255, 255, 255, 0.08);
- border-radius: 2px;
- overflow: hidden;
- margin-bottom: 4px;
-}
-
-.import-page-queue-fill {
- height: 100%;
- background: rgb(var(--accent-light-rgb));
- border-radius: 2px;
- width: 0%;
- transition: width 0.3s ease;
-}
-
-.import-page-queue-fill.error {
- background: #ef4444;
-}
-
-.import-page-queue-status {
- font-size: 10px;
- color: rgba(255, 255, 255, 0.4);
- text-align: right;
-}
-
-.import-page-queue-status.done {
- color: rgb(var(--accent-light-rgb));
-}
-
-.import-page-queue-status.error {
- color: #ef4444;
-}
-
-/* ========================================
- END IMPORT PAGE
- ======================================== */
-
/* ========================================
FAILED DOWNLOAD ERROR TOOLTIP
======================================== */
@@ -45706,10 +44755,6 @@ textarea.enhanced-meta-field-input {
.discog-download-wrap { margin: 12px 0 4px; }
-.artist-hero-actions .discog-download-wrap { margin: 0; }
-
-.artist-hero-actions .discog-btn-compact { margin-top: 0; }
-
.discog-download-btn {
position: relative;
display: flex;
@@ -45757,112 +44802,6 @@ textarea.enhanced-meta-field-input {
animation: discogShimmer 3s ease-in-out infinite;
}
-.artist-hero-actions {
- gap: 10px;
- align-items: center;
-}
-
-.artist-hero-actions .library-artist-radio-btn,
-.artist-hero-actions .library-artist-watchlist-btn,
-.artist-hero-actions .discog-download-btn,
-.artist-hero-actions .library-artist-enhance-btn {
- --artist-action-rgb: var(--accent-rgb);
- min-height: 38px;
- width: auto;
- max-width: none;
- padding: 8px 14px 8px 12px;
- gap: 8px;
- border-radius: 999px;
- color: rgba(255, 255, 255, 0.82);
- background:
- linear-gradient(135deg, rgba(var(--artist-action-rgb), 0.13), rgba(var(--artist-action-rgb), 0.03)),
- rgba(255, 255, 255, 0.035);
- border: 1px solid rgba(var(--artist-action-rgb), 0.26);
- box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.08),
- 0 8px 22px rgba(0, 0, 0, 0.22);
- font-size: 12px;
- font-weight: 650;
- letter-spacing: 0;
- line-height: 1;
- backdrop-filter: blur(14px) saturate(1.25);
- transform: none;
-}
-
-.artist-hero-actions .library-artist-radio-btn { --artist-action-rgb: 79, 195, 247; }
-.artist-hero-actions .library-artist-watchlist-btn { --artist-action-rgb: 255, 193, 7; }
-.artist-hero-actions .discog-download-btn { --artist-action-rgb: var(--accent-rgb); }
-.artist-hero-actions .library-artist-enhance-btn { --artist-action-rgb: 180, 132, 255; }
-
-.artist-hero-actions .library-artist-radio-btn::before,
-.artist-hero-actions .library-artist-watchlist-btn::before,
-.artist-hero-actions .library-artist-enhance-btn::before,
-.artist-hero-actions .discog-download-btn::before {
- content: '';
- width: 7px;
- height: 7px;
- border-radius: 999px;
- position: relative;
- inset: auto;
- flex: 0 0 auto;
- opacity: 1;
- background: rgb(var(--artist-action-rgb));
- box-shadow: 0 0 14px rgba(var(--artist-action-rgb), 0.85);
- transition: transform 0.2s ease, box-shadow 0.2s ease;
-}
-
-.artist-hero-actions .library-artist-radio-btn:hover,
-.artist-hero-actions .library-artist-watchlist-btn:hover:not(:disabled),
-.artist-hero-actions .discog-download-btn:hover,
-.artist-hero-actions .library-artist-enhance-btn:hover {
- color: #ffffff;
- border-color: rgba(var(--artist-action-rgb), 0.5);
- background:
- linear-gradient(135deg, rgba(var(--artist-action-rgb), 0.22), rgba(var(--artist-action-rgb), 0.06)),
- rgba(255, 255, 255, 0.055);
- box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.12),
- 0 12px 28px rgba(0, 0, 0, 0.28),
- 0 0 0 1px rgba(var(--artist-action-rgb), 0.12);
- transform: translateY(-1px);
-}
-
-.artist-hero-actions .library-artist-radio-btn:hover::before,
-.artist-hero-actions .library-artist-watchlist-btn:hover:not(:disabled)::before,
-.artist-hero-actions .discog-download-btn:hover::before,
-.artist-hero-actions .library-artist-enhance-btn:hover::before {
- transform: scale(1.25);
- box-shadow: 0 0 18px rgba(var(--artist-action-rgb), 1);
-}
-
-.artist-hero-actions .library-artist-watchlist-btn.watching {
- color: #ffd761;
- border-color: rgba(255, 193, 7, 0.52);
- background:
- linear-gradient(135deg, rgba(255, 193, 7, 0.24), rgba(255, 193, 7, 0.06)),
- rgba(255, 255, 255, 0.04);
-}
-
-.artist-hero-actions .library-artist-radio-btn .radio-icon,
-.artist-hero-actions .library-artist-watchlist-btn .watchlist-icon,
-.artist-hero-actions .discog-download-btn .discog-btn-icon,
-.artist-hero-actions .library-artist-enhance-btn .enhance-icon {
- font-size: 13px;
- position: relative;
- z-index: 1;
- transform: none;
-}
-
-.artist-hero-actions .library-artist-radio-btn:hover .radio-icon,
-.artist-hero-actions .library-artist-watchlist-btn:hover:not(:disabled) .watchlist-icon,
-.artist-hero-actions .library-artist-enhance-btn:hover .enhance-icon {
- transform: none;
-}
-
-.artist-hero-actions .discog-btn-shimmer {
- display: none;
-}
-
@keyframes discogShimmer {
0%, 100% { left: -100%; }
50% { left: 150%; }
@@ -47281,56 +46220,26 @@ textarea.enhanced-meta-field-input {
/* Radio mode button */
.np-radio-btn {
- --np-radio-rgb: 79, 195, 247;
- position: relative;
- overflow: hidden;
- background:
- linear-gradient(135deg, rgba(var(--np-radio-rgb), 0.1), rgba(var(--np-radio-rgb), 0.025)),
- rgba(255, 255, 255, 0.035);
- border: 1px solid rgba(var(--np-radio-rgb), 0.22);
- color: rgba(255, 255, 255, 0.62);
+ background: none;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.35);
font-size: 11px;
- font-weight: 650;
- line-height: 1;
- padding: 7px 10px;
- border-radius: 999px;
+ padding: 3px 8px;
+ border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
- gap: 6px;
- min-height: 30px;
- transition: all 0.18s ease;
+ gap: 4px;
+ transition: all 0.15s ease;
}
.np-radio-btn:hover {
- color: rgba(255, 255, 255, 0.9);
- border-color: rgba(var(--np-radio-rgb), 0.42);
- background:
- linear-gradient(135deg, rgba(var(--np-radio-rgb), 0.16), rgba(var(--np-radio-rgb), 0.04)),
- rgba(255, 255, 255, 0.055);
+ color: rgba(255, 255, 255, 0.7);
+ border-color: rgba(255, 255, 255, 0.2);
}
.np-radio-btn.active {
- color: #ffffff;
- border-color: rgba(var(--np-radio-rgb), 0.55);
- background:
- linear-gradient(135deg, rgba(var(--np-radio-rgb), 0.28), rgba(var(--np-radio-rgb), 0.07)),
- rgba(255, 255, 255, 0.06);
- box-shadow: 0 0 0 1px rgba(var(--np-radio-rgb), 0.08), 0 0 18px rgba(var(--np-radio-rgb), 0.12);
-}
-.np-radio-label {
- position: relative;
- z-index: 1;
-}
-.np-radio-pulse {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.24);
- box-shadow: none;
- transition: all 0.18s ease;
-}
-.np-radio-btn.active .np-radio-pulse {
- background: rgb(var(--np-radio-rgb));
- box-shadow: 0 0 12px rgba(var(--np-radio-rgb), 0.95);
+ color: rgb(var(--accent-rgb));
+ border-color: rgba(var(--accent-rgb), 0.3);
+ background: rgba(var(--accent-rgb), 0.08);
}
.np-queue-header-actions {
display: flex;
@@ -50453,18 +49362,15 @@ tr.tag-diff-same {
transition: transform 0.2s ease;
}
-.repair-master-toggle input,
-.auto-import-toggle-label input {
+.repair-master-toggle input {
display: none;
}
-.repair-master-toggle input:checked + .repair-toggle-slider,
-.auto-import-toggle-label input:checked + .repair-toggle-slider {
+.repair-master-toggle input:checked + .repair-toggle-slider {
background: var(--accent-color, #6366f1);
}
-.repair-master-toggle input:checked + .repair-toggle-slider::after,
-.auto-import-toggle-label input:checked + .repair-toggle-slider::after {
+.repair-master-toggle input:checked + .repair-toggle-slider::after {
transform: translateX(20px);
}
@@ -59601,388 +58507,6 @@ body.reduce-effects *::after {
.wl-orb-group.expanded { max-width: 100%; }
}
-/* ═══════════════════════════════════════════════════════════════════
- AUTO-IMPORT TAB
- ═══════════════════════════════════════════════════════════════════ */
-
-.auto-import-controls {
- padding: 12px 0 16px;
- border-bottom: 1px solid rgba(255,255,255,0.05);
- margin-bottom: 16px;
-}
-
-.auto-import-toggle-row {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.auto-import-toggle-label {
- display: flex;
- align-items: center;
- gap: 8px;
- cursor: pointer;
- font-size: 13px;
- font-weight: 600;
- color: rgba(255,255,255,0.7);
-}
-
-.auto-import-toggle-label input { display: none; }
-
-.auto-import-status {
- font-size: 12px;
- font-weight: 500;
- padding: 2px 10px;
- border-radius: 6px;
-}
-.auto-import-status.active { color: #4ade80; background: rgba(74,222,128,0.1); }
-.auto-import-status.scanning { color: rgb(var(--accent-light-rgb)); background: rgba(var(--accent-rgb),0.1); }
-.auto-import-status.disabled { color: rgba(255,255,255,0.3); }
-
-.auto-import-settings-row {
- display: flex;
- align-items: center;
- gap: 16px;
- margin-top: 10px;
- flex-wrap: wrap;
- font-size: 12px;
- color: rgba(255,255,255,0.5);
-}
-
-.auto-import-settings-row label { display: flex; align-items: center; gap: 6px; }
-.auto-import-settings-row input[type="range"] { width: 100px; }
-.auto-import-settings-row select {
- background: rgba(255,255,255,0.05);
- border: 1px solid rgba(255,255,255,0.1);
- color: #fff;
- border-radius: 6px;
- padding: 3px 8px;
- font-size: 12px;
-}
-
-.auto-import-empty {
- text-align: center;
- padding: 40px 20px;
- color: rgba(255,255,255,0.3);
- font-size: 13px;
-}
-
-/* Result cards */
-.auto-import-card {
- display: flex;
- flex-direction: column;
- padding: 14px 16px;
- background: rgba(255,255,255,0.02);
- border: 1px solid rgba(255,255,255,0.06);
- border-radius: 12px;
- margin-bottom: 8px;
- transition: all 0.2s;
-}
-
-.auto-import-card-top {
- display: flex;
- gap: 14px;
- align-items: center;
-}
-
-.auto-import-card:hover {
- background: rgba(255,255,255,0.04);
- border-color: rgba(255,255,255,0.1);
-}
-
-.auto-import-completed { border-left: 3px solid #4ade80; }
-.auto-import-review { border-left: 3px solid #fbbf24; }
-.auto-import-failed { border-left: 3px solid #f87171; }
-.auto-import-processing { border-left: 3px solid #60a5fa; }
-
-.auto-import-card-art {
- width: 56px; height: 56px;
- border-radius: 8px;
- object-fit: cover;
- flex-shrink: 0;
-}
-
-.auto-import-card-art-fallback {
- width: 56px; height: 56px;
- border-radius: 8px;
- background: rgba(255,255,255,0.05);
- display: flex; align-items: center; justify-content: center;
- font-size: 22px; opacity: 0.3; flex-shrink: 0;
-}
-
-.auto-import-card-center { flex: 1; min-width: 0; }
-
-.auto-import-card-album {
- font-size: 14px; font-weight: 600; color: #fff;
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
-}
-
-.auto-import-card-artist {
- font-size: 12px; color: rgba(255,255,255,0.45);
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
-}
-
-.auto-import-card-folder {
- font-size: 10px; color: rgba(255,255,255,0.25); margin-top: 2px;
-}
-
-.auto-import-match-info {
- font-size: 10px; color: rgba(255,255,255,0.35); margin-top: 2px;
-}
-
-.auto-import-card-error {
- font-size: 10px; color: #f87171; margin-top: 2px;
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
-}
-
-.auto-import-card-right {
- display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
- flex-shrink: 0; min-width: 80px;
-}
-
-.auto-import-confidence-bar {
- width: 60px; height: 4px;
- background: rgba(255,255,255,0.06);
- border-radius: 2px; overflow: hidden;
-}
-
-.auto-import-confidence-fill { height: 100%; border-radius: 2px; }
-.auto-import-conf-high { background: #4ade80; }
-.auto-import-conf-medium { background: #fbbf24; }
-.auto-import-conf-low { background: #f87171; }
-
-.auto-import-confidence-text {
- font-size: 10px; font-weight: 600; color: rgba(255,255,255,0.5);
-}
-
-/* Scan Now button */
-.auto-import-scan-now-btn {
- background: rgba(255,255,255,0.06);
- border: 1px solid rgba(255,255,255,0.1);
- color: rgba(255,255,255,0.6);
- font-size: 11px;
- padding: 4px 12px;
- border-radius: 6px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 5px;
- transition: all 0.15s;
- margin-left: auto;
-}
-
-.auto-import-scan-now-btn:hover {
- background: rgba(255,255,255,0.1);
- color: rgba(255,255,255,0.9);
-}
-
-/* Live progress */
-.auto-import-progress {
- margin-top: 8px;
- padding: 8px 12px;
- background: rgba(var(--accent-rgb), 0.04);
- border: 1px solid rgba(var(--accent-rgb), 0.1);
- border-radius: 8px;
-}
-
-.auto-import-progress-text {
- font-size: 11px;
- color: rgba(255,255,255,0.6);
- margin-bottom: 4px;
-}
-
-.auto-import-progress-bar {
- height: 3px;
- background: rgba(255,255,255,0.06);
- border-radius: 2px;
- overflow: hidden;
-}
-
-.auto-import-progress-fill {
- height: 100%;
- background: rgba(var(--accent-rgb), 0.6);
- border-radius: 2px;
- width: 100%;
- animation: adlPulse 1.5s ease-in-out infinite;
-}
-
-/* Stats summary */
-.auto-import-stats {
- display: flex;
- gap: 16px;
- padding: 8px 0;
- margin-bottom: 4px;
-}
-
-.auto-import-stat {
- font-size: 12px;
- color: rgba(255,255,255,0.5);
- font-weight: 500;
-}
-
-.auto-import-stat-review { color: #fbbf24; }
-.auto-import-stat-failed { color: #f87171; }
-
-/* Filter pills */
-.auto-import-filters {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 0;
- margin-bottom: 8px;
-}
-
-/* Batch action buttons */
-.auto-import-batch-btn {
- font-size: 11px;
- padding: 4px 12px;
- border-radius: 6px;
- border: 1px solid rgba(var(--accent-rgb), 0.3);
- background: rgba(var(--accent-rgb), 0.08);
- color: rgba(var(--accent-rgb), 1);
- cursor: pointer;
- transition: all 0.15s;
-}
-
-.auto-import-batch-btn:hover {
- background: rgba(var(--accent-rgb), 0.15);
-}
-
-.auto-import-clear-btn {
- border-color: rgba(255,255,255,0.1);
- background: rgba(255,255,255,0.04);
- color: rgba(255,255,255,0.5);
-}
-
-.auto-import-clear-btn:hover {
- background: rgba(255,255,255,0.08);
- color: rgba(255,255,255,0.8);
-}
-
-.auto-import-card-meta {
- display: flex; gap: 8px; align-items: center;
- font-size: 10px; color: rgba(255,255,255,0.3); margin-top: 3px;
-}
-
-.auto-import-method-badge {
- background: rgba(255,255,255,0.06);
- padding: 1px 6px;
- border-radius: 4px;
- font-size: 9px;
- color: rgba(255,255,255,0.45);
- text-transform: uppercase;
- letter-spacing: 0.3px;
-}
-
-.auto-import-card-folder-path {
- font-size: 9px;
- color: rgba(255,255,255,0.15);
- margin-top: 6px;
- padding-top: 6px;
- border-top: 1px solid rgba(255,255,255,0.03);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-/* Expandable track list */
-.auto-import-track-list {
- display: none;
- margin-top: 8px;
- padding-top: 8px;
- border-top: 1px solid rgba(255,255,255,0.04);
-}
-
-.auto-import-track-list.expanded {
- display: block;
-}
-
-.auto-import-track-list-header {
- display: grid;
- grid-template-columns: 1fr 1fr 50px;
- gap: 8px;
- font-size: 9px;
- font-weight: 600;
- color: rgba(255,255,255,0.3);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- padding-bottom: 4px;
- margin-bottom: 4px;
- border-bottom: 1px solid rgba(255,255,255,0.03);
-}
-
-.auto-import-track-row {
- display: grid;
- grid-template-columns: 1fr 1fr 50px;
- gap: 8px;
- padding: 3px 0;
- font-size: 11px;
-}
-
-.auto-import-track-name {
- color: rgba(255,255,255,0.7);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.auto-import-track-file {
- color: rgba(255,255,255,0.3);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.auto-import-track-conf {
- text-align: right;
- font-weight: 600;
- font-size: 10px;
-}
-
-.auto-import-track-conf.auto-import-conf-high { color: #4ade80; }
-.auto-import-track-conf.auto-import-conf-medium { color: #fbbf24; }
-.auto-import-track-conf.auto-import-conf-low { color: #f87171; }
-
-.auto-import-status-badge {
- font-size: 9px; font-weight: 600; padding: 2px 8px;
- border-radius: 6px; white-space: nowrap;
-}
-.auto-import-badge-completed { background: rgba(74,222,128,0.1); color: #4ade80; }
-.auto-import-badge-review { background: rgba(251,191,36,0.1); color: #fbbf24; }
-.auto-import-badge-failed { background: rgba(248,113,113,0.1); color: #f87171; }
-.auto-import-badge-neutral { background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.4); }
-.auto-import-badge-processing {
- background: rgba(96,165,250,0.12);
- color: #60a5fa;
- animation: auto-import-badge-pulse 1.6s ease-in-out infinite;
-}
-@keyframes auto-import-badge-pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.55; }
-}
-
-.auto-import-track-row-active {
- background: rgba(96,165,250,0.08);
- border-left: 2px solid #60a5fa;
- padding-left: 6px;
- margin-left: -8px;
- border-radius: 3px;
-}
-.auto-import-track-row-active .auto-import-track-name { color: #60a5fa; font-weight: 600; }
-.auto-import-track-row-done .auto-import-track-name,
-.auto-import-track-row-done .auto-import-track-file { opacity: 0.4; }
-
-.auto-import-actions {
- display: flex; gap: 4px; margin-top: 4px;
-}
-
-.auto-import-actions button { font-size: 10px; padding: 3px 10px; }
-
-@media (max-width: 768px) {
- .auto-import-card { flex-direction: column; align-items: flex-start; }
- .auto-import-card-right { flex-direction: row; width: 100%; justify-content: space-between; }
-}
-
/* ── Legacy (hidden) ── */
#wishlist-page-categories { display: none;
margin-bottom: 24px;