diff --git a/index.html b/index.html
index f6e5ec9..c2feb8b 100644
--- a/index.html
+++ b/index.html
@@ -2396,41 +2396,40 @@
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)