Fix cover.jpg not using Cover Art Archive during wishlist processing
cover.jpg was always written from Spotify/iTunes URL (640x640) when the first track in an album reached _download_cover_art before MusicBrainz lookup completed. Later tracks with MBID skipped because file existed. Fix: when cover.jpg exists but is small (<200KB) and we now have a CAA MBID, attempt to upgrade it with the high-res CAA version. If CAA fetch fails, keep existing cover — no pointless overwrites. Also adds Forgot PIN recovery to the profile selection PIN dialog, reusing the same credential verification flow as the launch lock screen. Backend reset endpoint now accepts profile_id parameter.
This commit is contained in:
parent
4439873542
commit
58ee8d8a8a
4 changed files with 117 additions and 7 deletions
|
|
@ -18181,15 +18181,33 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
|
|||
return
|
||||
try:
|
||||
cover_path = os.path.join(target_dir, "cover.jpg")
|
||||
release_mbid = album_info.get('musicbrainz_release_id') if album_info else None
|
||||
prefer_caa = config_manager.get('metadata_enhancement.prefer_caa_art', False)
|
||||
|
||||
# If cover.jpg exists but we now have a CAA MBID, check if we should upgrade
|
||||
if os.path.exists(cover_path):
|
||||
return
|
||||
if release_mbid and prefer_caa:
|
||||
try:
|
||||
existing_size = os.path.getsize(cover_path)
|
||||
# Typical Spotify/iTunes cover is ~50-150KB at 640x640
|
||||
# CAA covers are usually 300KB+ at 1200x1200
|
||||
if existing_size > 200_000:
|
||||
return # Already high-res, skip
|
||||
# Low-res cover exists — try to upgrade from CAA
|
||||
is_upgrade = True
|
||||
print(f"🔄 Existing cover.jpg is {existing_size // 1024}KB — attempting CAA upgrade...")
|
||||
except Exception:
|
||||
return
|
||||
else:
|
||||
return
|
||||
else:
|
||||
is_upgrade = False
|
||||
|
||||
image_data = None
|
||||
|
||||
# Try Cover Art Archive first (often 1200x1200+, original quality) — opt-in
|
||||
# The MBID is stored in album_info by _enhance_file_metadata before this is called
|
||||
release_mbid = album_info.get('musicbrainz_release_id')
|
||||
if release_mbid and config_manager.get('metadata_enhancement.prefer_caa_art', False):
|
||||
if release_mbid and prefer_caa:
|
||||
try:
|
||||
caa_url = f"https://coverartarchive.org/release/{release_mbid}/front"
|
||||
req = urllib.request.Request(caa_url, headers={'Accept': 'image/*'})
|
||||
|
|
@ -18202,6 +18220,11 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
|
|||
except Exception:
|
||||
image_data = None
|
||||
|
||||
# If upgrading and CAA failed, keep existing cover — don't overwrite with same low-res
|
||||
if is_upgrade and not image_data:
|
||||
print(f"📷 CAA upgrade failed — keeping existing cover.jpg")
|
||||
return
|
||||
|
||||
# Fallback to Spotify/iTunes/Deezer URL (typically 640x640)
|
||||
if not image_data:
|
||||
art_url = album_info.get('album_image_url')
|
||||
|
|
@ -36702,13 +36725,16 @@ def reset_pin_via_credential():
|
|||
if not matched:
|
||||
return jsonify({'success': False, 'error': 'Credential does not match any configured service'}), 401
|
||||
|
||||
# Credential verified — clear admin PIN and disable launch lock
|
||||
# Credential verified — clear PIN for the requested profile (default: admin)
|
||||
database = get_database()
|
||||
database.update_profile(1, pin_hash=None)
|
||||
config_manager.set('security.require_pin_on_launch', False)
|
||||
target_profile = data.get('profile_id', 1)
|
||||
database.update_profile(target_profile, pin_hash=None)
|
||||
# If clearing admin PIN, also disable launch lock
|
||||
if target_profile == 1:
|
||||
config_manager.set('security.require_pin_on_launch', False)
|
||||
session['launch_pin_verified'] = True
|
||||
|
||||
return jsonify({'success': True, 'message': 'PIN cleared and lock screen disabled. You can set a new PIN in Settings.'})
|
||||
return jsonify({'success': True, 'message': 'PIN cleared. You can set a new PIN in Settings.'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
<button id="profile-pin-submit" class="profile-pin-submit">Submit</button>
|
||||
</div>
|
||||
<p id="profile-pin-error" class="profile-pin-error" style="display: none;"></p>
|
||||
<a href="#" id="profile-pin-forgot" class="profile-pin-forgot" onclick="event.preventDefault(); showProfileForgotPin()">Forgot PIN?</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Profile Management Panel -->
|
||||
|
|
|
|||
|
|
@ -1210,6 +1210,75 @@ async function submitRecoveryCredential() {
|
|||
btn.textContent = 'Verify & Reset PIN';
|
||||
}
|
||||
|
||||
// ── Profile PIN Forgot Recovery ────────────────────────────────────────
|
||||
function showProfileForgotPin() {
|
||||
const dialog = document.getElementById('profile-pin-dialog');
|
||||
const content = dialog.querySelector('.profile-pin-content');
|
||||
|
||||
// Store the profile ID we're recovering for
|
||||
const profileName = document.getElementById('profile-pin-name').textContent;
|
||||
|
||||
// Replace dialog content with recovery form
|
||||
content.dataset.prevHtml = content.innerHTML;
|
||||
content.innerHTML = `
|
||||
<p style="color:#fff;font-size:14px;font-weight:600;margin-bottom:4px">Reset PIN for ${profileName}</p>
|
||||
<p style="color:rgba(255,255,255,0.5);font-size:12px;margin-bottom:12px">Enter any configured API credential<br>(Spotify secret, Plex token, etc.)</p>
|
||||
<input type="password" id="profile-recovery-input" class="profile-pin-input" maxlength="200" placeholder="Paste API credential" autocomplete="off">
|
||||
<div class="profile-pin-buttons">
|
||||
<button id="profile-recovery-cancel" class="profile-pin-cancel">Back</button>
|
||||
<button id="profile-recovery-submit" class="profile-pin-submit">Verify & Reset</button>
|
||||
</div>
|
||||
<p id="profile-recovery-error" class="profile-pin-error" style="display:none"></p>
|
||||
`;
|
||||
setTimeout(() => document.getElementById('profile-recovery-input').focus(), 100);
|
||||
|
||||
document.getElementById('profile-recovery-cancel').onclick = () => {
|
||||
content.innerHTML = content.dataset.prevHtml;
|
||||
};
|
||||
|
||||
document.getElementById('profile-recovery-submit').onclick = async () => {
|
||||
const input = document.getElementById('profile-recovery-input');
|
||||
const error = document.getElementById('profile-recovery-error');
|
||||
const credential = input.value.trim();
|
||||
if (!credential) return;
|
||||
|
||||
const btn = document.getElementById('profile-recovery-submit');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Verifying...';
|
||||
error.style.display = 'none';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/profiles/reset-pin-via-credential', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credential, profile_id: dialog._profileId || 1 })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
dialog.style.display = 'none';
|
||||
content.innerHTML = content.dataset.prevHtml;
|
||||
showToast('PIN cleared. You can set a new one in Settings.', 'success');
|
||||
// Re-try selecting the profile (now PIN-free)
|
||||
if (dialog._profileId) selectProfile(dialog._profileId);
|
||||
} else {
|
||||
error.textContent = data.error || 'Credential not recognized';
|
||||
error.style.display = 'block';
|
||||
input.value = '';
|
||||
input.focus();
|
||||
}
|
||||
} catch (e) {
|
||||
error.textContent = 'Connection error';
|
||||
error.style.display = 'block';
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Verify & Reset';
|
||||
};
|
||||
|
||||
document.getElementById('profile-recovery-input').onkeydown = (e) => {
|
||||
if (e.key === 'Enter') document.getElementById('profile-recovery-submit').click();
|
||||
};
|
||||
}
|
||||
|
||||
function showProfilePicker(profiles, canCancel = false) {
|
||||
const overlay = document.getElementById('profile-picker-overlay');
|
||||
const grid = document.getElementById('profile-picker-grid');
|
||||
|
|
@ -1309,6 +1378,7 @@ function showPinDialog(profile) {
|
|||
nameEl.textContent = profile.name;
|
||||
input.value = '';
|
||||
errorEl.style.display = 'none';
|
||||
dialog._profileId = profile.id;
|
||||
dialog.style.display = 'flex';
|
||||
setTimeout(() => input.focus(), 100);
|
||||
|
||||
|
|
|
|||
|
|
@ -39716,6 +39716,19 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.profile-pin-forgot {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile-pin-forgot:hover {
|
||||
color: rgba(138, 43, 226, 0.8);
|
||||
}
|
||||
|
||||
/* Profile Management Panel */
|
||||
.profile-manage-panel {
|
||||
position: fixed;
|
||||
|
|
|
|||
Loading…
Reference in a new issue