diff --git a/web_server.py b/web_server.py index 8c227569..50a237c8 100644 --- a/web_server.py +++ b/web_server.py @@ -15468,12 +15468,50 @@ def sync_artist_library(artist_id): @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).""" + """Delete an album and all its tracks from the database, optionally deleting files on disk.""" try: + delete_files = request.args.get('delete_files', 'false').lower() == 'true' database = get_database() + files_deleted = 0 + files_failed = 0 + with database._get_connection() as conn: cursor = conn.cursor() - # Delete all tracks belonging to this album first + + # If deleting files, resolve and remove each track's file first + if delete_files: + cursor.execute("SELECT id, file_path FROM tracks WHERE album_id = ?", (album_id,)) + track_rows = cursor.fetchall() + for row in track_rows: + fp = row['file_path'] + if not fp: + continue + resolved = _resolve_library_file_path(fp) + if resolved and os.path.exists(resolved): + try: + os.remove(resolved) + files_deleted += 1 + except Exception as e: + logger.warning(f"Failed to delete track file: {e}") + files_failed += 1 + else: + files_failed += 1 + + # Try to remove the album folder if it's now empty + if track_rows: + first_fp = track_rows[0]['file_path'] + if first_fp: + resolved_first = _resolve_library_file_path(first_fp) + if resolved_first: + album_dir = os.path.dirname(resolved_first) + try: + if os.path.isdir(album_dir) and not os.listdir(album_dir): + os.rmdir(album_dir) + logger.info(f"Removed empty album directory: {album_dir}") + except Exception: + pass + + # Delete all tracks belonging to this album cursor.execute("DELETE FROM tracks WHERE album_id = ?", (album_id,)) tracks_deleted = cursor.rowcount # Delete the album itself @@ -15482,7 +15520,13 @@ def library_delete_album(album_id): conn.rollback() return jsonify({"success": False, "error": "Album not found"}), 404 conn.commit() - return jsonify({"success": True, "deleted_count": 1, "tracks_deleted": tracks_deleted}) + return jsonify({ + "success": True, + "deleted_count": 1, + "tracks_deleted": tracks_deleted, + "files_deleted": files_deleted, + "files_failed": files_failed + }) except Exception as e: print(f"Error deleting album {album_id}: {e}") import traceback diff --git a/webui/static/script.js b/webui/static/script.js index f314696a..d79ec59d 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -47755,12 +47755,30 @@ function _pollRedownloadProgress(taskId, overlay) { } async function deleteLibraryAlbum(albumId) { - if (!await showConfirmDialog({ title: 'Delete Album', message: 'Delete this album and all its tracks from the library? (Files on disk are not affected)', confirmText: 'Delete', destructive: true })) return; + const choice = await _showAlbumDeleteDialog(); + if (!choice) return; + + const deleteFiles = choice === 'delete_files'; + const params = deleteFiles ? '?delete_files=true' : ''; + try { - const response = await fetch(`/api/library/album/${albumId}`, { method: 'DELETE' }); + const response = await fetch(`/api/library/album/${albumId}${params}`, { method: 'DELETE' }); const result = await response.json(); if (!result.success) throw new Error(result.error); - showToast(`Album deleted (${result.tracks_deleted || 0} tracks removed)`, 'success'); + + let msg = `Album removed from library (${result.tracks_deleted || 0} tracks)`; + let toastType = 'success'; + if (deleteFiles) { + if (result.files_deleted > 0) { + msg = `Album deleted — ${result.files_deleted} files removed from disk`; + } + if (result.files_failed > 0) { + msg += ` (${result.files_failed} files could not be deleted)`; + toastType = 'warning'; + } + } + showToast(msg, toastType); + if (artistDetailPageState.enhancedData) { const album = (artistDetailPageState.enhancedData.albums || []).find(a => a.id === albumId); if (album && album.tracks) { @@ -47777,6 +47795,53 @@ async function deleteLibraryAlbum(albumId) { } } +function _showAlbumDeleteDialog() { + return new Promise(resolve => { + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;'; + + const close = (val) => { overlay.remove(); resolve(val); }; + overlay.onclick = e => { if (e.target === overlay) close(null); }; + + overlay.innerHTML = ` +
+
+

Delete Album

+ +
+

How should this album be deleted?

+
+ + +
+
+ `; + + overlay.querySelectorAll('.smart-delete-option').forEach(btn => { + btn.addEventListener('click', () => close(btn.dataset.choice)); + }); + overlay.querySelector('.smart-delete-close').addEventListener('click', () => close(null)); + + const escHandler = e => { if (e.key === 'Escape') { document.removeEventListener('keydown', escHandler); close(null); } }; + document.addEventListener('keydown', escHandler); + + document.body.appendChild(overlay); + }); +} + function extractFormat(filePath) { if (!filePath) return '-'; const ext = filePath.split('.').pop().toLowerCase();