176 lines
5.2 KiB
JavaScript
176 lines
5.2 KiB
JavaScript
import { malojaSettings } from './storage.js';
|
|
import { lastFMStorage } from './storage.js';
|
|
|
|
export class MalojaScrobbler {
|
|
constructor() {
|
|
this.currentTrack = null;
|
|
this.scrobbleTimer = null;
|
|
this.scrobbleThreshold = 0;
|
|
this.hasScrobbled = false;
|
|
this.isScrobbling = false;
|
|
}
|
|
|
|
getApiUrl() {
|
|
const customUrl = malojaSettings.getCustomUrl();
|
|
// Remove trailing slash if present
|
|
return customUrl ? customUrl.replace(/\/$/, '') : '';
|
|
}
|
|
|
|
isEnabled() {
|
|
return malojaSettings.isEnabled() && !!malojaSettings.getToken() && !!this.getApiUrl();
|
|
}
|
|
|
|
getApiKey() {
|
|
return malojaSettings.getToken();
|
|
}
|
|
|
|
_getScrobbleArtist(track) {
|
|
if (!track) return 'Unknown Artist';
|
|
|
|
let artistName = 'Unknown Artist';
|
|
|
|
if (track.artist?.name) {
|
|
artistName = track.artist.name;
|
|
} else if (typeof track.artist === 'string') {
|
|
artistName = track.artist;
|
|
} else if (track.artists && track.artists.length > 0) {
|
|
const first = track.artists[0];
|
|
artistName = typeof first === 'string' ? first : first.name || 'Unknown Artist';
|
|
}
|
|
|
|
if (typeof artistName !== 'string') return 'Unknown Artist';
|
|
|
|
artistName = artistName
|
|
.split(/\s*[&]\s*|\s+feat\.?\s+|\s+ft\.?\s+|\s+featuring\s+|\s+with\s+|\s+x\s+/i)[0]
|
|
.trim();
|
|
|
|
return artistName || 'Unknown Artist';
|
|
}
|
|
|
|
async submitScrobble(track, timestamp = null) {
|
|
if (!this.isEnabled()) return;
|
|
|
|
const apiUrl = this.getApiUrl();
|
|
const apiKey = this.getApiKey();
|
|
|
|
if (!apiUrl || !apiKey) return;
|
|
|
|
const artist = this._getScrobbleArtist(track);
|
|
const title = track.cleanTitle || track.title;
|
|
|
|
// Build the scrobble data
|
|
const scrobbleData = {
|
|
artist: artist,
|
|
title: title,
|
|
key: apiKey,
|
|
};
|
|
|
|
if (track.album?.title) {
|
|
scrobbleData.album = track.album.title;
|
|
}
|
|
|
|
if (track.duration) {
|
|
scrobbleData.duration = Math.floor(track.duration);
|
|
}
|
|
|
|
if (track.trackNumber) {
|
|
scrobbleData.track_number = track.trackNumber;
|
|
}
|
|
|
|
if (timestamp) {
|
|
scrobbleData.time = timestamp;
|
|
}
|
|
|
|
try {
|
|
// Try the newer Maloja API format first (mlj_1)
|
|
let response = await fetch(`${apiUrl}/apis/mlj_1/newscrobble`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams(scrobbleData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// Fallback to older API format
|
|
response = await fetch(`${apiUrl}/apis/native/newscrobble`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: new URLSearchParams(scrobbleData),
|
|
});
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
throw new Error(`Maloja API Error ${response.status}: ${text}`);
|
|
}
|
|
|
|
console.log(`[Maloja] Submitted scrobble: ${title} by ${artist}`);
|
|
} catch (error) {
|
|
console.error('[Maloja] Submission failed:', error);
|
|
}
|
|
}
|
|
|
|
async updateNowPlaying(track) {
|
|
if (!this.isEnabled()) return;
|
|
|
|
this.currentTrack = track;
|
|
// 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
|
|
|
|
const scrobblePercentage = lastFMStorage.getScrobblePercentage() / 100;
|
|
this.scrobbleThreshold = Math.min(track.duration * scrobblePercentage, 240);
|
|
this.scheduleScrobble(this.scrobbleThreshold * 1000);
|
|
}
|
|
|
|
scheduleScrobble(delay) {
|
|
this.clearScrobbleTimer();
|
|
this.scrobbleTimer = setTimeout(async () => {
|
|
await this.scrobbleCurrentTrack();
|
|
}, delay);
|
|
}
|
|
|
|
clearScrobbleTimer() {
|
|
if (this.scrobbleTimer) {
|
|
clearTimeout(this.scrobbleTimer);
|
|
this.scrobbleTimer = null;
|
|
}
|
|
}
|
|
|
|
async scrobbleCurrentTrack() {
|
|
if (!this.isEnabled() || !this.currentTrack || this.hasScrobbled) return;
|
|
|
|
this.isScrobbling = true;
|
|
|
|
try {
|
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
await this.submitScrobble(this.currentTrack, timestamp);
|
|
this.hasScrobbled = true;
|
|
} finally {
|
|
this.isScrobbling = false;
|
|
}
|
|
}
|
|
|
|
async onTrackChange(track) {
|
|
await this.updateNowPlaying(track);
|
|
}
|
|
|
|
onPlaybackStop() {
|
|
this.clearScrobbleTimer();
|
|
}
|
|
|
|
disconnect() {
|
|
this.clearScrobbleTimer();
|
|
this.currentTrack = null;
|
|
}
|
|
}
|