feat: username and password scrobbling for last.fm
This commit is contained in:
parent
3974ec7551
commit
43be45b76f
3 changed files with 289 additions and 5 deletions
163
index.html
163
index.html
|
|
@ -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">
|
||||
|
|
|
|||
49
js/lastfm.js
49
js/lastfm.js
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ========================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue