From 2522e0e5bedd8552802cae3ae3cfc082374d5d48 Mon Sep 17 00:00:00 2001 From: Eduard Prigoana Date: Sun, 8 Feb 2026 22:36:07 +0000 Subject: [PATCH] scrobbling and skeleton changes --- index.html | 178 +++++++++++++++++++++++++++------------------ js/lastfm.js | 14 +++- js/librefm.js | 16 +++- js/listenbrainz.js | 24 ++++-- js/maloja.js | 23 ++++-- js/settings.js | 34 +++++++++ js/storage.js | 18 +++++ js/ui.js | 4 +- todo.md | 3 - 9 files changed, 224 insertions(+), 90 deletions(-) diff --git a/index.html b/index.html index f6e5ec9..c2feb8b 100644 --- a/index.html +++ b/index.html @@ -2396,41 +2396,40 @@
- ListenBrainz Scrobbling + Scrobble Threshold Submit listens to ListenBrainz (requires User Token)Percentage of track to play before scrobbling (1-100%)
- -
- @@ -2472,6 +2471,85 @@
+
+
+
+ Libre.fm Scrobbling + Connect your Libre.fm account to scrobble tracks +
+
+ +
+
+ + + + +
+ +
+
+
+ ListenBrainz Scrobbling + Submit listens to ListenBrainz (requires User Token) +
+ +
+ + +
+
@@ -2510,44 +2588,6 @@ />
- -
-
-
- Libre.fm Scrobbling - Connect your Libre.fm account to scrobble tracks -
-
- -
-
- - - - -
diff --git a/js/lastfm.js b/js/lastfm.js index 9f4844e..c874511 100644 --- a/js/lastfm.js +++ b/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; } } diff --git a/js/librefm.js b/js/librefm.js index 8f715e4..e699991 100644 --- a/js/librefm.js +++ b/js/librefm.js @@ -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; } } diff --git a/js/listenbrainz.js b/js/listenbrainz.js index cdb24af..44ff52c 100644 --- a/js/listenbrainz.js +++ b/js/listenbrainz.js @@ -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) { diff --git a/js/maloja.js b/js/maloja.js index cd9746f..cbc809e 100644 --- a/js/maloja.js +++ b/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) { diff --git a/js/settings.js b/js/settings.js index be6c6eb..7007a55 100644 --- a/js/settings.js +++ b/js/settings.js @@ -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 // ======================================== diff --git a/js/storage.js b/js/storage.js index bd2831d..22fd1e7 100644 --- a/js/storage.js +++ b/js/storage.js @@ -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 = { diff --git a/js/ui.js b/js/ui.js index 9ab52ee..3759815 100644 --- a/js/ui.js +++ b/js/ui.js @@ -566,10 +566,10 @@ export class UIRenderer { } createSkeletonCards(count = 6, isArtist = false) { - return `
${Array(count) + return Array(count) .fill(0) .map(() => this.createSkeletonCard(isArtist)) - .join('')}
`; + .join(''); } setupSearchClearButton(inputElement, clearBtnSelector = '.search-clear-btn') { diff --git a/todo.md b/todo.md index de59d21..2a72170 100644 --- a/todo.md +++ b/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)