From a42927f369bad13106a87aaef6a3a6ad5c6e0f81 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:49:38 -0700 Subject: [PATCH] Add re-download capability to dead file findings (#189) Dead file Fix button now adds the track to wishlist for re-download instead of just removing the DB entry. Builds full wishlist-compatible track data from DB (artist, album, artwork, IDs) so the download pipeline can process it like any other wishlist item. --- core/repair_worker.py | 89 +++++++++++++++++++++++++++++++++++++++--- webui/static/script.js | 4 +- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/core/repair_worker.py b/core/repair_worker.py index 3455404b..044bc635 100644 --- a/core/repair_worker.py +++ b/core/repair_worker.py @@ -803,19 +803,98 @@ class RepairWorker: return handler(entity_type, entity_id, file_path, details) def _fix_dead_file(self, entity_type, entity_id, file_path, details): - """Remove the dead track entry from the database.""" + """Add dead file track to wishlist for re-download, then remove the dead DB entry.""" if not entity_id: return {'success': False, 'error': 'No track ID associated with this finding'} conn = None try: conn = self.db._get_connection() cursor = conn.cursor() + + # Fetch full track + album + artist data from DB + cursor.execute(""" + SELECT t.id, t.title, t.track_number, t.duration, t.bitrate, + t.spotify_track_id, t.itunes_track_id, t.deezer_id, t.isrc, + ar.name AS artist_name, ar.spotify_artist_id, + al.title AS album_title, al.spotify_album_id, + al.record_type, al.track_count, al.year, al.thumb_url AS album_thumb + FROM tracks t + LEFT JOIN artists ar ON ar.id = t.artist_id + LEFT JOIN albums al ON al.id = t.album_id + WHERE t.id = ? + """, (entity_id,)) + row = cursor.fetchone() + + if not row: + return {'success': False, 'error': 'Track not found in database'} + + track_name = row['title'] or details.get('title', 'Unknown') + artist_name = row['artist_name'] or details.get('artist', 'Unknown Artist') + album_title = row['album_title'] or details.get('album', '') + + # Best available ID for wishlist (spotify preferred, then itunes, deezer, fallback) + wishlist_id = (row['spotify_track_id'] + or row['itunes_track_id'] + or row['deezer_id'] + or f"redownload_{entity_id}") + + # Build album images list + album_images = [] + album_thumb = row['album_thumb'] or details.get('album_thumb_url') + if album_thumb: + album_images = [{'url': album_thumb}] + + # Build wishlist-compatible track data + spotify_track_data = { + 'id': wishlist_id, + 'name': track_name, + 'artists': [{'name': artist_name}], + 'album': { + 'name': album_title or track_name, + 'id': row['spotify_album_id'] or '', + 'release_date': str(row['year']) if row['year'] else '', + 'images': album_images, + 'album_type': row['record_type'] or 'album', + 'total_tracks': row['track_count'] or 0, + 'artists': [{'name': artist_name}], + }, + 'duration_ms': row['duration'] or 0, + 'track_number': row['track_number'] or 1, + 'disc_number': 1, + 'explicit': False, + 'external_urls': {}, + 'popularity': 0, + 'preview_url': None, + 'uri': f"spotify:track:{row['spotify_track_id']}" if row['spotify_track_id'] else '', + 'is_local': False, + } + + source_info = { + 'original_path': file_path or details.get('original_path', ''), + 'album_title': album_title, + 'artist': artist_name, + 'reason': 'dead_file_redownload', + } + + added = self.db.add_to_wishlist( + spotify_track_data, + failure_reason='Dead file — re-download requested', + source_type='redownload', + source_info=source_info, + ) + + if not added: + return {'success': False, 'error': 'Failed to add to wishlist (may already exist)'} + + # Remove dead track entry from DB cursor.execute("DELETE FROM tracks WHERE id = ?", (entity_id,)) conn.commit() - if cursor.rowcount > 0: - return {'success': True, 'action': 'removed_db_entry', - 'message': 'Removed dead track entry from database'} - return {'success': False, 'error': 'Track not found in database'} + + return {'success': True, 'action': 'added_to_wishlist', + 'message': f'Added "{track_name}" to wishlist for re-download'} + except Exception as e: + logger.error("Dead file re-download failed for track %s: %s", entity_id, e) + return {'success': False, 'error': str(e)} finally: if conn: conn.close() diff --git a/webui/static/script.js b/webui/static/script.js index 44197cbb..2f51a390 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -52734,7 +52734,7 @@ async function loadRepairFindings() { // Finding types that have an automated fix action const fixableTypes = { - dead_file: 'Remove Entry', + dead_file: 'Re-download', orphan_file: 'Delete File', track_number_mismatch: 'Fix', missing_cover_art: 'Apply Art', @@ -52747,7 +52747,7 @@ async function loadRepairFindings() { const icon = severityIcons[f.severity] || 'ℹ️'; const age = formatCacheAge(f.created_at); const actionLabels = { - removed_db_entry: 'Entry Removed', deleted_file: 'File Deleted', + removed_db_entry: 'Entry Removed', added_to_wishlist: 'Wishlisted', deleted_file: 'File Deleted', already_gone: 'Already Gone', fixed_track_number: 'Track # Fixed', applied_cover_art: 'Art Applied', applied_metadata: 'Metadata Applied', removed_duplicates: 'Duplicates Removed',