// MUSICBRAINZ ENRICHMENT UI - PHASE 5 WEB UI
// ============================================================================
/**
* Poll MusicBrainz status every 2 seconds and update UI
*/
async function updateMusicBrainzStatus() {
if (socketConnected) return; // WebSocket handles this
if (document.hidden) return; // Skip polling when tab is not visible
try {
const response = await fetch('/api/enrichment/musicbrainz/status');
if (!response.ok) { console.warn('MusicBrainz status endpoint unavailable'); return; }
const data = await response.json();
updateMusicBrainzStatusFromData(data);
} catch (error) {
console.error('Error updating MusicBrainz status:', error);
}
}
function updateMusicBrainzStatusFromData(data) {
const button = document.getElementById('musicbrainz-button');
if (!button) return;
// Update button state classes
button.classList.remove('active', 'paused', 'complete');
if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
} else if (data.paused) {
button.classList.add('paused');
}
// Update tooltip content
const tooltipStatus = document.getElementById('mb-tooltip-status');
const tooltipCurrent = document.getElementById('mb-tooltip-current');
const tooltipProgress = document.getElementById('mb-tooltip-progress');
if (tooltipStatus) {
if (data.idle) {
tooltipStatus.textContent = 'Complete';
} else if (data.running && !data.paused) {
tooltipStatus.textContent = 'Running';
} else if (data.paused) {
tooltipStatus.textContent = data.yield_reason === 'downloads' ? 'Yielding for downloads' : 'Paused';
} else {
tooltipStatus.textContent = 'Idle';
}
}
if (tooltipCurrent) {
if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
const type = data.current_item.type || 'item';
const name = data.current_item.name;
tooltipCurrent.textContent = `${type.charAt(0).toUpperCase() + type.slice(1)}: "${name}"`;
} else {
tooltipCurrent.textContent = 'No active matches';
}
}
if (tooltipProgress && data.progress) {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total} (${artists.percent || 0}%)`;
} else if (currentType === 'album' || (artistsComplete && !albumsComplete)) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total} (${albums.percent || 0}%)`;
} else if (currentType === 'track' || (artistsComplete && albumsComplete)) {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total} (${tracks.percent || 0}%)`;
} else {
progressText = `Artists: ${artists.matched || 0} / ${artists.total} (${artists.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
/**
* Toggle MusicBrainz enrichment pause/resume
*/
async function toggleMusicBrainzEnrichment() {
try {
const button = document.getElementById('musicbrainz-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/musicbrainz/pause' : '/api/enrichment/musicbrainz/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} MusicBrainz enrichment`);
}
// Immediately update UI
await updateMusicBrainzStatus();
console.log(`✅ MusicBrainz enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling MusicBrainz enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
// Initialize MusicBrainz UI on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('musicbrainz-button');
if (button) {
button.addEventListener('click', toggleMusicBrainzEnrichment);
// Start polling
updateMusicBrainzStatus();
setInterval(updateMusicBrainzStatus, 2000); // Poll every 2 seconds
console.log('✅ MusicBrainz UI initialized');
}
});
} else {
const button = document.getElementById('musicbrainz-button');
if (button) {
button.addEventListener('click', toggleMusicBrainzEnrichment);
// Start polling
updateMusicBrainzStatus();
setInterval(updateMusicBrainzStatus, 2000); // Poll every 2 seconds
console.log('✅ MusicBrainz UI initialized');
}
}
// ============================================================================
// AUDIODB ENRICHMENT UI
// ============================================================================
/**
* Poll AudioDB status every 2 seconds and update UI
*/
async function updateAudioDBStatus() {
if (socketConnected) return; // WebSocket handles this
if (document.hidden) return; // Skip polling when tab is not visible
try {
const response = await fetch('/api/enrichment/audiodb/status');
if (!response.ok) { console.warn('AudioDB status endpoint unavailable'); return; }
const data = await response.json();
updateAudioDBStatusFromData(data);
} catch (error) {
console.error('Error updating AudioDB status:', error);
}
}
function updateAudioDBStatusFromData(data) {
const button = document.getElementById('audiodb-button');
if (!button) return;
button.classList.remove('active', 'paused', 'complete');
if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
} else if (data.paused) {
button.classList.add('paused');
}
const tooltipStatus = document.getElementById('audiodb-tooltip-status');
const tooltipCurrent = document.getElementById('audiodb-tooltip-current');
const tooltipProgress = document.getElementById('audiodb-tooltip-progress');
if (tooltipStatus) {
if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running && !data.paused) { tooltipStatus.textContent = 'Running'; }
else if (data.paused) { tooltipStatus.textContent = data.yield_reason === 'downloads' ? 'Yielding for downloads' : 'Paused'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
const type = data.current_item.type || 'item';
const name = data.current_item.name;
tooltipCurrent.textContent = `${type.charAt(0).toUpperCase() + type.slice(1)}: "${name}"`;
} else {
tooltipCurrent.textContent = 'No active matches';
}
}
if (tooltipProgress && data.progress) {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType === 'album' || (artistsComplete && !albumsComplete)) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else if (currentType === 'track' || (artistsComplete && albumsComplete)) {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
} else {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
function updateDiscogsStatusFromData(data) {
const button = document.getElementById('discogs-button');
if (!button) return;
button.classList.remove('active', 'paused', 'complete');
if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
} else if (data.paused) {
button.classList.add('paused');
}
const tooltipStatus = document.getElementById('discogs-tooltip-status');
const tooltipCurrent = document.getElementById('discogs-tooltip-current');
const tooltipProgress = document.getElementById('discogs-tooltip-progress');
if (tooltipStatus) {
if (data.idle) tooltipStatus.textContent = 'Complete';
else if (data.running && !data.paused) tooltipStatus.textContent = 'Running';
else if (data.paused) tooltipStatus.textContent = data.yield_reason === 'downloads' ? 'Yielding for downloads' : 'Paused';
else tooltipStatus.textContent = 'Idle';
}
if (tooltipCurrent) {
if (data.idle) tooltipCurrent.textContent = 'All items processed';
else if (data.current_item) tooltipCurrent.textContent = `Processing: "${data.current_item}"`;
else tooltipCurrent.textContent = 'No active matches';
}
if (tooltipProgress && data.stats) {
const s = data.stats;
tooltipProgress.textContent = `Matched: ${s.matched || 0} | Not found: ${s.not_found || 0} | Pending: ${s.pending || 0}`;
}
}
async function toggleDiscogsEnrichment() {
try {
const button = document.getElementById('discogs-button');
if (!button) return;
const isPaused = button.classList.contains('paused') || button.classList.contains('complete');
const endpoint = isPaused ? '/api/enrichment/discogs/resume' : '/api/enrichment/discogs/pause';
const response = await fetch(endpoint, { method: 'POST' });
if (response.ok) {
showToast(isPaused ? 'Discogs enrichment resumed' : 'Discogs enrichment paused', 'info');
}
} catch (e) {
showToast('Failed to toggle Discogs enrichment', 'error');
}
}
/**
* Toggle AudioDB enrichment pause/resume
*/
async function toggleAudioDBEnrichment() {
try {
const button = document.getElementById('audiodb-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/audiodb/pause' : '/api/enrichment/audiodb/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} AudioDB enrichment`);
}
// Immediately update UI
await updateAudioDBStatus();
console.log(`✅ AudioDB enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling AudioDB enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
// Initialize AudioDB UI on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('audiodb-button');
if (button) {
button.addEventListener('click', toggleAudioDBEnrichment);
updateAudioDBStatus();
setInterval(updateAudioDBStatus, 2000);
console.log('✅ AudioDB UI initialized');
}
});
} else {
const button = document.getElementById('audiodb-button');
if (button) {
button.addEventListener('click', toggleAudioDBEnrichment);
updateAudioDBStatus();
setInterval(updateAudioDBStatus, 2000);
console.log('✅ AudioDB UI initialized');
}
}
// ===================================================================
// DEEZER ENRICHMENT STATUS
// ===================================================================
async function updateDeezerStatus() {
if (socketConnected) return; // WebSocket handles this
if (document.hidden) return; // Skip polling when tab is not visible
try {
const response = await fetch('/api/enrichment/deezer/status');
if (!response.ok) { console.warn('Deezer status endpoint unavailable'); return; }
const data = await response.json();
updateDeezerStatusFromData(data);
} catch (error) {
console.error('Error updating Deezer status:', error);
}
}
function updateDeezerStatusFromData(data) {
const button = document.getElementById('deezer-button');
if (!button) return;
button.classList.remove('active', 'paused', 'complete');
if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
} else if (data.paused) {
button.classList.add('paused');
}
const tooltipStatus = document.getElementById('deezer-tooltip-status');
const tooltipCurrent = document.getElementById('deezer-tooltip-current');
const tooltipProgress = document.getElementById('deezer-tooltip-progress');
if (tooltipStatus) {
if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running && !data.paused) { tooltipStatus.textContent = 'Running'; }
else if (data.paused) { tooltipStatus.textContent = data.yield_reason === 'downloads' ? 'Yielding for downloads' : 'Paused'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
}
}
if (data.progress && tooltipProgress) {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType === 'album' || (artistsComplete && !albumsComplete)) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else if (currentType === 'track' || (artistsComplete && albumsComplete)) {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
} else {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
async function toggleDeezerEnrichment() {
try {
const button = document.getElementById('deezer-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/deezer/pause' : '/api/enrichment/deezer/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} Deezer enrichment`);
}
// Immediately update UI
await updateDeezerStatus();
console.log(`✅ Deezer enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling Deezer enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
// Initialize Deezer UI on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('deezer-button');
if (button) {
button.addEventListener('click', toggleDeezerEnrichment);
updateDeezerStatus();
setInterval(updateDeezerStatus, 2000);
console.log('✅ Deezer UI initialized');
}
});
} else {
const button = document.getElementById('deezer-button');
if (button) {
button.addEventListener('click', toggleDeezerEnrichment);
updateDeezerStatus();
setInterval(updateDeezerStatus, 2000);
console.log('✅ Deezer UI initialized');
}
}
// ===================================================================
// SPOTIFY ENRICHMENT STATUS
// ===================================================================
async function updateSpotifyEnrichmentStatus() {
if (socketConnected) return; // WebSocket handles this
if (document.hidden) return; // Skip polling when tab is not visible
try {
const response = await fetch('/api/enrichment/spotify/status');
if (!response.ok) { console.warn('Spotify enrichment status endpoint unavailable'); return; }
const data = await response.json();
updateSpotifyEnrichmentStatusFromData(data);
} catch (error) {
console.error('Error updating Spotify enrichment status:', error);
}
}
function updateSpotifyEnrichmentStatusFromData(data) {
const button = document.getElementById('spotify-enrich-button');
if (!button) return;
const notAuthenticated = data.authenticated === false;
const isRateLimited = data.rate_limited === true;
const budgetExhausted = data.daily_budget && data.daily_budget.exhausted;
button.classList.remove('active', 'paused', 'complete', 'no-auth');
if (data.paused) {
button.classList.add('paused');
} else if (notAuthenticated) {
button.classList.add('no-auth');
} else if (isRateLimited || budgetExhausted) {
button.classList.add('paused');
} else if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
}
const tooltipStatus = document.getElementById('spotify-enrich-tooltip-status');
const tooltipCurrent = document.getElementById('spotify-enrich-tooltip-current');
const tooltipProgress = document.getElementById('spotify-enrich-tooltip-progress');
if (tooltipStatus) {
if (data.paused) { tooltipStatus.textContent = 'Paused'; }
else if (notAuthenticated) { tooltipStatus.textContent = 'Not Authenticated'; }
else if (isRateLimited) { tooltipStatus.textContent = 'Rate Limited'; }
else if (budgetExhausted) { tooltipStatus.textContent = 'Daily Limit Reached'; }
else if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running) { tooltipStatus.textContent = 'Running'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.paused) {
tooltipCurrent.textContent = notAuthenticated ? 'Connect Spotify in Settings to enrich' : 'Click to resume';
} else if (notAuthenticated) {
tooltipCurrent.textContent = 'Connect Spotify in Settings to enrich';
} else if (isRateLimited) {
const info = data.rate_limit || {};
const remaining = info.remaining_seconds || 0;
tooltipCurrent.textContent = remaining > 0 ? `Waiting ${Math.ceil(remaining / 60)}m for rate limit to clear` : 'Waiting for rate limit to clear';
} else if (budgetExhausted) {
const resets = data.daily_budget.resets_in_seconds || 0;
const hours = Math.floor(resets / 3600);
const mins = Math.floor((resets % 3600) / 60);
tooltipCurrent.textContent = `Resets in ${hours}h ${mins}m`;
} else if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
} else {
tooltipCurrent.textContent = 'Waiting for next item...';
}
}
if (data.progress && tooltipProgress) {
if (notAuthenticated) {
tooltipProgress.textContent = `Pending: ${data.stats?.pending || 0} items`;
} else {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type || '';
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist') {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType.includes('album')) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else if (currentType.includes('track')) {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
} else if (!artistsComplete) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (!albumsComplete) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
}
async function toggleSpotifyEnrichment() {
try {
const button = document.getElementById('spotify-enrich-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/spotify/pause' : '/api/enrichment/spotify/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
const data = await response.json().catch(() => ({}));
if (data.rate_limited) {
showToast('Cannot resume — Spotify is rate limited', 'warning');
return;
}
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} Spotify enrichment`);
}
await updateSpotifyEnrichmentStatus();
console.log(`Spotify enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling Spotify enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
// Initialize Spotify Enrichment UI on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('spotify-enrich-button');
if (button) {
button.addEventListener('click', toggleSpotifyEnrichment);
updateSpotifyEnrichmentStatus();
setInterval(updateSpotifyEnrichmentStatus, 2000);
}
});
} else {
const button = document.getElementById('spotify-enrich-button');
if (button) {
button.addEventListener('click', toggleSpotifyEnrichment);
updateSpotifyEnrichmentStatus();
setInterval(updateSpotifyEnrichmentStatus, 2000);
}
}
// ===================================================================
// ITUNES ENRICHMENT STATUS
// ===================================================================
async function updateiTunesEnrichmentStatus() {
if (socketConnected) return; // WebSocket handles this
if (document.hidden) return; // Skip polling when tab is not visible
try {
const response = await fetch('/api/enrichment/itunes/status');
if (!response.ok) { console.warn('iTunes enrichment status endpoint unavailable'); return; }
const data = await response.json();
updateiTunesEnrichmentStatusFromData(data);
} catch (error) {
console.error('Error updating iTunes enrichment status:', error);
}
}
function updateiTunesEnrichmentStatusFromData(data) {
const button = document.getElementById('itunes-enrich-button');
if (!button) return;
button.classList.remove('active', 'paused', 'complete');
if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
} else if (data.paused) {
button.classList.add('paused');
}
const tooltipStatus = document.getElementById('itunes-enrich-tooltip-status');
const tooltipCurrent = document.getElementById('itunes-enrich-tooltip-current');
const tooltipProgress = document.getElementById('itunes-enrich-tooltip-progress');
if (tooltipStatus) {
if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running && !data.paused) { tooltipStatus.textContent = 'Running'; }
else if (data.paused) { tooltipStatus.textContent = data.yield_reason === 'downloads' ? 'Yielding for downloads' : 'Paused'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
}
}
if (data.progress && tooltipProgress) {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type || '';
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist') {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType.includes('album')) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else if (currentType.includes('track')) {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
} else if (!artistsComplete) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (!albumsComplete) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
async function toggleiTunesEnrichment() {
try {
const button = document.getElementById('itunes-enrich-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/itunes/pause' : '/api/enrichment/itunes/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} iTunes enrichment`);
}
await updateiTunesEnrichmentStatus();
console.log(`iTunes enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling iTunes enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
// Initialize iTunes Enrichment UI on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('itunes-enrich-button');
if (button) {
button.addEventListener('click', toggleiTunesEnrichment);
updateiTunesEnrichmentStatus();
setInterval(updateiTunesEnrichmentStatus, 2000);
}
});
} else {
const button = document.getElementById('itunes-enrich-button');
if (button) {
button.addEventListener('click', toggleiTunesEnrichment);
updateiTunesEnrichmentStatus();
setInterval(updateiTunesEnrichmentStatus, 2000);
}
}
// ===================================================================
// LAST.FM ENRICHMENT STATUS
// ===================================================================
async function updateLastFMEnrichmentStatus() {
if (socketConnected) return;
if (document.hidden) return;
try {
const response = await fetch('/api/enrichment/lastfm/status');
if (!response.ok) { console.warn('Last.fm status endpoint unavailable'); return; }
const data = await response.json();
updateLastFMEnrichmentStatusFromData(data);
} catch (error) {
console.error('Error updating Last.fm status:', error);
}
}
function updateLastFMEnrichmentStatusFromData(data) {
const button = document.getElementById('lastfm-enrich-button');
if (!button) return;
const notAuthenticated = data.authenticated === false;
button.classList.remove('active', 'paused', 'complete', 'no-auth');
if (data.paused) {
button.classList.add('paused');
} else if (notAuthenticated) {
button.classList.add('no-auth');
} else if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
}
const tooltipStatus = document.getElementById('lastfm-enrich-tooltip-status');
const tooltipCurrent = document.getElementById('lastfm-enrich-tooltip-current');
const tooltipProgress = document.getElementById('lastfm-enrich-tooltip-progress');
if (tooltipStatus) {
if (data.paused) { tooltipStatus.textContent = 'Paused'; }
else if (notAuthenticated) { tooltipStatus.textContent = 'Not Authenticated'; }
else if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running) { tooltipStatus.textContent = 'Running'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.paused) {
tooltipCurrent.textContent = notAuthenticated ? 'Add Last.fm API key in Settings to enrich' : 'Click to resume';
} else if (notAuthenticated) {
tooltipCurrent.textContent = 'Add Last.fm API key in Settings to enrich';
} else if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
}
}
if (data.progress && tooltipProgress) {
if (notAuthenticated) {
tooltipProgress.textContent = `Pending: ${data.stats?.pending || 0} items`;
} else {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType === 'album' || (artistsComplete && !albumsComplete)) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else if (currentType === 'track' || (artistsComplete && albumsComplete)) {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
} else {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
}
async function toggleLastFMEnrichment() {
try {
const button = document.getElementById('lastfm-enrich-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/lastfm/pause' : '/api/enrichment/lastfm/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} Last.fm enrichment`);
}
await updateLastFMEnrichmentStatus();
console.log(`Last.fm enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling Last.fm enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('lastfm-enrich-button');
if (button) {
button.addEventListener('click', toggleLastFMEnrichment);
updateLastFMEnrichmentStatus();
setInterval(updateLastFMEnrichmentStatus, 2000);
}
});
} else {
const button = document.getElementById('lastfm-enrich-button');
if (button) {
button.addEventListener('click', toggleLastFMEnrichment);
updateLastFMEnrichmentStatus();
setInterval(updateLastFMEnrichmentStatus, 2000);
}
}
// ===================================================================
// GENIUS ENRICHMENT STATUS
// ===================================================================
async function updateGeniusEnrichmentStatus() {
if (socketConnected) return;
if (document.hidden) return;
try {
const response = await fetch('/api/enrichment/genius/status');
if (!response.ok) { console.warn('Genius status endpoint unavailable'); return; }
const data = await response.json();
updateGeniusEnrichmentStatusFromData(data);
} catch (error) {
console.error('Error updating Genius status:', error);
}
}
function updateGeniusEnrichmentStatusFromData(data) {
const button = document.getElementById('genius-enrich-button');
if (!button) return;
const notAuthenticated = data.authenticated === false;
button.classList.remove('active', 'paused', 'complete', 'no-auth');
if (data.paused) {
button.classList.add('paused');
} else if (notAuthenticated) {
button.classList.add('no-auth');
} else if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
}
const tooltipStatus = document.getElementById('genius-enrich-tooltip-status');
const tooltipCurrent = document.getElementById('genius-enrich-tooltip-current');
const tooltipProgress = document.getElementById('genius-enrich-tooltip-progress');
if (tooltipStatus) {
if (data.paused) { tooltipStatus.textContent = 'Paused'; }
else if (notAuthenticated) { tooltipStatus.textContent = 'Not Authenticated'; }
else if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running) { tooltipStatus.textContent = 'Running'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.paused) {
tooltipCurrent.textContent = notAuthenticated ? 'Add Genius access token in Settings to enrich' : 'Click to resume';
} else if (notAuthenticated) {
tooltipCurrent.textContent = 'Add Genius access token in Settings to enrich';
} else if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
}
}
if (data.progress && tooltipProgress) {
if (notAuthenticated) {
tooltipProgress.textContent = `Pending: ${data.stats?.pending || 0} items`;
} else {
const artists = data.progress.artists || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
}
async function toggleGeniusEnrichment() {
try {
const button = document.getElementById('genius-enrich-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/genius/pause' : '/api/enrichment/genius/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} Genius enrichment`);
}
await updateGeniusEnrichmentStatus();
console.log(`Genius enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling Genius enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('genius-enrich-button');
if (button) {
button.addEventListener('click', toggleGeniusEnrichment);
updateGeniusEnrichmentStatus();
setInterval(updateGeniusEnrichmentStatus, 2000);
}
});
} else {
const button = document.getElementById('genius-enrich-button');
if (button) {
button.addEventListener('click', toggleGeniusEnrichment);
updateGeniusEnrichmentStatus();
setInterval(updateGeniusEnrichmentStatus, 2000);
}
}
// ===================================================================
// TIDAL ENRICHMENT WORKER
// ===================================================================
async function updateTidalEnrichmentStatus() {
if (socketConnected) return;
if (document.hidden) return;
try {
const response = await fetch('/api/enrichment/tidal/status');
if (!response.ok) { console.warn('Tidal status endpoint unavailable'); return; }
const data = await response.json();
updateTidalEnrichmentStatusFromData(data);
} catch (error) {
console.error('Error updating Tidal status:', error);
}
}
function updateTidalEnrichmentStatusFromData(data) {
const button = document.getElementById('tidal-enrich-button');
if (!button) return;
const notAuthenticated = data.authenticated === false;
button.classList.remove('active', 'paused', 'complete', 'no-auth');
if (data.paused) {
button.classList.add('paused');
} else if (notAuthenticated) {
button.classList.add('no-auth');
} else if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
}
const tooltipStatus = document.getElementById('tidal-enrich-tooltip-status');
const tooltipCurrent = document.getElementById('tidal-enrich-tooltip-current');
const tooltipProgress = document.getElementById('tidal-enrich-tooltip-progress');
if (tooltipStatus) {
if (data.paused) { tooltipStatus.textContent = 'Paused'; }
else if (notAuthenticated) { tooltipStatus.textContent = 'Not Authenticated'; }
else if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running) { tooltipStatus.textContent = 'Running'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.paused) {
tooltipCurrent.textContent = notAuthenticated ? 'Connect Tidal in Settings to enrich' : 'Click to resume';
} else if (notAuthenticated) {
tooltipCurrent.textContent = 'Connect Tidal in Settings to enrich';
} else if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
}
}
if (data.progress && tooltipProgress) {
if (notAuthenticated) {
tooltipProgress.textContent = `Pending: ${data.stats?.pending || 0} items`;
} else {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType === 'album' || (!albumsComplete && !currentType)) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
}
async function toggleTidalEnrichment() {
try {
const button = document.getElementById('tidal-enrich-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/tidal/pause' : '/api/enrichment/tidal/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} Tidal enrichment`);
}
await updateTidalEnrichmentStatus();
console.log(`Tidal enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling Tidal enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('tidal-enrich-button');
if (button) {
button.addEventListener('click', toggleTidalEnrichment);
updateTidalEnrichmentStatus();
setInterval(updateTidalEnrichmentStatus, 2000);
}
});
} else {
const button = document.getElementById('tidal-enrich-button');
if (button) {
button.addEventListener('click', toggleTidalEnrichment);
updateTidalEnrichmentStatus();
setInterval(updateTidalEnrichmentStatus, 2000);
}
}
// ===================================================================
// QOBUZ ENRICHMENT WORKER
// ===================================================================
async function updateQobuzEnrichmentStatus() {
if (socketConnected) return;
if (document.hidden) return;
try {
const response = await fetch('/api/enrichment/qobuz/status');
if (!response.ok) { console.warn('Qobuz status endpoint unavailable'); return; }
const data = await response.json();
updateQobuzEnrichmentStatusFromData(data);
} catch (error) {
console.error('Error updating Qobuz status:', error);
}
}
function updateQobuzEnrichmentStatusFromData(data) {
const button = document.getElementById('qobuz-enrich-button');
if (!button) return;
const notAuthenticated = data.authenticated === false;
button.classList.remove('active', 'paused', 'complete', 'no-auth');
if (data.paused) {
button.classList.add('paused');
} else if (notAuthenticated) {
button.classList.add('no-auth');
} else if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
}
const tooltipStatus = document.getElementById('qobuz-enrich-tooltip-status');
const tooltipCurrent = document.getElementById('qobuz-enrich-tooltip-current');
const tooltipProgress = document.getElementById('qobuz-enrich-tooltip-progress');
if (tooltipStatus) {
if (data.paused) { tooltipStatus.textContent = 'Paused'; }
else if (notAuthenticated) { tooltipStatus.textContent = 'Not Authenticated'; }
else if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running) { tooltipStatus.textContent = 'Running'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.paused) {
tooltipCurrent.textContent = notAuthenticated ? 'Connect Qobuz in Settings to enrich' : 'Click to resume';
} else if (notAuthenticated) {
tooltipCurrent.textContent = 'Connect Qobuz in Settings to enrich';
} else if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
}
}
if (data.progress && tooltipProgress) {
if (notAuthenticated) {
tooltipProgress.textContent = `Pending: ${data.stats?.pending || 0} items`;
} else {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType === 'album' || (!albumsComplete && !currentType)) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
}
async function toggleQobuzEnrichment() {
try {
const button = document.getElementById('qobuz-enrich-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/qobuz/pause' : '/api/enrichment/qobuz/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} Qobuz enrichment`);
}
await updateQobuzEnrichmentStatus();
console.log(`Qobuz enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling Qobuz enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('qobuz-enrich-button');
if (button) {
button.addEventListener('click', toggleQobuzEnrichment);
updateQobuzEnrichmentStatus();
setInterval(updateQobuzEnrichmentStatus, 2000);
}
});
} else {
const button = document.getElementById('qobuz-enrich-button');
if (button) {
button.addEventListener('click', toggleQobuzEnrichment);
updateQobuzEnrichmentStatus();
setInterval(updateQobuzEnrichmentStatus, 2000);
}
}
// ===================================================================
// AMAZON MUSIC ENRICHMENT WORKER
// ===================================================================
async function updateAmazonEnrichmentStatus() {
if (socketConnected) return;
if (document.hidden) return;
try {
const response = await fetch('/api/enrichment/amazon/status');
if (!response.ok) { console.warn('Amazon enrichment status endpoint unavailable'); return; }
const data = await response.json();
updateAmazonEnrichmentStatusFromData(data);
} catch (error) {
console.error('Error updating Amazon enrichment status:', error);
}
}
function updateAmazonEnrichmentStatusFromData(data) {
const button = document.getElementById('amazon-enrich-button');
if (!button) return;
button.classList.remove('active', 'paused', 'complete');
if (data.paused) {
button.classList.add('paused');
} else if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
}
const tooltipStatus = document.getElementById('amazon-enrich-tooltip-status');
const tooltipCurrent = document.getElementById('amazon-enrich-tooltip-current');
const tooltipProgress = document.getElementById('amazon-enrich-tooltip-progress');
if (tooltipStatus) {
if (data.paused) { tooltipStatus.textContent = data.yield_reason === 'downloads' ? 'Yielding for downloads' : 'Paused'; }
else if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running) { tooltipStatus.textContent = 'Running'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.idle) {
tooltipCurrent.textContent = 'All items processed';
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Now: ${data.current_item.name}`;
} else {
tooltipCurrent.textContent = 'No active matches';
}
}
if (data.progress && tooltipProgress) {
const artists = data.progress.artists || {};
const albums = data.progress.albums || {};
const tracks = data.progress.tracks || {};
const currentType = data.current_item?.type;
let progressText = '';
const artistsComplete = artists.matched >= artists.total;
const albumsComplete = albums.matched >= albums.total;
if (currentType === 'artist' || (!artistsComplete && !currentType)) {
progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`;
} else if (currentType === 'album' || (!albumsComplete && !currentType)) {
progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`;
} else {
progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`;
}
tooltipProgress.textContent = progressText;
}
}
async function toggleAmazonEnrichment() {
try {
const button = document.getElementById('amazon-enrich-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/enrichment/amazon/pause' : '/api/enrichment/amazon/resume';
const response = await fetch(endpoint, { method: 'POST' });
if (!response.ok) {
throw new Error(`Failed to ${isRunning ? 'pause' : 'resume'} Amazon enrichment`);
}
await updateAmazonEnrichmentStatus();
console.log(`Amazon enrichment ${isRunning ? 'paused' : 'resumed'}`);
} catch (error) {
console.error('Error toggling Amazon enrichment:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('amazon-enrich-button');
if (button) {
button.addEventListener('click', toggleAmazonEnrichment);
updateAmazonEnrichmentStatus();
setInterval(updateAmazonEnrichmentStatus, 2000);
}
});
} else {
const button = document.getElementById('amazon-enrich-button');
if (button) {
button.addEventListener('click', toggleAmazonEnrichment);
updateAmazonEnrichmentStatus();
setInterval(updateAmazonEnrichmentStatus, 2000);
}
}
// ===================================================================
// HYDRABASE P2P MIRROR WORKER
// ===================================================================
async function updateHydrabaseStatus() {
if (socketConnected) return; // WebSocket handles this
if (document.hidden) return; // Skip polling when tab is not visible
try {
const response = await fetch('/api/hydrabase-worker/status');
if (!response.ok) return;
const data = await response.json();
updateHydrabaseStatusFromData(data);
} catch (error) {
// Silently ignore — worker may not be available
}
}
function updateHydrabaseStatusFromData(data) {
const button = document.getElementById('hydrabase-button');
if (!button) return;
button.classList.remove('active', 'paused');
if (data.running && !data.paused) {
button.classList.add('active');
} else if (data.paused) {
button.classList.add('paused');
}
const statusEl = document.getElementById('hydrabase-tooltip-status');
if (statusEl) {
if (data.paused) {
statusEl.textContent = 'Paused';
statusEl.style.color = '#ffc107';
} else if (data.running) {
statusEl.textContent = 'Active';
statusEl.style.color = '#ffffff';
} else {
statusEl.textContent = 'Stopped';
statusEl.style.color = '#ff5252';
}
}
}
async function toggleHydrabaseWorker() {
const button = document.getElementById('hydrabase-button');
if (!button) return;
const isRunning = button.classList.contains('active');
const endpoint = isRunning ? '/api/hydrabase-worker/pause' : '/api/hydrabase-worker/resume';
try {
await fetch(endpoint, { method: 'POST' });
await updateHydrabaseStatus();
} catch (error) {
console.error('Error toggling Hydrabase worker:', error);
}
}
// Initialize Hydrabase UI on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('hydrabase-button');
if (button) {
button.addEventListener('click', toggleHydrabaseWorker);
updateHydrabaseStatus();
setInterval(updateHydrabaseStatus, 2000);
}
});
} else {
const button = document.getElementById('hydrabase-button');
if (button) {
button.addEventListener('click', toggleHydrabaseWorker);
updateHydrabaseStatus();
setInterval(updateHydrabaseStatus, 2000);
}
}
// ===================================================================
// LIBRARY REPAIR WORKER
// ===================================================================
async function updateRepairStatus() {
if (socketConnected) return; // WebSocket handles this
if (document.hidden) return; // Skip polling when tab is not visible
try {
const response = await fetch('/api/repair/status');
if (!response.ok) { console.warn('Repair status endpoint unavailable'); return; }
const data = await response.json();
updateRepairStatusFromData(data);
} catch (error) {
console.error('Error updating repair status:', error);
}
}
function updateRepairStatusFromData(data) {
const button = document.getElementById('repair-button');
if (!button) return;
button.classList.remove('active', 'paused', 'complete');
if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
} else if (data.paused) {
button.classList.add('paused');
}
const tooltipStatus = document.getElementById('repair-tooltip-status');
const tooltipCurrent = document.getElementById('repair-tooltip-current');
const tooltipProgress = document.getElementById('repair-tooltip-progress');
if (tooltipStatus) {
if (data.idle) { tooltipStatus.textContent = 'Complete'; }
else if (data.running && !data.paused) { tooltipStatus.textContent = 'Running'; }
else if (data.paused) { tooltipStatus.textContent = data.yield_reason === 'downloads' ? 'Yielding for downloads' : 'Paused'; }
else { tooltipStatus.textContent = 'Idle'; }
}
if (tooltipCurrent) {
if (data.idle) {
tooltipCurrent.textContent = 'All jobs complete — waiting for next schedule';
} else if (data.current_job && data.current_job.display_name) {
const jobName = data.current_job.display_name;
const jobProgress = data.progress && data.progress.current_job;
if (jobProgress && jobProgress.total > 0) {
tooltipCurrent.textContent = `${jobName}: ${jobProgress.scanned} / ${jobProgress.total} (${jobProgress.percent}%)`;
} else {
tooltipCurrent.textContent = `Running: ${jobName}`;
}
} else if (data.current_item && data.current_item.name) {
tooltipCurrent.textContent = `Running: ${data.current_item.name}`;
} else {
tooltipCurrent.textContent = 'No active repairs';
}
}
if (tooltipProgress && data.progress) {
const tracks = data.progress.tracks || {};
const parts = [];
if (tracks.total > 0) parts.push(`Checked: ${tracks.checked || 0} / ${tracks.total || 0}`);
if (tracks.repaired > 0) parts.push(`Repaired: ${tracks.repaired}`);
const pending = data.findings_pending || 0;
if (pending > 0) parts.push(`Findings: ${pending}`);
tooltipProgress.textContent = parts.length ? parts.join(' · ') : 'No items processed yet';
}
// Update findings badge
const badge = document.getElementById('repair-findings-badge');
const findingsPending = data.findings_pending || 0;
if (badge) {
badge.textContent = findingsPending;
badge.style.display = findingsPending > 0 ? '' : 'none';
}
const tabBadge = document.getElementById('repair-findings-tab-badge');
if (tabBadge) {
tabBadge.textContent = findingsPending;
tabBadge.style.display = findingsPending > 0 ? '' : 'none';
}
// Update master toggle in modal if open
const masterToggle = document.getElementById('repair-master-toggle');
const masterLabel = document.getElementById('repair-master-label');
if (masterToggle) masterToggle.checked = data.enabled || false;
if (masterLabel) masterLabel.textContent = data.enabled ? 'Enabled' : 'Disabled';
// Update button state
if (!data.enabled) {
button.classList.add('paused');
button.classList.remove('active', 'complete');
}
}
// ── SoulID Worker Status ──
function updateSoulIDStatusFromData(data) {
const button = document.getElementById('soulid-button');
if (!button) return;
button.classList.remove('active', 'complete');
if (data.idle) {
button.classList.add('complete');
} else if (data.running && !data.paused) {
button.classList.add('active');
}
const tooltipStatus = document.getElementById('soulid-tooltip-status');
const tooltipCurrent = document.getElementById('soulid-tooltip-current');
const tooltipProgress = document.getElementById('soulid-tooltip-progress');
if (tooltipStatus) {
if (data.idle) tooltipStatus.textContent = 'Complete';
else if (data.running && !data.paused) tooltipStatus.textContent = 'Running';
else if (data.paused) tooltipStatus.textContent = 'Paused';
else tooltipStatus.textContent = 'Idle';
}
if (tooltipCurrent) {
if (data.current_item) {
tooltipCurrent.textContent = data.current_item;
} else if (data.idle) {
tooltipCurrent.textContent = 'All entities have soul IDs';
} else {
tooltipCurrent.textContent = 'No items processing';
}
}
if (tooltipProgress && data.stats) {
const s = data.stats;
const parts = [];
if (s.artists_processed) parts.push(`Artists: ${s.artists_processed}`);
if (s.albums_processed) parts.push(`Albums: ${s.albums_processed}`);
if (s.tracks_processed) parts.push(`Tracks: ${s.tracks_processed}`);
if (s.pending > 0) parts.push(`Pending: ${s.pending}`);
tooltipProgress.textContent = parts.length ? parts.join(' · ') : 'No items processed yet';
}
}
// ── Repair Modal State ──
let _repairCurrentTab = 'jobs';
let _repairFindingsPage = 0;
let _repairSelectedFindings = new Set();
let _repairFindingsTotal = 0;
let _repairFindingsAutoSwitched = false; // Set after auto-switching to "All Status" so we don't loop
const REPAIR_FINDINGS_PAGE_SIZE = 30;
let _repairJobsCache = {}; // Cache job data for help modal
/**
* Open the Library Maintenance modal
*/
async function openRepairModal() {
navigateToPage('tools');
// Scroll to maintenance section
setTimeout(() => {
const section = document.querySelector('.tools-maintenance-section');
if (section) section.scrollIntoView({ behavior: 'smooth' });
}, 100);
_repairCurrentTab = 'jobs';
switchRepairTab('jobs');
// Load master toggle state
updateRepairStatus();
// Load any active job progress
try {
const resp = await fetch('/api/repair/progress');
if (resp.ok) {
const data = await resp.json();
if (Object.keys(data).length > 0) {
// Brief delay so job cards are rendered first
setTimeout(() => updateRepairJobProgressFromData(data), 300);
}
}
} catch (e) { /* ignore */ }
}
function closeRepairModal() {
// No-op — repair content now lives on the tools page, no modal to close
}
async function toggleRepairMaster() {
try {
const response = await fetch('/api/repair/toggle', { method: 'POST' });
if (!response.ok) throw new Error('Failed to toggle');
const data = await response.json();
const label = document.getElementById('repair-master-label');
const toggle = document.getElementById('repair-master-toggle');
if (label) label.textContent = data.enabled ? 'Enabled' : 'Disabled';
if (toggle) toggle.checked = data.enabled;
await updateRepairStatus();
} catch (error) {
console.error('Error toggling repair master:', error);
showToast('Error toggling maintenance worker', 'error');
}
}
function switchRepairTab(tab) {
_repairCurrentTab = tab;
document.querySelectorAll('.repair-tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
document.querySelectorAll('.repair-tab-content').forEach(c => {
c.style.display = 'none';
});
const content = document.getElementById(`repair-tab-${tab}`);
if (content) content.style.display = '';
if (tab === 'jobs') loadRepairJobs();
else if (tab === 'findings') { loadRepairFindingsDashboard(); loadRepairFindings(); }
else if (tab === 'history') loadRepairHistory();
}
// Turn a snake_case setting key into a human label. Handles acronym fix-ups
// (EP, ID, URL, MB, AC, OS) that the naive Title-Case would otherwise botch.
function _prettifyRepairSettingKey(key) {
const words = key.replace(/^_+/, '').split('_');
const acronyms = { 'eps': 'EPs', 'id': 'ID', 'url': 'URL', 'mb': 'MB',
'ac': 'AC', 'os': 'OS', 'api': 'API', 'mp3': 'MP3',
'flac': 'FLAC', 'cd': 'CD' };
return words.map(w => acronyms[w.toLowerCase()] || (w.charAt(0).toUpperCase() + w.slice(1))).join(' ');
}
async function loadRepairJobs() {
const container = document.getElementById('repair-jobs-list');
if (!container) return;
try {
const response = await fetch('/api/repair/jobs');
if (!response.ok) throw new Error('Failed to fetch jobs');
const data = await response.json();
const jobs = data.jobs || [];
// Cache job data for help modal
_repairJobsCache = {};
jobs.forEach(j => { _repairJobsCache[j.job_id] = j; });
if (jobs.length === 0) {
container.innerHTML = `
🔧
No Maintenance Jobs
Library maintenance jobs will appear here once available.
`;
return;
}
// Populate findings job filter dropdown
const jobFilter = document.getElementById('repair-findings-job-filter');
if (jobFilter && jobFilter.options.length <= 1) {
jobs.forEach(job => {
const opt = document.createElement('option');
opt.value = job.job_id;
opt.textContent = job.display_name;
jobFilter.appendChild(opt);
});
}
container.innerHTML = jobs.map(job => {
const lastRunText = job.last_run ? formatCacheAge(job.last_run.finished_at) : 'Never';
const nextRunText = job.next_run ? formatCacheAge(job.next_run) : (job.enabled ? 'Pending' : '-');
const statusClass = job.is_running ? 'running' : (job.enabled ? 'idle' : 'disabled');
const dotClass = job.is_running ? 'running' : (job.enabled ? 'enabled' : 'disabled');
const cardClass = job.is_running ? 'running' : (!job.enabled ? 'disabled' : '');
// Build flow badges
const flowParts = [];
flowParts.push(`${job.is_running ? '▶ Running' : 'Scan'}`);
if (job.auto_fix) {
flowParts.push('→');
const isDryRun = job.settings && job.settings.dry_run === true;
if (isDryRun) {
flowParts.push('Dry Run');
} else {
flowParts.push('Auto-fix');
}
}
// Badge: prefer the CURRENT pending count from the API
// (matches what the Findings tab actually shows); fall
// back to the historical ``findings_created`` from the
// last run when pending = 0 but the last scan did find
// something — that way users still see a hint that a scan
// happened recently. Without this, a scan that finds 372
// duplicates and then has them all bulk-fixed shows
// "372 findings" on the badge while the Findings tab
// Pending filter is empty, which reads as a bug.
const pendingCount = job.pending_findings_count || 0;
const lastScanCount = job.last_run ? (job.last_run.findings_created || 0) : 0;
if (pendingCount > 0) {
flowParts.push('→');
flowParts.push(`${pendingCount} pending`);
} else if (lastScanCount > 0) {
// Show historical count with a clear "found in last scan"
// qualifier so users don't expect them on the Pending tab.
flowParts.push('→');
flowParts.push(`${lastScanCount} found in last scan`);
}
// Build meta parts
const metaParts = [];
metaParts.push('Last: ' + lastRunText);
metaParts.push('Next: ' + nextRunText);
if (job.last_run) {
metaParts.push(`Scanned: ${(job.last_run.items_scanned || 0).toLocaleString()}`);
if (job.last_run.auto_fixed) metaParts.push(`Fixed: ${job.last_run.auto_fixed}`);
}
if (job.last_run && job.last_run.duration_seconds) {
metaParts.push(`${job.last_run.duration_seconds.toFixed(1)}s`);
}
// Build settings HTML
let settingsHtml = '';
if (job.settings && Object.keys(job.settings).length > 0) {
const settingsRows = Object.entries(job.settings).map(([key, val]) => {
// Section header: keys starting with `_section_` render as a
// group divider + title instead of a setting row. The value
// is the human-readable title.
if (key.startsWith('_section_')) {
return `
`;
document.body.appendChild(overlay);
}
async function saveRepairJobSettings(jobId) {
try {
const inputs = document.querySelectorAll(`.repair-setting-input[data-job="${jobId}"]`);
let intervalHours = null;
const settings = {};
inputs.forEach(input => {
const key = input.dataset.key;
if (key === '_interval_hours') {
intervalHours = parseInt(input.value) || 24;
} else {
if (input.type === 'checkbox') settings[key] = input.checked;
else if (input.type === 'number') settings[key] = parseFloat(input.value);
else settings[key] = input.value;
}
});
await fetch(`/api/repair/jobs/${jobId}/settings`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interval_hours: intervalHours, settings })
});
showToast('Settings saved', 'success');
} catch (error) {
console.error('Error saving job settings:', error);
showToast('Error saving settings', 'error');
}
}
async function runRepairJobNow(jobId) {
try {
await fetch(`/api/repair/jobs/${jobId}/run`, { method: 'POST' });
showToast('Job started', 'success');
setTimeout(() => loadRepairJobs(), 1000);
} catch (error) {
console.error('Error running job:', error);
showToast('Error starting job', 'error');
}
}
// ── Repair Job Live Progress ──
const _repairProgressLogCounts = {};
const _repairProgressHideTimers = {};
function updateRepairJobProgressFromData(data) {
for (const [jobId, state] of Object.entries(data)) {
const card = document.querySelector(`.repair-job-card[data-job-id="${jobId}"]`);
if (!card) continue;
// Update status dot
const statusDot = card.querySelector('.repair-job-status');
if (statusDot) {
if (state.status === 'running') statusDot.className = 'repair-job-status running';
else if (state.status === 'finished') statusDot.className = 'repair-job-status enabled';
else if (state.status === 'error') statusDot.className = 'repair-job-status enabled';
}
// Update flow badge to show running state
const firstBadge = card.querySelector('.repair-flow-badge.scan');
if (firstBadge) {
if (state.status === 'running') firstBadge.innerHTML = '▶ Running';
else if (state.status === 'finished') firstBadge.innerHTML = '✓ Complete';
else if (state.status === 'error') firstBadge.innerHTML = '✗ Error';
}
// Add/update card running class
card.classList.toggle('running', state.status === 'running');
card.classList.remove('disabled');
// Create or find progress panel (bar-first layout like automation)
let panel = card.querySelector('.repair-job-progress');
if (!panel) {
panel = document.createElement('div');
panel.className = 'repair-job-progress';
panel.innerHTML = `
`;
card.appendChild(panel);
}
// Show panel
panel.classList.add('visible');
panel.classList.toggle('finished', state.status === 'finished');
panel.classList.toggle('error', state.status === 'error');
if (state.status === 'running') {
panel.classList.remove('finished', 'error');
if (_repairProgressHideTimers[jobId]) {
clearTimeout(_repairProgressHideTimers[jobId]);
delete _repairProgressHideTimers[jobId];
}
// Reset log for re-run
if (_repairProgressLogCounts[jobId] > 0 && state.log && state.log.length < _repairProgressLogCounts[jobId]) {
const existingLog = panel.querySelector('.repair-progress-log');
if (existingLog) existingLog.innerHTML = '';
_repairProgressLogCounts[jobId] = 0;
}
}
// Update progress bar
const bar = panel.querySelector('.repair-progress-bar');
if (bar) bar.style.width = (state.progress || 0) + '%';
// Update phase
const phaseEl = panel.querySelector('.repair-progress-phase');
if (phaseEl && state.phase) phaseEl.textContent = state.phase;
// Update log
const logEl = panel.querySelector('.repair-progress-log');
if (logEl && state.log) {
const prevCount = _repairProgressLogCounts[jobId] || 0;
if (state.log.length > prevCount) {
const newLines = state.log.slice(prevCount);
for (const line of newLines) {
const div = document.createElement('div');
div.className = 'repair-log-line ' + (line.type || 'info');
div.textContent = line.text;
logEl.appendChild(div);
}
logEl.scrollTop = logEl.scrollHeight;
}
_repairProgressLogCounts[jobId] = state.log.length;
}
// Auto-hide panel after completion
if (state.status === 'finished' || state.status === 'error') {
if (!_repairProgressHideTimers[jobId]) {
_repairProgressHideTimers[jobId] = setTimeout(() => {
panel.classList.remove('visible');
card.classList.remove('running');
delete _repairProgressHideTimers[jobId];
delete _repairProgressLogCounts[jobId];
// Reload to get updated stats
loadRepairJobs();
}, 30000);
}
} else {
// Clear any existing hide timer if job restarts
if (_repairProgressHideTimers[jobId]) {
clearTimeout(_repairProgressHideTimers[jobId]);
delete _repairProgressHideTimers[jobId];
}
}
}
}
async function loadRepairFindingsDashboard() {
const dashboard = document.getElementById('repair-findings-dashboard');
if (!dashboard) return;
try {
const response = await fetch('/api/repair/findings/counts');
if (!response.ok) throw new Error('Failed to fetch counts');
const data = await response.json();
const pending = data.pending || 0;
const resolved = data.resolved || 0;
const dismissed = data.dismissed || 0;
const autoFixed = data.auto_fixed || 0;
const byJob = data.by_job || {};
// Summary stats row
let html = '
';
html += `
${pending.toLocaleString()} pending
`;
html += `
${resolved.toLocaleString()} resolved
`;
html += `
${dismissed.toLocaleString()} dismissed
`;
if (autoFixed > 0) {
html += `
${autoFixed.toLocaleString()} auto-fixed
`;
}
html += '
';
// Per-job chips (only if there are pending findings)
const jobIds = Object.keys(byJob).sort((a, b) => byJob[b].total - byJob[a].total);
if (jobIds.length > 0) {
html += '
';
const jobFilter = document.getElementById('repair-findings-job-filter');
const activeJob = jobFilter ? jobFilter.value : '';
for (const jid of jobIds) {
const job = byJob[jid];
const isActive = activeJob === jid;
const severityDots = [];
if (job.warning > 0) severityDots.push(``);
if (job.info > 0) severityDots.push(``);
html += `
';
if (albumUrl) {
const albumLabel = d.album_title || 'Album';
html += `
${_escFinding(albumLabel)}
`;
}
if (artistUrl) {
const artistLabel = d.artist_name || d.artist || 'Artist';
html += `
${_escFinding(artistLabel)}
`;
}
html += '
';
return html;
}
function _renderFindingDetail(f) {
const d = f.details || {};
const rows = [];
const media = _renderFindingMedia(d);
switch (f.finding_type) {
case 'dead_file':
if (d.artist) rows.push(['Artist', d.artist]);
if (d.album) rows.push(['Album', d.album]);
if (d.title) rows.push(['Title', d.title]);
if (d.track_id) rows.push(['Track ID', d.track_id]);
if (d.original_path) rows.push(['Original Path', d.original_path, 'path']);
return media + _gridRows(rows) + _renderPlayButton(f);
case 'orphan_file':
if (d.folder) rows.push(['Folder', d.folder, 'path']);
if (d.format) rows.push(['Format', d.format.toUpperCase()]);
if (d.file_size) rows.push(['File Size', _formatFileSize(d.file_size)]);
if (d.modified) rows.push(['Last Modified', d.modified]);
if (f.file_path) rows.push(['Full Path', f.file_path, 'path']);
return _gridRows(rows) + _renderPlayButton(f);
case 'acoustid_mismatch': {
let html = media + '
';
html += _renderScoreBar(d.fingerprint_score, 'Fingerprint');
html += _renderScoreBar(d.title_similarity, 'Title Match');
html += _renderScoreBar(d.artist_similarity, 'Artist Match');
html += '
`;
}
return incHtml;
case 'path_mismatch':
if (d.from) rows.push(['Current Path', d.from, 'path']);
if (d.to) rows.push(['Expected Path', d.to, 'success']);
return _gridRows(rows);
case 'metadata_gap':
if (d.artist) rows.push(['Artist', d.artist]);
if (d.album) rows.push(['Album', d.album]);
if (d.title) rows.push(['Title', d.title]);
if (d.spotify_track_id) rows.push(['Spotify ID', d.spotify_track_id]);
if (d.resolved_source) rows.push(['Resolved Source', d.resolved_source]);
if (d.resolved_track_id) rows.push(['Resolved Track ID', d.resolved_track_id]);
if (d.found_fields && typeof d.found_fields === 'object') {
Object.entries(d.found_fields).forEach(([k, v]) => {
rows.push([`Found: ${k}`, String(v), 'success']);
});
}
return media + _gridRows(rows);
case 'missing_cover_art':
if (d.artist) rows.push(['Artist', d.artist]);
if (d.album_title) rows.push(['Album', d.album_title]);
if (d.spotify_album_id) rows.push(['Spotify ID', d.spotify_album_id]);
let artHtml = '';
// Show artist image + found artwork side by side
if (d.artist_thumb_url || d.found_artwork_url) {
artHtml += '
';
if (d.artist_thumb_url) {
artHtml += `
${_escFinding(d.artist || 'Artist')}
`;
}
if (d.found_artwork_url) {
artHtml += `
Found Artwork
`;
}
artHtml += '
';
}
artHtml += _gridRows(rows);
return artHtml;
case 'track_number_mismatch':
if (d.album_title) rows.push(['Album', d.album_title]);
if (d.artist_name) rows.push(['Artist', d.artist_name]);
if (d.matched_title) rows.push(['Matched To', d.matched_title]);
if (d.file_title) rows.push(['File Title', d.file_title]);
if (d.current_track_num !== undefined) rows.push(['Current Track #', String(d.current_track_num)]);
if (d.correct_track_num !== undefined) rows.push(['Correct Track #', String(d.correct_track_num), 'success']);
if (f.file_path) rows.push(['File', f.file_path, 'path']);
let tnHtml = media;
if (d.match_score) {
tnHtml += '