diff --git a/web_server.py b/web_server.py index 18b3f8ef..59fc7d06 100644 --- a/web_server.py +++ b/web_server.py @@ -12474,6 +12474,77 @@ def library_delete_track(track_id): return jsonify({"success": False, "error": str(e)}), 500 +@app.route('/api/library/artist//sync', methods=['POST']) +def sync_artist_library(artist_id): + """Validate an artist's library entries — remove stale tracks/albums, recount.""" + try: + database = get_database() + with database._get_connection() as conn: + cursor = conn.cursor() + + # Get all tracks for this artist + cursor.execute(""" + SELECT t.id, t.file_path, t.title, t.album_id + FROM tracks t WHERE t.artist_id = ? + """, (artist_id,)) + tracks = cursor.fetchall() + + # Get artist name for logging + cursor.execute("SELECT name FROM artists WHERE id = ?", (artist_id,)) + artist_row = cursor.fetchone() + artist_name = artist_row['name'] if artist_row else f'ID {artist_id}' + + stale_tracks = [] + valid_tracks = 0 + + for track in tracks: + file_path = track['file_path'] + if not file_path: + stale_tracks.append(track['id']) + continue + + # Check if file exists on disk + resolved = _resolve_library_file_path(file_path) + if resolved and os.path.exists(resolved): + valid_tracks += 1 + else: + stale_tracks.append(track['id']) + + # Remove stale tracks + if stale_tracks: + placeholders = ','.join('?' for _ in stale_tracks) + cursor.execute(f"DELETE FROM tracks WHERE id IN ({placeholders})", stale_tracks) + + # Remove empty albums (no tracks left from ANY artist) + cursor.execute(""" + DELETE FROM albums WHERE artist_id = ? + AND id NOT IN (SELECT DISTINCT album_id FROM tracks) + """, (artist_id,)) + empty_albums_removed = cursor.rowcount + + # Update track_count on remaining albums + cursor.execute(""" + UPDATE albums SET track_count = ( + SELECT COUNT(*) FROM tracks WHERE tracks.album_id = albums.id + ) WHERE artist_id = ? + """, (artist_id,)) + + conn.commit() + + print(f"🔄 [Artist Sync] {artist_name}: {valid_tracks} valid, {len(stale_tracks)} stale removed, {empty_albums_removed} empty albums cleaned") + + return jsonify({ + "success": True, + "artist_name": artist_name, + "valid_tracks": valid_tracks, + "stale_removed": len(stale_tracks), + "empty_albums_removed": empty_albums_removed + }) + + except Exception as e: + print(f"❌ Error syncing artist {artist_id}: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/api/library/album/', methods=['DELETE']) def library_delete_album(album_id): """Delete an album and all its tracks from the database (does NOT delete files on disk).""" diff --git a/webui/static/script.js b/webui/static/script.js index 4eb40b17..0d2a1584 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -40669,6 +40669,39 @@ function renderArtistMetaPanel(artist) { headerRight.appendChild(enrichWrap); } + // Sync / Validate button + const syncBtn = document.createElement('button'); + syncBtn.className = 'enhanced-sync-btn'; + syncBtn.innerHTML = '🔄 Sync'; + syncBtn.title = 'Validate files — removes stale entries for tracks no longer on disk'; + syncBtn.onclick = async (e) => { + e.stopPropagation(); + syncBtn.disabled = true; + syncBtn.textContent = 'Syncing...'; + try { + const res = await fetch(`/api/library/artist/${artist.id}/sync`, { method: 'POST' }); + const data = await res.json(); + if (data.success) { + const parts = []; + if (data.stale_removed > 0) parts.push(`${data.stale_removed} stale tracks removed`); + if (data.empty_albums_removed > 0) parts.push(`${data.empty_albums_removed} empty albums cleaned`); + if (parts.length === 0) parts.push('All files verified'); + showToast(`${data.artist_name}: ${parts.join(', ')}`, 'success'); + // Refresh enhanced view if anything changed + if (data.stale_removed > 0 || data.empty_albums_removed > 0) { + loadEnhancedViewData(artist.id); + } + } else { + showToast(`Sync failed: ${data.error}`, 'error'); + } + } catch (err) { + showToast(`Sync failed: ${err.message}`, 'error'); + } + syncBtn.disabled = false; + syncBtn.innerHTML = '🔄 Sync'; + }; + headerRight.appendChild(syncBtn); + header.appendChild(headerRight); panel.appendChild(header); diff --git a/webui/static/style.css b/webui/static/style.css index 907041c0..e9571e1f 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -40589,6 +40589,31 @@ textarea.enhanced-meta-field-input { background: rgba(var(--accent-rgb), 0.15); border-color: rgba(var(--accent-rgb), 0.35); } + +.enhanced-sync-btn { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + padding: 8px 16px; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; +} + +.enhanced-sync-btn:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.2); + color: #fff; +} + +.enhanced-sync-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .enhanced-enrich-btn.small { padding: 5px 12px; font-size: 11px;