listenbrainz love on like
This commit is contained in:
parent
5d0d375242
commit
7bcb9e1fb5
6 changed files with 153 additions and 23 deletions
47
index.html
47
index.html
|
|
@ -476,15 +476,12 @@
|
|||
<div id="jspf-import-panel" class="import-panel" style="display: none">
|
||||
<p style="margin-bottom: 0.5rem; font-size: 0.9rem">Import from JSPF</p>
|
||||
<p style="font-size: 0.8rem; margin: 0">
|
||||
JSPF (JSON Shareable Playlist Format) is supported by const replayGainPreamp = document.getElementById('replay-gain-preamp');
|
||||
if (replayGainPreamp) {
|
||||
replayGainPreamp.value = replayGainSettings.getPreamp();
|
||||
replayGainPreamp.addEventListener('change', (e) => {
|
||||
const val = parseFloat(e.target.value);
|
||||
replayGainSettings.setPreamp(isNaN(val) ? 3 : val);
|
||||
player.applyReplayGain();
|
||||
});
|
||||
} Import playlists with rich metadata including MusicBrainz identifiers.
|
||||
JSPF (JSON Shareable Playlist Format) is supported by const replayGainPreamp =
|
||||
document.getElementById('replay-gain-preamp'); if (replayGainPreamp) {
|
||||
replayGainPreamp.value = replayGainSettings.getPreamp();
|
||||
replayGainPreamp.addEventListener('change', (e) => { const val = parseFloat(e.target.value);
|
||||
replayGainSettings.setPreamp(isNaN(val) ? 3 : val); player.applyReplayGain(); }); } Import
|
||||
playlists with rich metadata including MusicBrainz identifiers.
|
||||
</p>
|
||||
<br />
|
||||
<input
|
||||
|
|
@ -2350,7 +2347,23 @@
|
|||
</section>
|
||||
<section class="content-section" id="artist-section-in-library" style="display: none">
|
||||
<h2 class="section-title">
|
||||
<button id="in-library-toggle" aria-expanded="false" aria-controls="artist-detail-in-library" style="background: none; border: none; color: inherit; font: inherit; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; padding: 0; user-select: none;">
|
||||
<button
|
||||
id="in-library-toggle"
|
||||
aria-expanded="false"
|
||||
aria-controls="artist-detail-in-library"
|
||||
style="
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
"
|
||||
>
|
||||
In Your Library
|
||||
<use
|
||||
svg="!lucide/chevron-right.svg"
|
||||
|
|
@ -3592,11 +3605,23 @@
|
|||
<input
|
||||
type="url"
|
||||
id="listenbrainz-custom-url-input"
|
||||
placeholder="https://api.listenbrainz.org/1"
|
||||
placeholder="https://api.listenbrainz.org"
|
||||
class="template-input"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item" id="listenbrainz-love-setting" style="display: none">
|
||||
<div class="info">
|
||||
<span class="label">Love on Like</span>
|
||||
<span class="description"
|
||||
>Automatically 'love' tracks on ListenBrainz when you like them</span
|
||||
>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="listenbrainz-love-toggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
getShareUrl,
|
||||
escapeHtml,
|
||||
} from './utils.js';
|
||||
import { lastFMStorage, libreFmSettings, waveformSettings } from './storage.js';
|
||||
import { lastFMStorage, libreFmSettings, listenBrainzSettings, waveformSettings } from './storage.js';
|
||||
import { showNotification, downloadTrackWithMetadata, downloadAlbumAsZip, downloadPlaylistAsZip } from './downloads.js';
|
||||
import { downloadQualitySettings } from './storage.js';
|
||||
import { updateTabTitle, navigate } from './router.js';
|
||||
|
|
@ -1050,6 +1050,9 @@ export async function handleTrackAction(
|
|||
if (libreFmSettings.isEnabled() && libreFmSettings.shouldLoveOnLike()) {
|
||||
scrobbler.loveTrack(item);
|
||||
}
|
||||
if (listenBrainzSettings.isEnabled() && listenBrainzSettings.shouldLoveOnLike()) {
|
||||
scrobbler.loveTrack(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Update all instances of this item's like button on the page
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { listenBrainzSettings, lastFMStorage } from './storage.js';
|
|||
|
||||
export class ListenBrainzScrobbler {
|
||||
constructor() {
|
||||
this.DEFAULT_API_URL = 'https://api.listenbrainz.org/1';
|
||||
this.DEFAULT_API_URL = 'https://api.listenbrainz.org';
|
||||
this.currentTrack = null;
|
||||
this.scrobbleTimer = null;
|
||||
this.scrobbleThreshold = 0;
|
||||
|
|
@ -12,7 +12,8 @@ export class ListenBrainzScrobbler {
|
|||
|
||||
getApiUrl() {
|
||||
const customUrl = listenBrainzSettings.getCustomUrl();
|
||||
return customUrl || this.DEFAULT_API_URL;
|
||||
const base = customUrl || this.DEFAULT_API_URL;
|
||||
return base.replace(/\/1\/?$/, '');
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
|
|
@ -26,7 +27,6 @@ export class ListenBrainzScrobbler {
|
|||
_getMetadata(track) {
|
||||
if (!track) return null;
|
||||
|
||||
// Get the primary artist name
|
||||
let artistName = 'Unknown Artist';
|
||||
|
||||
if (track.artist?.name) {
|
||||
|
|
@ -38,10 +38,9 @@ export class ListenBrainzScrobbler {
|
|||
artistName = typeof first === 'string' ? first : first.name || 'Unknown Artist';
|
||||
}
|
||||
|
||||
// Clean artist name
|
||||
if (typeof artistName === 'string') {
|
||||
artistName = artistName
|
||||
.split(/\s*[&]\s*|\s+feat\.?\s+|\s+ft\.?\s+|\s+featuring\s+|\s+with\s+|\s+x\s+/i)[0]
|
||||
.split(/\s*[&]\s*|\s+feat\.?\s*|\s+ft\.?\s*|\s+featuring\s+|\s+with\s+|\s+x\s+/i)[0]
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +72,54 @@ export class ListenBrainzScrobbler {
|
|||
return payload;
|
||||
}
|
||||
|
||||
async _lookupRecordingMbid(track) {
|
||||
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';
|
||||
}
|
||||
artistName = artistName
|
||||
.split(/\s*[&]\s*|\s+feat\.?\s*|\s+ft\.?\s*|\s+featuring\s+|\s+with\s+|\s+x\s+/i)[0]
|
||||
.trim();
|
||||
|
||||
const trackName = track.cleanTitle || track.title;
|
||||
if (!artistName || !trackName) return null;
|
||||
|
||||
try {
|
||||
const apiUrl = this.getApiUrl();
|
||||
const params = new URLSearchParams({
|
||||
recording_name: trackName,
|
||||
artist_name: artistName,
|
||||
});
|
||||
|
||||
const response = await fetch(`${apiUrl}/1/metadata/lookup/?${params}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Token ${this.getToken()}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[ListenBrainz] MBID lookup failed: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data?.recording_mbid) {
|
||||
console.log(`[ListenBrainz] Found MBID: ${data.recording_mbid}`);
|
||||
return data.recording_mbid;
|
||||
}
|
||||
console.warn('[ListenBrainz] No recording_mbid found in lookup response');
|
||||
} catch (error) {
|
||||
console.error('[ListenBrainz] MBID lookup error:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async submitListen(listenType, track, timestamp = null) {
|
||||
if (!this.isEnabled()) return;
|
||||
|
||||
|
|
@ -96,7 +143,7 @@ export class ListenBrainzScrobbler {
|
|||
|
||||
try {
|
||||
const apiUrl = this.getApiUrl();
|
||||
const response = await fetch(`${apiUrl}/submit-listens`, {
|
||||
const response = await fetch(`${apiUrl}/1/submit-listens`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Token ${this.getToken()}`,
|
||||
|
|
@ -106,7 +153,6 @@ export class ListenBrainzScrobbler {
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// ListenBrainz doesn't always return JSON on error
|
||||
const text = await response.text();
|
||||
throw new Error(`ListenBrainz API Error ${response.status}: ${text}`);
|
||||
}
|
||||
|
|
@ -121,8 +167,6 @@ export class ListenBrainzScrobbler {
|
|||
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;
|
||||
}
|
||||
|
|
@ -175,4 +219,38 @@ export class ListenBrainzScrobbler {
|
|||
this.clearScrobbleTimer();
|
||||
this.currentTrack = null;
|
||||
}
|
||||
|
||||
async loveTrack(track) {
|
||||
if (!this.isEnabled()) return;
|
||||
|
||||
try {
|
||||
const apiUrl = this.getApiUrl();
|
||||
const mbid = await this._lookupRecordingMbid(track);
|
||||
|
||||
if (mbid) {
|
||||
const response = await fetch(`${apiUrl}/1/feedback/recording-feedback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Token ${this.getToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recording_mbid: mbid,
|
||||
score: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`ListenBrainz Feedback Error ${response.status}: ${text}`);
|
||||
}
|
||||
|
||||
console.log('[ListenBrainz] Loved track:', track.title);
|
||||
} else {
|
||||
console.warn('[ListenBrainz] Could not find recording MBID for love feedback');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ListenBrainz] Failed to love track:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export class MultiScrobbler {
|
|||
async loveTrack(track) {
|
||||
await this.lastfm.loveTrack(track);
|
||||
await this.librefm.loveTrack(track);
|
||||
// ListenBrainz and Maloja feedback could be added here when supported
|
||||
await this.listenbrainz.loveTrack(track);
|
||||
// Maloja feedback could be added here when supported
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -467,6 +467,8 @@ export async 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 lbLoveSetting = document.getElementById('listenbrainz-love-setting');
|
||||
const lbLoveToggle = document.getElementById('listenbrainz-love-toggle');
|
||||
const lbTokenInput = document.getElementById('listenbrainz-token-input');
|
||||
const lbCustomUrlInput = document.getElementById('listenbrainz-custom-url-input');
|
||||
|
||||
|
|
@ -475,8 +477,10 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
|||
if (lbToggle) lbToggle.checked = isEnabled;
|
||||
if (lbTokenSetting) lbTokenSetting.style.display = isEnabled ? 'flex' : 'none';
|
||||
if (lbCustomUrlSetting) lbCustomUrlSetting.style.display = isEnabled ? 'flex' : 'none';
|
||||
if (lbLoveSetting) lbLoveSetting.style.display = isEnabled ? 'flex' : 'none';
|
||||
if (lbTokenInput) lbTokenInput.value = listenBrainzSettings.getToken();
|
||||
if (lbCustomUrlInput) lbCustomUrlInput.value = listenBrainzSettings.getCustomUrl();
|
||||
if (lbLoveToggle) lbLoveToggle.checked = listenBrainzSettings.shouldLoveOnLike();
|
||||
};
|
||||
|
||||
updateListenBrainzUI();
|
||||
|
|
@ -501,6 +505,12 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
|||
});
|
||||
}
|
||||
|
||||
if (lbLoveToggle) {
|
||||
lbLoveToggle.addEventListener('change', (e) => {
|
||||
listenBrainzSettings.setLoveOnLike(e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Maloja Settings
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export const apiSettings = {
|
|||
{ url: 'https://katze.qqdl.site', version: '2.2' },
|
||||
{ url: 'https://hund.qqdl.site', version: '2.2' },
|
||||
{ url: 'https://wolf.qqdl.site', version: '2.2' },
|
||||
{ url: 'https://hifi.p1nkhamster.xyz/', version: '2.6'},
|
||||
{ url: 'https://hifi.p1nkhamster.xyz/', version: '2.6' },
|
||||
],
|
||||
};
|
||||
this.instancesLoaded = true;
|
||||
|
|
@ -1592,6 +1592,7 @@ export const listenBrainzSettings = {
|
|||
ENABLED_KEY: 'listenbrainz-enabled',
|
||||
TOKEN_KEY: 'listenbrainz-token',
|
||||
CUSTOM_URL_KEY: 'listenbrainz-custom-url',
|
||||
LOVE_ON_LIKE_KEY: 'listenbrainz-love-on-like',
|
||||
|
||||
isEnabled() {
|
||||
try {
|
||||
|
|
@ -1628,6 +1629,18 @@ export const listenBrainzSettings = {
|
|||
setCustomUrl(url) {
|
||||
localStorage.setItem(this.CUSTOM_URL_KEY, url);
|
||||
},
|
||||
|
||||
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 malojaSettings = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue