feat: username and password scrobbling for last.fm

This commit is contained in:
Eduard Prigoana 2026-02-09 12:33:11 +00:00
parent 3974ec7551
commit 43be45b76f
3 changed files with 289 additions and 5 deletions

View file

@ -2595,6 +2595,169 @@
<div id="lastfm-controls">
<button id="lastfm-connect-btn" class="btn-secondary">Connect Last.fm</button>
</div>
<div id="lastfm-credential-auth" style="display: none; margin-top: 12px">
<div id="lastfm-credential-form" style="display: none">
<p style="margin: 0 0 8px 0; font-size: 0.85rem; color: var(--muted)">
Enter your Last.fm credentials:
</p>
<input
type="text"
id="lastfm-username"
placeholder="Username"
style="
padding: 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--background);
color: var(--foreground);
width: 100%;
margin-bottom: 8px;
"
/>
<input
type="password"
id="lastfm-password"
placeholder="Password"
style="
padding: 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--background);
color: var(--foreground);
width: 100%;
margin-bottom: 8px;
"
/>
<div style="display: flex; gap: 8px">
<button
id="lastfm-login-credentials"
class="btn-secondary"
style="padding: 6px 12px; font-size: 0.85rem; flex: 1"
>
Login
</button>
<button
id="lastfm-use-oauth"
class="btn-secondary"
style="padding: 6px 12px; font-size: 0.85rem; flex: 1"
>
Use OAuth Instead
</button>
</div>
</div>
</div>
<div id="lastfm-credential-auth" style="display: none; margin-top: 12px">
<div style="display: flex; flex-direction: column; gap: 8px">
<div id="lastfm-credential-toggle-container">
<button
id="lastfm-show-credential-auth"
class="btn-secondary"
style="padding: 6px 12px; font-size: 0.85rem; width: 100%"
>
Login with Username/Password
</button>
</div>
<div id="lastfm-credential-form" style="display: none">
<input
type="text"
id="lastfm-username"
placeholder="Last.fm Username"
style="
padding: 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--background);
color: var(--foreground);
width: 100%;
margin-bottom: 8px;
"
/>
<input
type="password"
id="lastfm-password"
placeholder="Last.fm Password"
style="
padding: 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--background);
color: var(--foreground);
width: 100%;
margin-bottom: 8px;
"
/>
<div style="display: flex; gap: 8px">
<button
id="lastfm-login-credentials"
class="btn-secondary"
style="padding: 6px 12px; font-size: 0.85rem; flex: 1"
>
Login
</button>
<button
id="lastfm-use-oauth"
class="btn-secondary"
style="padding: 6px 12px; font-size: 0.85rem; flex: 1"
>
Use OAuth
</button>
</div>
</div>
</div>
</div>
<div id="lastfm-credential-auth" style="margin-top: 12px; display: none">
<div style="display: flex; flex-direction: column; gap: 8px">
<input
type="text"
id="lastfm-username"
placeholder="Last.fm Username"
style="
padding: 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--background);
color: var(--foreground);
"
/>
<input
type="password"
id="lastfm-password"
placeholder="Last.fm Password"
style="
padding: 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--background);
color: var(--foreground);
"
/>
<div style="display: flex; gap: 8px; margin-top: 4px">
<button
id="lastfm-login-credentials"
class="btn-secondary"
style="padding: 6px 12px; font-size: 0.85rem"
>
Login with Credentials
</button>
<button
id="lastfm-use-oauth"
class="btn-secondary"
style="padding: 6px 12px; font-size: 0.85rem"
>
Use OAuth Instead
</button>
</div>
<div style="display: flex; gap: 8px; margin-top: 4px">
<button
id="lastfm-show-credential-auth"
class="btn-secondary"
style="padding: 6px 12px; font-size: 0.85rem"
>
Login with Username/Password
</button>
</div>
</div>
</div>
</div>
<div class="setting-item" id="lastfm-toggle-setting" style="display: none">

View file

@ -190,6 +190,55 @@ export class LastFMScrobbler {
}
}
async authenticateWithCredentials(username, password) {
try {
const params = {
username: username,
password: password,
api_key: this.API_KEY,
method: 'auth.getMobileSession',
};
const signature = await this.generateSignature(params);
const formData = new URLSearchParams({
username: username,
password: password,
api_key: this.API_KEY,
method: 'auth.getMobileSession',
api_sig: signature,
format: 'json',
});
const response = await fetch(this.API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData,
});
const data = await response.json();
if (data.error) {
throw new Error(data.message || 'Last.fm authentication error');
}
if (data.session) {
this.saveSession(data.session.key, data.session.name);
return {
success: true,
username: data.session.name,
};
}
throw new Error('No session returned');
} catch (error) {
console.error('Mobile authentication failed:', error);
throw error;
}
}
async updateNowPlaying(track) {
if (!this.isAuthenticated()) return;

View file

@ -130,6 +130,12 @@ export function initializeSettings(scrobbler, player, api, ui) {
const lastfmCustomApiSecret = document.getElementById('lastfm-custom-api-secret');
const lastfmSaveCustomCreds = document.getElementById('lastfm-save-custom-creds');
const lastfmClearCustomCreds = document.getElementById('lastfm-clear-custom-creds');
const lastfmCredentialAuth = document.getElementById('lastfm-credential-auth');
const lastfmCredentialForm = document.getElementById('lastfm-credential-form');
const lastfmUsernameInput = document.getElementById('lastfm-username');
const lastfmPasswordInput = document.getElementById('lastfm-password');
const lastfmLoginCredentialsBtn = document.getElementById('lastfm-login-credentials');
const lastfmUseOAuthBtn = document.getElementById('lastfm-use-oauth');
function updateLastFMUI() {
if (scrobbler.lastfm.isAuthenticated()) {
@ -143,6 +149,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
lastfmCustomCredsToggleSetting.style.display = 'flex';
lastfmCustomCredsToggle.checked = lastFMStorage.useCustomCredentials();
updateCustomCredsUI();
hideCredentialAuth();
} else {
lastfmStatus.textContent = 'Connect your Last.fm account to scrobble tracks';
lastfmConnectBtn.textContent = 'Connect Last.fm';
@ -151,9 +158,25 @@ export function initializeSettings(scrobbler, player, api, ui) {
lastfmLoveSetting.style.display = 'none';
lastfmCustomCredsToggleSetting.style.display = 'none';
lastfmCustomCredsSetting.style.display = 'none';
// Hide credential auth by default - only show on OAuth failure
hideCredentialAuth();
}
}
function showCredentialAuth() {
if (lastfmCredentialAuth) lastfmCredentialAuth.style.display = 'block';
if (lastfmCredentialForm) lastfmCredentialForm.style.display = 'block';
// Focus on username field
if (lastfmUsernameInput) lastfmUsernameInput.focus();
}
function hideCredentialAuth() {
if (lastfmCredentialAuth) lastfmCredentialAuth.style.display = 'none';
if (lastfmCredentialForm) lastfmCredentialForm.style.display = 'none';
if (lastfmUsernameInput) lastfmUsernameInput.value = '';
if (lastfmPasswordInput) lastfmPasswordInput.value = '';
}
function updateCustomCredsUI() {
const useCustom = lastFMStorage.useCustomCredentials();
lastfmCustomCredsSetting.style.display = useCustom ? 'flex' : 'none';
@ -197,17 +220,22 @@ export function initializeSettings(scrobbler, player, api, ui) {
lastfmConnectBtn.textContent = 'Waiting for authorization...';
let attempts = 0;
const maxAttempts = 30;
const maxAttempts = 5;
const checkAuth = setInterval(async () => {
attempts++;
if (attempts > maxAttempts) {
clearInterval(checkAuth);
if (authWindow && !authWindow.closed) authWindow.close();
lastfmConnectBtn.textContent = 'Connect Last.fm';
lastfmConnectBtn.disabled = false;
if (authWindow && !authWindow.closed) authWindow.close();
alert('Authorization timed out. Please try again.');
// Ask user if they want to use credentials instead
if (
confirm('Authorization timed out. Would you like to login with username and password instead?')
) {
showCredentialAuth();
}
return;
}
@ -229,10 +257,13 @@ export function initializeSettings(scrobbler, player, api, ui) {
}, 2000);
} catch (error) {
console.error('Last.fm connection failed:', error);
alert('Failed to connect to Last.fm: ' + error.message);
if (authWindow && !authWindow.closed) authWindow.close();
lastfmConnectBtn.textContent = 'Connect Last.fm';
lastfmConnectBtn.disabled = false;
if (authWindow && !authWindow.closed) authWindow.close();
// Ask user if they want to use credentials instead
if (confirm('Failed to connect to Last.fm. Would you like to login with username and password instead?')) {
showCredentialAuth();
}
}
});
@ -321,6 +352,47 @@ export function initializeSettings(scrobbler, player, api, ui) {
});
}
// Last.fm Credential Auth - Login with credentials
if (lastfmLoginCredentialsBtn) {
lastfmLoginCredentialsBtn.addEventListener('click', async () => {
const username = lastfmUsernameInput?.value?.trim();
const password = lastfmPasswordInput?.value;
if (!username || !password) {
alert('Please enter both username and password.');
return;
}
lastfmLoginCredentialsBtn.disabled = true;
lastfmLoginCredentialsBtn.textContent = 'Logging in...';
try {
const result = await scrobbler.lastfm.authenticateWithCredentials(username, password);
if (result.success) {
lastFMStorage.setEnabled(true);
lastfmToggle.checked = true;
updateLastFMUI();
// Clear password for security
if (lastfmPasswordInput) lastfmPasswordInput.value = '';
alert(`Successfully connected to Last.fm as ${result.username}!`);
}
} catch (error) {
console.error('Last.fm credential login failed:', error);
alert('Failed to login: ' + error.message);
} finally {
lastfmLoginCredentialsBtn.disabled = false;
lastfmLoginCredentialsBtn.textContent = 'Login';
}
});
}
// Last.fm Credential Auth - Switch back to OAuth
if (lastfmUseOAuthBtn) {
lastfmUseOAuthBtn.addEventListener('click', () => {
hideCredentialAuth();
});
}
// ========================================
// Global Scrobble Settings
// ========================================