more scrobbling sources

This commit is contained in:
Eduard Prigoana 2026-02-05 10:25:04 +00:00
parent 776c63ca3e
commit 2a572aec42
9 changed files with 830 additions and 23 deletions

View file

@ -2315,6 +2315,19 @@
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">
@ -2354,6 +2367,83 @@
</label>
</div>
</div>
<div class="settings-group">
<div class="setting-item">
<div class="info">
<span class="label">Maloja Scrobbling</span>
<span class="description">Submit listens to a self-hosted Maloja server</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="maloja-enabled-toggle" />
<span class="slider"></span>
</label>
</div>
<div class="setting-item" id="maloja-token-setting" style="display: none">
<div class="info">
<span class="label">API Key</span>
<span class="description">Found in your Maloja settings</span>
</div>
<input
type="password"
id="maloja-token-input"
placeholder="Enter API Key"
class="template-input"
style="width: 250px"
/>
</div>
<div class="setting-item" id="maloja-custom-url-setting" style="display: none">
<div class="info">
<span class="label">Maloja Server URL</span>
<span class="description">Your Maloja instance URL</span>
</div>
<input
type="url"
id="maloja-custom-url-input"
placeholder="https://maloja.example.com"
class="template-input"
style="width: 250px"
/>
</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">

View file

@ -10,7 +10,7 @@ import {
SVG_BIN,
getTrackArtists,
} from './utils.js';
import { lastFMStorage, waveformSettings } from './storage.js';
import { lastFMStorage, libreFmSettings, waveformSettings } from './storage.js';
import { showNotification, downloadTrackWithMetadata, downloadAlbumAsZip, downloadPlaylistAsZip } from './downloads.js';
import { downloadQualitySettings } from './storage.js';
import { updateTabTitle, navigate } from './router.js';
@ -858,8 +858,13 @@ export async function handleTrackAction(
const added = await db.toggleFavorite(type, item);
syncManager.syncLibraryItem(type, item, added);
if (added && type === 'track' && scrobbler && lastFMStorage.isEnabled() && lastFMStorage.shouldLoveOnLike()) {
scrobbler.loveTrack(item);
if (added && type === 'track' && scrobbler) {
if (lastFMStorage.isEnabled() && lastFMStorage.shouldLoveOnLike()) {
scrobbler.loveTrack(item);
}
if (libreFmSettings.isEnabled() && libreFmSettings.shouldLoveOnLike()) {
scrobbler.loveTrack(item);
}
}
// Update all instances of this item's like button on the page

289
js/librefm.js Normal file
View file

@ -0,0 +1,289 @@
import { libreFmSettings } 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.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('https://cdn.jsdelivr.net/npm/md5@2.3.0/+esm');
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;
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);
this.scrobbleThreshold = Math.min(track.duration / 2, 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(() => {
this.scrobbleCurrentTrack();
}, delay);
}
clearScrobbleTimer() {
if (this.scrobbleTimer) {
clearTimeout(this.scrobbleTimer);
this.scrobbleTimer = null;
}
}
async scrobbleCurrentTrack() {
if (!this.isAuthenticated() || !this.currentTrack || this.hasScrobbled) return;
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);
}
}
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);
}
}
onTrackChange(track) {
if (!this.isAuthenticated()) return;
this.updateNowPlaying(track);
}
onPlaybackStop() {
this.clearScrobbleTimer();
}
disconnect() {
this.clearSession();
this.clearScrobbleTimer();
this.currentTrack = null;
}
}

View file

@ -2,13 +2,18 @@ import { listenBrainzSettings } from './storage.js';
export class ListenBrainzScrobbler {
constructor() {
this.API_URL = 'https://api.listenbrainz.org/1';
this.DEFAULT_API_URL = 'https://api.listenbrainz.org/1';
this.currentTrack = null;
this.scrobbleTimer = null;
this.scrobbleThreshold = 0;
this.hasScrobbled = false;
}
getApiUrl() {
const customUrl = listenBrainzSettings.getCustomUrl();
return customUrl || this.DEFAULT_API_URL;
}
isEnabled() {
return listenBrainzSettings.isEnabled() && !!listenBrainzSettings.getToken();
}
@ -89,7 +94,8 @@ export class ListenBrainzScrobbler {
};
try {
const response = await fetch(`${this.API_URL}/submit-listens`, {
const apiUrl = this.getApiUrl();
const response = await fetch(`${apiUrl}/submit-listens`, {
method: 'POST',
headers: {
Authorization: `Token ${this.getToken()}`,

163
js/maloja.js Normal file
View file

@ -0,0 +1,163 @@
import { malojaSettings } from './storage.js';
export class MalojaScrobbler {
constructor() {
this.currentTrack = null;
this.scrobbleTimer = null;
this.scrobbleThreshold = 0;
this.hasScrobbled = 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;
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);
this.scheduleScrobble(this.scrobbleThreshold * 1000);
}
scheduleScrobble(delay) {
this.clearScrobbleTimer();
this.scrobbleTimer = setTimeout(() => {
this.scrobbleCurrentTrack();
}, delay);
}
clearScrobbleTimer() {
if (this.scrobbleTimer) {
clearTimeout(this.scrobbleTimer);
this.scrobbleTimer = null;
}
}
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;
}
onTrackChange(track) {
this.updateNowPlaying(track);
}
onPlaybackStop() {
this.clearScrobbleTimer();
}
disconnect() {
this.clearScrobbleTimer();
this.currentTrack = null;
}
}

View file

@ -1,10 +1,14 @@
import { LastFMScrobbler } from './lastfm.js';
import { ListenBrainzScrobbler } from './listenbrainz.js';
import { MalojaScrobbler } from './maloja.js';
import { LibreFmScrobbler } from './librefm.js';
export class MultiScrobbler {
constructor() {
this.lastfm = new LastFMScrobbler();
this.listenbrainz = new ListenBrainzScrobbler();
this.maloja = new MalojaScrobbler();
this.librefm = new LibreFmScrobbler();
}
// Proxy method for Last.fm specific usage (auth flow)
@ -12,30 +16,47 @@ export class MultiScrobbler {
return this.lastfm;
}
// Proxy method for Libre.fm specific usage (auth flow)
getLibreFm() {
return this.librefm;
}
isAuthenticated() {
// Return true if any service is configured, so events.js will proceed to call updateNowPlaying
// Individual services check their own enabled/auth state internally
return this.lastfm.isAuthenticated() || this.listenbrainz.isEnabled();
return (
this.lastfm.isAuthenticated() ||
this.listenbrainz.isEnabled() ||
this.maloja.isEnabled() ||
this.librefm.isAuthenticated()
);
}
updateNowPlaying(track) {
this.lastfm.updateNowPlaying(track);
this.listenbrainz.updateNowPlaying(track);
this.maloja.updateNowPlaying(track);
this.librefm.updateNowPlaying(track);
}
onTrackChange(track) {
this.lastfm.onTrackChange(track);
this.listenbrainz.onTrackChange(track);
this.maloja.onTrackChange(track);
this.librefm.onTrackChange(track);
}
onPlaybackStop() {
this.lastfm.onPlaybackStop();
this.listenbrainz.onPlaybackStop();
this.maloja.onPlaybackStop();
this.librefm.onPlaybackStop();
}
// Love/Like is currently Last.fm specific in the UI, but we can extend later
// Love/Like tracks on all services that support it
async loveTrack(track) {
await this.lastfm.loveTrack(track);
// ListenBrainz feedback could be added here
await this.librefm.loveTrack(track);
// ListenBrainz and Maloja feedback could be added here when supported
}
}

View file

@ -18,6 +18,8 @@ import {
playlistSettings,
equalizerSettings,
listenBrainzSettings,
malojaSettings,
libreFmSettings,
homePageSettings,
sidebarSectionSettings,
} from './storage.js';
@ -223,13 +225,17 @@ export function initializeSettings(scrobbler, player, api, ui) {
// ========================================
const lbToggle = document.getElementById('listenbrainz-enabled-toggle');
const lbTokenSetting = document.getElementById('listenbrainz-token-setting');
const lbCustomUrlSetting = document.getElementById('listenbrainz-custom-url-setting');
const lbTokenInput = document.getElementById('listenbrainz-token-input');
const lbCustomUrlInput = document.getElementById('listenbrainz-custom-url-input');
const updateListenBrainzUI = () => {
const isEnabled = listenBrainzSettings.isEnabled();
if (lbToggle) lbToggle.checked = isEnabled;
if (lbTokenSetting) lbTokenSetting.style.display = isEnabled ? 'flex' : 'none';
if (lbCustomUrlSetting) lbCustomUrlSetting.style.display = isEnabled ? 'flex' : 'none';
if (lbTokenInput) lbTokenInput.value = listenBrainzSettings.getToken();
if (lbCustomUrlInput) lbCustomUrlInput.value = listenBrainzSettings.getCustomUrl();
};
updateListenBrainzUI();
@ -248,6 +254,164 @@ export function initializeSettings(scrobbler, player, api, ui) {
});
}
if (lbCustomUrlInput) {
lbCustomUrlInput.addEventListener('change', (e) => {
listenBrainzSettings.setCustomUrl(e.target.value.trim());
});
}
// ========================================
// Maloja Settings
// ========================================
const malojaToggle = document.getElementById('maloja-enabled-toggle');
const malojaTokenSetting = document.getElementById('maloja-token-setting');
const malojaCustomUrlSetting = document.getElementById('maloja-custom-url-setting');
const malojaTokenInput = document.getElementById('maloja-token-input');
const malojaCustomUrlInput = document.getElementById('maloja-custom-url-input');
const updateMalojaUI = () => {
const isEnabled = malojaSettings.isEnabled();
if (malojaToggle) malojaToggle.checked = isEnabled;
if (malojaTokenSetting) malojaTokenSetting.style.display = isEnabled ? 'flex' : 'none';
if (malojaCustomUrlSetting) malojaCustomUrlSetting.style.display = isEnabled ? 'flex' : 'none';
if (malojaTokenInput) malojaTokenInput.value = malojaSettings.getToken();
if (malojaCustomUrlInput) malojaCustomUrlInput.value = malojaSettings.getCustomUrl();
};
updateMalojaUI();
if (malojaToggle) {
malojaToggle.addEventListener('change', (e) => {
const enabled = e.target.checked;
malojaSettings.setEnabled(enabled);
updateMalojaUI();
});
}
if (malojaTokenInput) {
malojaTokenInput.addEventListener('change', (e) => {
malojaSettings.setToken(e.target.value.trim());
});
}
if (malojaCustomUrlInput) {
malojaCustomUrlInput.addEventListener('change', (e) => {
malojaSettings.setCustomUrl(e.target.value.trim());
});
}
// ========================================
// Libre.fm Settings
// ========================================
const librefmConnectBtn = document.getElementById('librefm-connect-btn');
const librefmStatus = document.getElementById('librefm-status');
const librefmToggle = document.getElementById('librefm-toggle');
const librefmToggleSetting = document.getElementById('librefm-toggle-setting');
const librefmLoveToggle = document.getElementById('librefm-love-toggle');
const librefmLoveSetting = document.getElementById('librefm-love-setting');
function updateLibreFmUI() {
if (scrobbler.librefm.isAuthenticated()) {
librefmStatus.textContent = `Connected as ${scrobbler.librefm.username}`;
librefmConnectBtn.textContent = 'Disconnect';
librefmConnectBtn.classList.add('danger');
librefmToggleSetting.style.display = 'flex';
librefmLoveSetting.style.display = 'flex';
librefmToggle.checked = libreFmSettings.isEnabled();
librefmLoveToggle.checked = libreFmSettings.shouldLoveOnLike();
} else {
librefmStatus.textContent = 'Connect your Libre.fm account to scrobble tracks';
librefmConnectBtn.textContent = 'Connect Libre.fm';
librefmConnectBtn.classList.remove('danger');
librefmToggleSetting.style.display = 'none';
librefmLoveSetting.style.display = 'none';
}
}
if (librefmConnectBtn) {
updateLibreFmUI();
librefmConnectBtn.addEventListener('click', async () => {
if (scrobbler.librefm.isAuthenticated()) {
if (confirm('Disconnect from Libre.fm?')) {
scrobbler.librefm.disconnect();
updateLibreFmUI();
}
return;
}
const authWindow = window.open('', '_blank');
librefmConnectBtn.disabled = true;
librefmConnectBtn.textContent = 'Opening Libre.fm...';
try {
const { token, url } = await scrobbler.librefm.getAuthUrl();
if (authWindow) {
authWindow.location.href = url;
} else {
alert('Popup blocked! Please allow popups.');
librefmConnectBtn.textContent = 'Connect Libre.fm';
librefmConnectBtn.disabled = false;
return;
}
librefmConnectBtn.textContent = 'Waiting for authorization...';
let attempts = 0;
const maxAttempts = 30;
const checkAuth = setInterval(async () => {
attempts++;
if (attempts > maxAttempts) {
clearInterval(checkAuth);
librefmConnectBtn.textContent = 'Connect Libre.fm';
librefmConnectBtn.disabled = false;
if (authWindow && !authWindow.closed) authWindow.close();
alert('Authorization timed out. Please try again.');
return;
}
try {
const result = await scrobbler.librefm.completeAuthentication(token);
if (result.success) {
clearInterval(checkAuth);
if (authWindow && !authWindow.closed) authWindow.close();
updateLibreFmUI();
librefmConnectBtn.disabled = false;
libreFmSettings.setEnabled(true);
librefmToggle.checked = true;
alert(`Successfully connected to Libre.fm as ${result.username}!`);
}
} catch {
// Still waiting
}
}, 2000);
} catch (error) {
console.error('Libre.fm connection failed:', error);
alert('Failed to connect to Libre.fm: ' + error.message);
librefmConnectBtn.textContent = 'Connect Libre.fm';
librefmConnectBtn.disabled = false;
if (authWindow && !authWindow.closed) authWindow.close();
}
});
// Libre.fm Toggles
if (librefmToggle) {
librefmToggle.addEventListener('change', (e) => {
libreFmSettings.setEnabled(e.target.checked);
});
}
if (librefmLoveToggle) {
librefmLoveToggle.addEventListener('change', (e) => {
libreFmSettings.setLoveOnLike(e.target.checked);
});
}
}
// Theme picker
const themePicker = document.getElementById('theme-picker');
const currentTheme = themeManager.getTheme();

View file

@ -902,6 +902,7 @@ export const queueManager = {
export const listenBrainzSettings = {
ENABLED_KEY: 'listenbrainz-enabled',
TOKEN_KEY: 'listenbrainz-token',
CUSTOM_URL_KEY: 'listenbrainz-custom-url',
isEnabled() {
try {
@ -926,6 +927,89 @@ export const listenBrainzSettings = {
setToken(token) {
localStorage.setItem(this.TOKEN_KEY, token);
},
getCustomUrl() {
try {
return localStorage.getItem(this.CUSTOM_URL_KEY) || '';
} catch {
return '';
}
},
setCustomUrl(url) {
localStorage.setItem(this.CUSTOM_URL_KEY, url);
},
};
export const malojaSettings = {
ENABLED_KEY: 'maloja-enabled',
TOKEN_KEY: 'maloja-token',
CUSTOM_URL_KEY: 'maloja-custom-url',
isEnabled() {
try {
return localStorage.getItem(this.ENABLED_KEY) === 'true';
} catch {
return false;
}
},
setEnabled(enabled) {
localStorage.setItem(this.ENABLED_KEY, enabled ? 'true' : 'false');
},
getToken() {
try {
return localStorage.getItem(this.TOKEN_KEY) || '';
} catch {
return '';
}
},
setToken(token) {
localStorage.setItem(this.TOKEN_KEY, token);
},
getCustomUrl() {
try {
return localStorage.getItem(this.CUSTOM_URL_KEY) || '';
} catch {
return '';
}
},
setCustomUrl(url) {
localStorage.setItem(this.CUSTOM_URL_KEY, url);
},
};
export const libreFmSettings = {
ENABLED_KEY: 'librefm-enabled',
LOVE_ON_LIKE_KEY: 'librefm-love-on-like',
isEnabled() {
try {
return localStorage.getItem(this.ENABLED_KEY) === 'true';
} catch {
return false;
}
},
setEnabled(enabled) {
localStorage.setItem(this.ENABLED_KEY, enabled ? 'true' : 'false');
},
shouldLoveOnLike() {
try {
return localStorage.getItem(this.LOVE_ON_LIKE_KEY) === 'true';
} catch {
return false;
}
},
setLoveOnLike(enabled) {
localStorage.setItem(this.LOVE_ON_LIKE_KEY, enabled ? 'true' : 'false');
},
};
export const homePageSettings = {

15
package-lock.json generated
View file

@ -74,7 +74,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@ -1604,7 +1603,6 @@
"integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
@ -1646,7 +1644,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@ -1690,7 +1687,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@ -3142,7 +3138,6 @@
"resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz",
"integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=20"
},
@ -3191,7 +3186,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3215,7 +3209,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -3503,7 +3496,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -4288,7 +4280,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -6645,7 +6636,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -6729,7 +6719,6 @@
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -7679,7 +7668,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-syntax-patches-for-csstree": "^1.0.19",
@ -8094,7 +8082,6 @@
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@ -8419,7 +8406,6 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -8807,7 +8793,6 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},