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:
parent
0594673cad
commit
f08550140f
4 changed files with 132 additions and 10 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue