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
|
return
|
||||||
try:
|
try:
|
||||||
cover_path = os.path.join(target_dir, "cover.jpg")
|
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):
|
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
|
image_data = None
|
||||||
|
|
||||||
# Try Cover Art Archive first (often 1200x1200+, original quality) — opt-in
|
# 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
|
# 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 prefer_caa:
|
||||||
if release_mbid and config_manager.get('metadata_enhancement.prefer_caa_art', False):
|
|
||||||
try:
|
try:
|
||||||
caa_url = f"https://coverartarchive.org/release/{release_mbid}/front"
|
caa_url = f"https://coverartarchive.org/release/{release_mbid}/front"
|
||||||
req = urllib.request.Request(caa_url, headers={'Accept': 'image/*'})
|
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:
|
except Exception:
|
||||||
image_data = None
|
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)
|
# Fallback to Spotify/iTunes/Deezer URL (typically 640x640)
|
||||||
if not image_data:
|
if not image_data:
|
||||||
art_url = album_info.get('album_image_url')
|
art_url = album_info.get('album_image_url')
|
||||||
|
|
@ -36702,13 +36725,16 @@ def reset_pin_via_credential():
|
||||||
if not matched:
|
if not matched:
|
||||||
return jsonify({'success': False, 'error': 'Credential does not match any configured service'}), 401
|
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 = get_database()
|
||||||
database.update_profile(1, pin_hash=None)
|
target_profile = data.get('profile_id', 1)
|
||||||
config_manager.set('security.require_pin_on_launch', False)
|
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
|
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:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
<button id="profile-pin-submit" class="profile-pin-submit">Submit</button>
|
<button id="profile-pin-submit" class="profile-pin-submit">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="profile-pin-error" class="profile-pin-error" style="display: none;"></p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<!-- Profile Management Panel -->
|
<!-- Profile Management Panel -->
|
||||||
|
|
|
||||||
|
|
@ -1210,6 +1210,75 @@ async function submitRecoveryCredential() {
|
||||||
btn.textContent = 'Verify & Reset PIN';
|
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) {
|
function showProfilePicker(profiles, canCancel = false) {
|
||||||
const overlay = document.getElementById('profile-picker-overlay');
|
const overlay = document.getElementById('profile-picker-overlay');
|
||||||
const grid = document.getElementById('profile-picker-grid');
|
const grid = document.getElementById('profile-picker-grid');
|
||||||
|
|
@ -1309,6 +1378,7 @@ function showPinDialog(profile) {
|
||||||
nameEl.textContent = profile.name;
|
nameEl.textContent = profile.name;
|
||||||
input.value = '';
|
input.value = '';
|
||||||
errorEl.style.display = 'none';
|
errorEl.style.display = 'none';
|
||||||
|
dialog._profileId = profile.id;
|
||||||
dialog.style.display = 'flex';
|
dialog.style.display = 'flex';
|
||||||
setTimeout(() => input.focus(), 100);
|
setTimeout(() => input.focus(), 100);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39716,6 +39716,19 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
||||||
margin-top: 12px;
|
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 Management Panel */
|
||||||
.profile-manage-panel {
|
.profile-manage-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue