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.
This commit is contained in:
parent
2888a5dda1
commit
a42927f369
2 changed files with 86 additions and 7 deletions
|
|
@ -803,19 +803,98 @@ class RepairWorker:
|
||||||
return handler(entity_type, entity_id, file_path, details)
|
return handler(entity_type, entity_id, file_path, details)
|
||||||
|
|
||||||
def _fix_dead_file(self, 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:
|
if not entity_id:
|
||||||
return {'success': False, 'error': 'No track ID associated with this finding'}
|
return {'success': False, 'error': 'No track ID associated with this finding'}
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = self.db._get_connection()
|
conn = self.db._get_connection()
|
||||||
cursor = conn.cursor()
|
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,))
|
cursor.execute("DELETE FROM tracks WHERE id = ?", (entity_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
if cursor.rowcount > 0:
|
|
||||||
return {'success': True, 'action': 'removed_db_entry',
|
return {'success': True, 'action': 'added_to_wishlist',
|
||||||
'message': 'Removed dead track entry from database'}
|
'message': f'Added "{track_name}" to wishlist for re-download'}
|
||||||
return {'success': False, 'error': 'Track not found in database'}
|
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:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
|
||||||
|
|
@ -52734,7 +52734,7 @@ async function loadRepairFindings() {
|
||||||
|
|
||||||
// Finding types that have an automated fix action
|
// Finding types that have an automated fix action
|
||||||
const fixableTypes = {
|
const fixableTypes = {
|
||||||
dead_file: 'Remove Entry',
|
dead_file: 'Re-download',
|
||||||
orphan_file: 'Delete File',
|
orphan_file: 'Delete File',
|
||||||
track_number_mismatch: 'Fix',
|
track_number_mismatch: 'Fix',
|
||||||
missing_cover_art: 'Apply Art',
|
missing_cover_art: 'Apply Art',
|
||||||
|
|
@ -52747,7 +52747,7 @@ async function loadRepairFindings() {
|
||||||
const icon = severityIcons[f.severity] || 'ℹ️';
|
const icon = severityIcons[f.severity] || 'ℹ️';
|
||||||
const age = formatCacheAge(f.created_at);
|
const age = formatCacheAge(f.created_at);
|
||||||
const actionLabels = {
|
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',
|
already_gone: 'Already Gone', fixed_track_number: 'Track # Fixed',
|
||||||
applied_cover_art: 'Art Applied', applied_metadata: 'Metadata Applied',
|
applied_cover_art: 'Art Applied', applied_metadata: 'Metadata Applied',
|
||||||
removed_duplicates: 'Duplicates Removed',
|
removed_duplicates: 'Duplicates Removed',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue