kv-music/js/librefm.js
2026-04-04 01:37:47 +03:00

299 lines
8.8 KiB
JavaScript

import { libreFmSettings, lastFMStorage } from './storage.js';
export class LibreFmScrobbler {
constructor() {
this.API_KEY = 'monochrome_music_app';
this.API_SECRET = 'monochrome_music_secret_2024';
this.API_URL = 'https://libre.fm/2.0/';
this.sessionKey = null;
this.username = null;
this.currentTrack = null;
this.scrobbleTimer = null;
this.scrobbleThreshold = 0;
this.hasScrobbled = false;
this.isScrobbling = false;
this.loadSession();
}
loadSession() {
try {
const session = localStorage.getItem('librefm-session');
if (session) {
const data = JSON.parse(session);
this.sessionKey = data.key;
this.username = data.name;
}
} catch {
console.error('Failed to load Libre.fm session');
}
}
saveSession(sessionKey, username) {
this.sessionKey = sessionKey;
this.username = username;
localStorage.setItem(
'librefm-session',
JSON.stringify({
key: sessionKey,
name: username,
})
);
}
clearSession() {
this.sessionKey = null;
this.username = null;
localStorage.removeItem('librefm-session');
}
isAuthenticated() {
return !!this.sessionKey && libreFmSettings.isEnabled();
}
_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 generateSignature(params) {
const filteredParams = { ...params };
delete filteredParams.format;
delete filteredParams.callback;
const sortedKeys = Object.keys(filteredParams).sort();
const signatureString = sortedKeys.map((key) => `${key}${filteredParams[key]}`).join('') + this.API_SECRET;
try {
const { default: md5 } = await import('./md5.js');
return md5(signatureString);
} catch {
console.error('MD5 library not available');
throw new Error('MD5 library required for Libre.fm');
}
}
async makeRequest(method, params = {}, requiresAuth = false) {
const requestParams = {
method,
api_key: this.API_KEY,
...params,
};
if (requiresAuth && this.sessionKey) {
requestParams.sk = this.sessionKey;
}
const signature = await this.generateSignature(requestParams);
const formData = new URLSearchParams({
...requestParams,
api_sig: signature,
format: 'json',
});
try {
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 || 'Libre.fm API error');
}
return data;
} catch (error) {
console.error('Libre.fm API request failed:', error);
throw error;
}
}
async getAuthUrl() {
try {
// First, get a token from Libre.fm
const data = await this.makeRequest('auth.getToken');
const token = data.token;
localStorage.setItem('librefm-pending-token', token);
return {
token,
url: `https://libre.fm/api/auth/?api_key=${this.API_KEY}&token=${token}`,
};
} catch (error) {
console.error('Failed to get auth URL:', error);
throw error;
}
}
async completeAuthentication(token) {
try {
const data = await this.makeRequest('auth.getSession', { token });
if (data.session) {
this.saveSession(data.session.key, data.session.name);
localStorage.removeItem('librefm-pending-token');
return {
success: true,
username: data.session.name,
};
}
throw new Error('No session returned');
} catch (error) {
console.error('Authentication failed:', error);
throw error;
}
}
async updateNowPlaying(track) {
if (!this.isAuthenticated()) 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();
try {
const scrobbleTitle = track.cleanTitle || track.title;
const params = {
artist: this._getScrobbleArtist(track),
track: scrobbleTitle,
};
if (track.album?.title) {
params.album = track.album.title;
}
if (track.duration) {
params.duration = Math.floor(track.duration);
}
if (track.trackNumber) {
params.trackNumber = track.trackNumber;
}
await this.makeRequest('track.updateNowPlaying', params, true);
console.log('[Libre.fm] Now playing updated:', scrobbleTitle);
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);
}
}
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.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;
const params = {
artist: this._getScrobbleArtist(this.currentTrack),
track: scrobbleTitle,
timestamp: timestamp,
};
if (this.currentTrack.album?.title) {
params.album = this.currentTrack.album.title;
}
if (this.currentTrack.duration) {
params.duration = Math.floor(this.currentTrack.duration);
}
if (this.currentTrack.trackNumber) {
params.trackNumber = this.currentTrack.trackNumber;
}
await this.makeRequest('track.scrobble', params, true);
this.hasScrobbled = true;
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;
}
}
async loveTrack(track) {
if (!this.isAuthenticated()) return;
try {
const params = {
artist: this._getScrobbleArtist(track),
track: track.title,
};
await this.makeRequest('track.love', params, true);
console.log('[Libre.fm] Loved track:', track.title);
} catch (error) {
console.error('[Libre.fm] Failed to love track:', error);
}
}
async onTrackChange(track) {
if (!this.isAuthenticated()) return;
await this.updateNowPlaying(track);
}
onPlaybackStop() {
this.clearScrobbleTimer();
}
disconnect() {
this.clearSession();
this.clearScrobbleTimer();
this.currentTrack = null;
}
}