scrobbling and skeleton changes
This commit is contained in:
parent
09a60753f1
commit
2522e0e5be
9 changed files with 224 additions and 90 deletions
178
index.html
178
index.html
|
|
@ -2396,41 +2396,40 @@
|
|||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">ListenBrainz Scrobbling</span>
|
||||
<span class="label">Scrobble Threshold</span>
|
||||
<span class="description"
|
||||
>Submit listens to ListenBrainz (requires User Token)</span
|
||||
>Percentage of track to play before scrobbling (1-100%)</span
|
||||
>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="listenbrainz-enabled-toggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item" id="listenbrainz-token-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">User Token</span>
|
||||
<span class="description">Found on your ListenBrainz profile page</span>
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<input
|
||||
type="range"
|
||||
id="scrobble-percentage-slider"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
value="75"
|
||||
style="width: 100px"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
id="scrobble-percentage-input"
|
||||
min="1"
|
||||
max="100"
|
||||
value="75"
|
||||
style="
|
||||
width: 50px;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
"
|
||||
/>
|
||||
<span style="font-size: 0.9rem">%</span>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="listenbrainz-token-input"
|
||||
placeholder="Enter Token"
|
||||
class="template-input"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item" id="listenbrainz-custom-url-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">Custom API URL (Optional)</span>
|
||||
<span class="description">Leave empty to use official ListenBrainz server</span>
|
||||
</div>
|
||||
<input
|
||||
type="url"
|
||||
id="listenbrainz-custom-url-input"
|
||||
placeholder="https://api.listenbrainz.org/1"
|
||||
class="template-input"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -2472,6 +2471,85 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Libre.fm Scrobbling</span>
|
||||
<span class="description" id="librefm-status"
|
||||
>Connect your Libre.fm account to scrobble tracks</span
|
||||
>
|
||||
</div>
|
||||
<div id="librefm-controls">
|
||||
<button id="librefm-connect-btn" class="btn-secondary">Connect Libre.fm</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item" id="librefm-toggle-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">Enable Scrobbling</span>
|
||||
<span class="description">Automatically scrobble played tracks</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="librefm-toggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item" id="librefm-love-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">Love on Like</span>
|
||||
<span class="description"
|
||||
>Automatically 'love' tracks on Libre.fm when you like them</span
|
||||
>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="librefm-love-toggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">ListenBrainz Scrobbling</span>
|
||||
<span class="description"
|
||||
>Submit listens to ListenBrainz (requires User Token)</span
|
||||
>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="listenbrainz-enabled-toggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item" id="listenbrainz-token-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">User Token</span>
|
||||
<span class="description">Found on your ListenBrainz profile page</span>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="listenbrainz-token-input"
|
||||
placeholder="Enter Token"
|
||||
class="template-input"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item" id="listenbrainz-custom-url-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">Custom API URL (Optional)</span>
|
||||
<span class="description">Leave empty to use official ListenBrainz server</span>
|
||||
</div>
|
||||
<input
|
||||
type="url"
|
||||
id="listenbrainz-custom-url-input"
|
||||
placeholder="https://api.listenbrainz.org/1"
|
||||
class="template-input"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
|
|
@ -2510,44 +2588,6 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Libre.fm Scrobbling</span>
|
||||
<span class="description" id="librefm-status"
|
||||
>Connect your Libre.fm account to scrobble tracks</span
|
||||
>
|
||||
</div>
|
||||
<div id="librefm-controls">
|
||||
<button id="librefm-connect-btn" class="btn-secondary">Connect Libre.fm</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item" id="librefm-toggle-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">Enable Scrobbling</span>
|
||||
<span class="description">Automatically scrobble played tracks</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="librefm-toggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item" id="librefm-love-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">Love on Like</span>
|
||||
<span class="description"
|
||||
>Automatically 'love' tracks on Libre.fm when you like them</span
|
||||
>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="librefm-love-toggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-tab-content" id="settings-tab-audio">
|
||||
|
|
|
|||
14
js/lastfm.js
14
js/lastfm.js
|
|
@ -13,6 +13,7 @@ export class LastFMScrobbler {
|
|||
this.scrobbleTimer = null;
|
||||
this.scrobbleThreshold = 0;
|
||||
this.hasScrobbled = false;
|
||||
this.isScrobbling = false;
|
||||
|
||||
this.loadSession();
|
||||
}
|
||||
|
|
@ -178,7 +179,11 @@ export class LastFMScrobbler {
|
|||
if (!this.isAuthenticated()) return;
|
||||
|
||||
this.currentTrack = track;
|
||||
this.hasScrobbled = false;
|
||||
// Only reset hasScrobbled if we're not currently in the middle of scrobbling
|
||||
// to prevent race conditions that could cause double scrobbles
|
||||
if (!this.isScrobbling) {
|
||||
this.hasScrobbled = false;
|
||||
}
|
||||
this.clearScrobbleTimer();
|
||||
|
||||
try {
|
||||
|
|
@ -204,7 +209,8 @@ export class LastFMScrobbler {
|
|||
|
||||
console.log('Now playing updated:', scrobbleTitle);
|
||||
|
||||
this.scrobbleThreshold = Math.min(track.duration / 2, 240);
|
||||
const scrobblePercentage = lastFMStorage.getScrobblePercentage() / 100;
|
||||
this.scrobbleThreshold = Math.min(track.duration * scrobblePercentage, 240);
|
||||
this.scheduleScrobble(this.scrobbleThreshold * 1000);
|
||||
} catch (error) {
|
||||
console.error('Failed to update now playing:', error);
|
||||
|
|
@ -229,6 +235,8 @@ export class LastFMScrobbler {
|
|||
async scrobbleCurrentTrack() {
|
||||
if (!this.isAuthenticated() || !this.currentTrack || this.hasScrobbled) return;
|
||||
|
||||
this.isScrobbling = true;
|
||||
|
||||
try {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const scrobbleTitle = this.currentTrack.cleanTitle || this.currentTrack.title;
|
||||
|
|
@ -257,6 +265,8 @@ export class LastFMScrobbler {
|
|||
console.log('Scrobbled:', this.currentTrack.cleanTitle || this.currentTrack.title);
|
||||
} catch (error) {
|
||||
console.error('Failed to scrobble:', error);
|
||||
} finally {
|
||||
this.isScrobbling = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { libreFmSettings } from './storage.js';
|
||||
import { libreFmSettings, lastFMStorage } from './storage.js';
|
||||
|
||||
export class LibreFmScrobbler {
|
||||
constructor() {
|
||||
|
|
@ -12,6 +12,7 @@ export class LibreFmScrobbler {
|
|||
this.scrobbleTimer = null;
|
||||
this.scrobbleThreshold = 0;
|
||||
this.hasScrobbled = false;
|
||||
this.isScrobbling = false;
|
||||
|
||||
this.loadSession();
|
||||
}
|
||||
|
|
@ -174,7 +175,11 @@ export class LibreFmScrobbler {
|
|||
if (!this.isAuthenticated()) return;
|
||||
|
||||
this.currentTrack = track;
|
||||
this.hasScrobbled = false;
|
||||
// Only reset hasScrobbled if we're not currently in the middle of scrobbling
|
||||
// to prevent race conditions that could cause double scrobbles
|
||||
if (!this.isScrobbling) {
|
||||
this.hasScrobbled = false;
|
||||
}
|
||||
this.clearScrobbleTimer();
|
||||
|
||||
try {
|
||||
|
|
@ -200,7 +205,8 @@ export class LibreFmScrobbler {
|
|||
|
||||
console.log('[Libre.fm] Now playing updated:', scrobbleTitle);
|
||||
|
||||
this.scrobbleThreshold = Math.min(track.duration / 2, 240);
|
||||
const scrobblePercentage = lastFMStorage.getScrobblePercentage() / 100;
|
||||
this.scrobbleThreshold = Math.min(track.duration * scrobblePercentage, 240);
|
||||
this.scheduleScrobble(this.scrobbleThreshold * 1000);
|
||||
} catch (error) {
|
||||
console.error('[Libre.fm] Failed to update now playing:', error);
|
||||
|
|
@ -225,6 +231,8 @@ export class LibreFmScrobbler {
|
|||
async scrobbleCurrentTrack() {
|
||||
if (!this.isAuthenticated() || !this.currentTrack || this.hasScrobbled) return;
|
||||
|
||||
this.isScrobbling = true;
|
||||
|
||||
try {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const scrobbleTitle = this.currentTrack.cleanTitle || this.currentTrack.title;
|
||||
|
|
@ -253,6 +261,8 @@ export class LibreFmScrobbler {
|
|||
console.log('[Libre.fm] Scrobbled:', this.currentTrack.cleanTitle || this.currentTrack.title);
|
||||
} catch (error) {
|
||||
console.error('[Libre.fm] Failed to scrobble:', error);
|
||||
} finally {
|
||||
this.isScrobbling = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { listenBrainzSettings } from './storage.js';
|
||||
import { listenBrainzSettings, lastFMStorage } from './storage.js';
|
||||
|
||||
export class ListenBrainzScrobbler {
|
||||
constructor() {
|
||||
|
|
@ -7,6 +7,7 @@ export class ListenBrainzScrobbler {
|
|||
this.scrobbleTimer = null;
|
||||
this.scrobbleThreshold = 0;
|
||||
this.hasScrobbled = false;
|
||||
this.isScrobbling = false;
|
||||
}
|
||||
|
||||
getApiUrl() {
|
||||
|
|
@ -120,12 +121,17 @@ export class ListenBrainzScrobbler {
|
|||
if (!this.isEnabled()) return;
|
||||
|
||||
this.currentTrack = track;
|
||||
this.hasScrobbled = false;
|
||||
// Only reset hasScrobbled if we're not currently in the middle of scrobbling
|
||||
// to prevent race conditions that could cause double scrobbles
|
||||
if (!this.isScrobbling) {
|
||||
this.hasScrobbled = false;
|
||||
}
|
||||
this.clearScrobbleTimer();
|
||||
|
||||
await this.submitListen('playing_now', track);
|
||||
|
||||
this.scrobbleThreshold = Math.min(track.duration / 2, 240);
|
||||
const scrobblePercentage = lastFMStorage.getScrobblePercentage() / 100;
|
||||
this.scrobbleThreshold = Math.min(track.duration * scrobblePercentage, 240);
|
||||
this.scheduleScrobble(this.scrobbleThreshold * 1000);
|
||||
}
|
||||
|
||||
|
|
@ -146,9 +152,15 @@ export class ListenBrainzScrobbler {
|
|||
async scrobbleCurrentTrack() {
|
||||
if (!this.isEnabled() || !this.currentTrack || this.hasScrobbled) return;
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
await this.submitListen('single', this.currentTrack, timestamp);
|
||||
this.hasScrobbled = true;
|
||||
this.isScrobbling = true;
|
||||
|
||||
try {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
await this.submitListen('single', this.currentTrack, timestamp);
|
||||
this.hasScrobbled = true;
|
||||
} finally {
|
||||
this.isScrobbling = false;
|
||||
}
|
||||
}
|
||||
|
||||
onTrackChange(track) {
|
||||
|
|
|
|||
23
js/maloja.js
23
js/maloja.js
|
|
@ -1,4 +1,5 @@
|
|||
import { malojaSettings } from './storage.js';
|
||||
import { lastFMStorage } from './storage.js';
|
||||
|
||||
export class MalojaScrobbler {
|
||||
constructor() {
|
||||
|
|
@ -6,6 +7,7 @@ export class MalojaScrobbler {
|
|||
this.scrobbleTimer = null;
|
||||
this.scrobbleThreshold = 0;
|
||||
this.hasScrobbled = false;
|
||||
this.isScrobbling = false;
|
||||
}
|
||||
|
||||
getApiUrl() {
|
||||
|
|
@ -115,14 +117,19 @@ export class MalojaScrobbler {
|
|||
if (!this.isEnabled()) return;
|
||||
|
||||
this.currentTrack = track;
|
||||
this.hasScrobbled = false;
|
||||
// Only reset hasScrobbled if we're not currently in the middle of scrobbling
|
||||
// to prevent race conditions that could cause double scrobbles
|
||||
if (!this.isScrobbling) {
|
||||
this.hasScrobbled = false;
|
||||
}
|
||||
this.clearScrobbleTimer();
|
||||
|
||||
// Maloja doesn't have a separate "now playing" endpoint like Last.fm
|
||||
// It just scrobbles when the track is actually played
|
||||
// We'll set up the timer to scrobble after the threshold
|
||||
|
||||
this.scrobbleThreshold = Math.min(track.duration / 2, 240);
|
||||
const scrobblePercentage = lastFMStorage.getScrobblePercentage() / 100;
|
||||
this.scrobbleThreshold = Math.min(track.duration * scrobblePercentage, 240);
|
||||
this.scheduleScrobble(this.scrobbleThreshold * 1000);
|
||||
}
|
||||
|
||||
|
|
@ -143,9 +150,15 @@ export class MalojaScrobbler {
|
|||
async scrobbleCurrentTrack() {
|
||||
if (!this.isEnabled() || !this.currentTrack || this.hasScrobbled) return;
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
await this.submitScrobble(this.currentTrack, timestamp);
|
||||
this.hasScrobbled = true;
|
||||
this.isScrobbling = true;
|
||||
|
||||
try {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
await this.submitScrobble(this.currentTrack, timestamp);
|
||||
this.hasScrobbled = true;
|
||||
} finally {
|
||||
this.isScrobbling = false;
|
||||
}
|
||||
}
|
||||
|
||||
onTrackChange(track) {
|
||||
|
|
|
|||
|
|
@ -220,6 +220,40 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Global Scrobble Settings
|
||||
// ========================================
|
||||
const scrobblePercentageSlider = document.getElementById('scrobble-percentage-slider');
|
||||
const scrobblePercentageInput = document.getElementById('scrobble-percentage-input');
|
||||
|
||||
if (scrobblePercentageSlider && scrobblePercentageInput) {
|
||||
const percentage = lastFMStorage.getScrobblePercentage();
|
||||
scrobblePercentageSlider.value = percentage;
|
||||
scrobblePercentageInput.value = percentage;
|
||||
|
||||
scrobblePercentageSlider.addEventListener('input', (e) => {
|
||||
const newPercentage = parseInt(e.target.value, 10);
|
||||
scrobblePercentageInput.value = newPercentage;
|
||||
lastFMStorage.setScrobblePercentage(newPercentage);
|
||||
});
|
||||
|
||||
scrobblePercentageInput.addEventListener('change', (e) => {
|
||||
let newPercentage = parseInt(e.target.value, 10);
|
||||
newPercentage = Math.max(1, Math.min(100, newPercentage || 75));
|
||||
scrobblePercentageSlider.value = newPercentage;
|
||||
scrobblePercentageInput.value = newPercentage;
|
||||
lastFMStorage.setScrobblePercentage(newPercentage);
|
||||
});
|
||||
|
||||
scrobblePercentageInput.addEventListener('input', (e) => {
|
||||
let newPercentage = parseInt(e.target.value, 10);
|
||||
if (!isNaN(newPercentage) && newPercentage >= 1 && newPercentage <= 100) {
|
||||
scrobblePercentageSlider.value = newPercentage;
|
||||
lastFMStorage.setScrobblePercentage(newPercentage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ListenBrainz Settings
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -279,6 +279,10 @@ export const themeManager = {
|
|||
};
|
||||
|
||||
export const lastFMStorage = {
|
||||
STORAGE_KEY: 'lastfm-enabled',
|
||||
LOVE_ON_LIKE_KEY: 'lastfm-love-on-like',
|
||||
SCROBBLE_PERCENTAGE_KEY: 'lastfm-scrobble-percentage',
|
||||
|
||||
isEnabled() {
|
||||
try {
|
||||
return localStorage.getItem(this.STORAGE_KEY) === 'true';
|
||||
|
|
@ -302,6 +306,20 @@ export const lastFMStorage = {
|
|||
setLoveOnLike(enabled) {
|
||||
localStorage.setItem(this.LOVE_ON_LIKE_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
|
||||
getScrobblePercentage() {
|
||||
try {
|
||||
const value = localStorage.getItem(this.SCROBBLE_PERCENTAGE_KEY);
|
||||
return value ? parseInt(value, 10) : 75;
|
||||
} catch {
|
||||
return 75;
|
||||
}
|
||||
},
|
||||
|
||||
setScrobblePercentage(percentage) {
|
||||
const validPercentage = Math.max(1, Math.min(100, parseInt(percentage, 10) || 75));
|
||||
localStorage.setItem(this.SCROBBLE_PERCENTAGE_KEY, validPercentage.toString());
|
||||
},
|
||||
};
|
||||
|
||||
export const nowPlayingSettings = {
|
||||
|
|
|
|||
4
js/ui.js
4
js/ui.js
|
|
@ -566,10 +566,10 @@ export class UIRenderer {
|
|||
}
|
||||
|
||||
createSkeletonCards(count = 6, isArtist = false) {
|
||||
return `<div class="card-grid">${Array(count)
|
||||
return Array(count)
|
||||
.fill(0)
|
||||
.map(() => this.createSkeletonCard(isArtist))
|
||||
.join('')}</div>`;
|
||||
.join('');
|
||||
}
|
||||
|
||||
setupSearchClearButton(inputElement, clearBtnSelector = '.search-clear-btn') {
|
||||
|
|
|
|||
3
todo.md
3
todo.md
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
Sorted by ease of implementation (easiest to hardest):
|
||||
|
||||
- [x] Album click navigation: Clicking on the album in the player bar navigates to the album page (default behavior - can be changed in settings)
|
||||
- [ ] Fix button overlap: Next track and casting buttons overlap on some screen resolutions
|
||||
- [ ] Reduce API calls: Minimize unnecessary API calls throughout the app
|
||||
- [ ] Editor's Picks: Create a JSON file of curated album IDs with metadata for the home screen. Include an option to disable in settings to avoid extra API calls.
|
||||
|
||||
- [ ] Update notifications: Add ability to show the update popup in settings, with an option to automatically update (enabled by default)
|
||||
|
|
|
|||
Loading…
Reference in a new issue