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'}
|
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:
|
class RepairWorker:
|
||||||
"""Multi-job background maintenance worker.
|
"""Multi-job background maintenance worker.
|
||||||
|
|
||||||
|
|
@ -772,12 +797,21 @@ class RepairWorker:
|
||||||
if not file_path:
|
if not file_path:
|
||||||
return {'success': False, 'error': 'No file path associated with this finding'}
|
return {'success': False, 'error': 'No file path associated with this finding'}
|
||||||
try:
|
try:
|
||||||
if os.path.exists(file_path):
|
# Resolve path in case of cross-environment mismatch
|
||||||
os.remove(file_path)
|
download_folder = None
|
||||||
# Clean up empty parent directories
|
if self._config_manager:
|
||||||
parent = os.path.dirname(file_path)
|
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)
|
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)
|
os.rmdir(parent)
|
||||||
parent = os.path.dirname(parent)
|
parent = os.path.dirname(parent)
|
||||||
else:
|
else:
|
||||||
|
|
@ -900,6 +934,13 @@ class RepairWorker:
|
||||||
if not remove_ids:
|
if not remove_ids:
|
||||||
return {'success': False, 'error': 'No duplicates to remove'}
|
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
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = self.db._get_connection()
|
conn = self.db._get_connection()
|
||||||
|
|
@ -908,12 +949,40 @@ class RepairWorker:
|
||||||
cursor.execute(f"DELETE FROM tracks WHERE id IN ({placeholders})", remove_ids)
|
cursor.execute(f"DELETE FROM tracks WHERE id IN ({placeholders})", remove_ids)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
removed = cursor.rowcount
|
removed = cursor.rowcount
|
||||||
return {'success': True, 'action': 'removed_duplicates',
|
|
||||||
'message': f'Kept best quality copy, removed {removed} duplicate(s)'}
|
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
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:
|
def dismiss_finding(self, finding_id: int) -> bool:
|
||||||
"""Dismiss a finding."""
|
"""Dismiss a finding."""
|
||||||
conn = None
|
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-overlay" id="repair-modal" style="display:none;" onclick="if(event.target===this)closeRepairModal()">
|
||||||
<div class="repair-modal">
|
<div class="repair-modal">
|
||||||
<div class="repair-modal-header">
|
<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">
|
<div class="repair-modal-header-actions">
|
||||||
<label class="repair-master-toggle">
|
<label class="repair-master-toggle">
|
||||||
<input type="checkbox" id="repair-master-toggle" onchange="toggleRepairMaster()">
|
<input type="checkbox" id="repair-master-toggle" onchange="toggleRepairMaster()">
|
||||||
|
|
|
||||||
|
|
@ -50754,7 +50754,9 @@ function renderRepairFindingsPagination(total, currentPage) {
|
||||||
async function fixRepairFinding(id) {
|
async function fixRepairFinding(id) {
|
||||||
const card = document.querySelector(`.repair-finding-card[data-id="${id}"]`);
|
const card = document.querySelector(`.repair-finding-card[data-id="${id}"]`);
|
||||||
const fixBtn = card ? card.querySelector('.repair-finding-btn.fix') : null;
|
const fixBtn = card ? card.querySelector('.repair-finding-btn.fix') : null;
|
||||||
|
let originalText = '';
|
||||||
if (fixBtn) {
|
if (fixBtn) {
|
||||||
|
originalText = fixBtn.textContent;
|
||||||
fixBtn.disabled = true;
|
fixBtn.disabled = true;
|
||||||
fixBtn.textContent = '...';
|
fixBtn.textContent = '...';
|
||||||
}
|
}
|
||||||
|
|
@ -50774,7 +50776,7 @@ async function fixRepairFinding(id) {
|
||||||
showToast('Error applying fix', 'error');
|
showToast('Error applying fix', 'error');
|
||||||
if (fixBtn) {
|
if (fixBtn) {
|
||||||
fixBtn.disabled = false;
|
fixBtn.disabled = false;
|
||||||
fixBtn.textContent = 'Fix';
|
fixBtn.textContent = originalText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39733,8 +39733,31 @@ tr.tag-diff-same {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 20px 24px;
|
padding: 18px 24px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
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 {
|
.repair-modal-title {
|
||||||
|
|
@ -39742,12 +39765,21 @@ tr.tag-diff-same {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin: 0;
|
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 {
|
.repair-modal-header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repair-modal-close {
|
.repair-modal-close {
|
||||||
|
|
@ -40885,6 +40917,19 @@ tr.tag-diff-same {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repair-modal-logo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repair-modal-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repair-modal-subtitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.repair-tab-content {
|
.repair-tab-content {
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue