Add whistle logo to maintenance modal header & harden fix handlers

- Add whisoul.png to modal header with subtitle text and gradient background
- Responsive: smaller logo on mobile, hide subtitle
- Share _resolve_file_path in repair_worker for cross-environment path compat
- Use path resolution in orphan and duplicate file deletion
- Guard directory cleanup against removing the transfer folder itself
- Restore correct button label text on fix error recovery
This commit is contained in:
Broque Thomas 2026-03-15 13:25:40 -07:00
parent 0594673cad
commit f08550140f
4 changed files with 132 additions and 10 deletions

View file

@ -23,6 +23,31 @@ logger = get_logger("repair_worker")
AUDIO_EXTENSIONS = {'.mp3', '.flac', '.ogg', '.opus', '.m4a', '.aac', '.wav', '.wma', '.aiff', '.aif'}
def _resolve_file_path(file_path, transfer_folder, download_folder=None):
"""Resolve a stored DB path to an actual file on disk.
Tries the raw path first, then progressively shorter suffixes against
configured directories. Handles cross-environment path mismatches
(e.g. Docker paths vs native Windows paths).
"""
if not file_path:
return None
if os.path.exists(file_path):
return file_path
path_parts = file_path.replace('\\', '/').split('/')
for base_dir in [transfer_folder, download_folder]:
if not base_dir or not os.path.isdir(base_dir):
continue
for i in range(1, len(path_parts)):
candidate = os.path.join(base_dir, *path_parts[i:])
if os.path.exists(candidate):
return candidate
return None
class RepairWorker:
"""Multi-job background maintenance worker.
@ -772,12 +797,21 @@ class RepairWorker:
if not file_path:
return {'success': False, 'error': 'No file path associated with this finding'}
try:
if os.path.exists(file_path):
os.remove(file_path)
# Clean up empty parent directories
parent = os.path.dirname(file_path)
# Resolve path in case of cross-environment mismatch
download_folder = None
if self._config_manager:
download_folder = self._config_manager.get('soulseek.download_path', '')
resolved = _resolve_file_path(file_path, self.transfer_folder, download_folder) or file_path
if os.path.exists(resolved):
os.remove(resolved)
# Clean up empty parent directories (never remove transfer folder itself)
transfer_norm = os.path.normpath(self.transfer_folder)
parent = os.path.dirname(resolved)
for _ in range(3): # Up to 3 levels (track/album/artist)
if parent and os.path.isdir(parent) and not os.listdir(parent):
if (parent and os.path.isdir(parent)
and os.path.normpath(parent) != transfer_norm
and not os.listdir(parent)):
os.rmdir(parent)
parent = os.path.dirname(parent)
else:
@ -900,6 +934,13 @@ class RepairWorker:
if not remove_ids:
return {'success': False, 'error': 'No duplicates to remove'}
# Collect file paths before deleting DB entries
remove_paths = []
for t in tracks:
tid = t.get('track_id') or t.get('id')
if tid and str(tid) != str(best_id) and t.get('file_path'):
remove_paths.append(t['file_path'])
conn = None
try:
conn = self.db._get_connection()
@ -908,12 +949,40 @@ class RepairWorker:
cursor.execute(f"DELETE FROM tracks WHERE id IN ({placeholders})", remove_ids)
conn.commit()
removed = cursor.rowcount
return {'success': True, 'action': 'removed_duplicates',
'message': f'Kept best quality copy, removed {removed} duplicate(s)'}
finally:
if conn:
conn.close()
# Delete duplicate files from disk (resolve paths for cross-environment compat)
download_folder = None
if self._config_manager:
download_folder = self._config_manager.get('soulseek.download_path', '')
transfer_norm = os.path.normpath(self.transfer_folder)
files_deleted = 0
for fpath in remove_paths:
try:
resolved = _resolve_file_path(fpath, self.transfer_folder, download_folder)
if resolved and os.path.exists(resolved):
os.remove(resolved)
files_deleted += 1
# Clean up empty parent directories (never remove transfer folder itself)
parent = os.path.dirname(resolved)
for _ in range(3):
if (parent and os.path.isdir(parent)
and os.path.normpath(parent) != transfer_norm
and not os.listdir(parent)):
os.rmdir(parent)
parent = os.path.dirname(parent)
else:
break
except OSError:
pass # Best effort — DB entry already removed
msg = f'Kept best quality copy, removed {removed} duplicate(s)'
if files_deleted:
msg += f' and {files_deleted} file(s) from disk'
return {'success': True, 'action': 'removed_duplicates', 'message': msg}
def dismiss_finding(self, finding_id: int) -> bool:
"""Dismiss a finding."""
conn = None

View file

@ -5612,7 +5612,13 @@
<div class="repair-modal-overlay" id="repair-modal" style="display:none;" onclick="if(event.target===this)closeRepairModal()">
<div class="repair-modal">
<div class="repair-modal-header">
<h2 class="repair-modal-title">Library Maintenance</h2>
<div class="repair-modal-header-left">
<img src="/static/whisoul.png" alt="" class="repair-modal-logo" />
<div class="repair-modal-header-text">
<h2 class="repair-modal-title">Library Maintenance</h2>
<p class="repair-modal-subtitle">Scan, detect, and fix issues in your music library</p>
</div>
</div>
<div class="repair-modal-header-actions">
<label class="repair-master-toggle">
<input type="checkbox" id="repair-master-toggle" onchange="toggleRepairMaster()">

View file

@ -50754,7 +50754,9 @@ function renderRepairFindingsPagination(total, currentPage) {
async function fixRepairFinding(id) {
const card = document.querySelector(`.repair-finding-card[data-id="${id}"]`);
const fixBtn = card ? card.querySelector('.repair-finding-btn.fix') : null;
let originalText = '';
if (fixBtn) {
originalText = fixBtn.textContent;
fixBtn.disabled = true;
fixBtn.textContent = '...';
}
@ -50774,7 +50776,7 @@ async function fixRepairFinding(id) {
showToast('Error applying fix', 'error');
if (fixBtn) {
fixBtn.disabled = false;
fixBtn.textContent = 'Fix';
fixBtn.textContent = originalText;
}
}
}

View file

@ -39733,8 +39733,31 @@ tr.tag-diff-same {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
padding: 18px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.02) 0%, rgba(var(--accent-rgb, 99, 102, 241), 0.04) 100%);
}
.repair-modal-header-left {
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
}
.repair-modal-logo {
width: 56px;
height: 56px;
object-fit: contain;
flex-shrink: 0;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
}
.repair-modal-header-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.repair-modal-title {
@ -39742,12 +39765,21 @@ tr.tag-diff-same {
font-weight: 600;
color: #fff;
margin: 0;
line-height: 1.2;
}
.repair-modal-subtitle {
font-size: 12px;
color: rgba(255, 255, 255, 0.35);
margin: 0;
line-height: 1.3;
}
.repair-modal-header-actions {
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.repair-modal-close {
@ -40885,6 +40917,19 @@ tr.tag-diff-same {
padding: 14px 16px;
}
.repair-modal-logo {
width: 40px;
height: 40px;
}
.repair-modal-title {
font-size: 16px;
}
.repair-modal-subtitle {
display: none;
}
.repair-tab-content {
padding: 12px 14px;
}