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">
|
<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="margin-bottom: 0.5rem; font-size: 0.9rem">Import from JSPF</p>
|
||||||
<p style="font-size: 0.8rem; margin: 0">
|
<p style="font-size: 0.8rem; margin: 0">
|
||||||
JSPF (JSON Shareable Playlist Format) is supported by const replayGainPreamp = document.getElementById('replay-gain-preamp');
|
JSPF (JSON Shareable Playlist Format) is supported by const replayGainPreamp =
|
||||||
if (replayGainPreamp) {
|
document.getElementById('replay-gain-preamp'); if (replayGainPreamp) {
|
||||||
replayGainPreamp.value = replayGainSettings.getPreamp();
|
replayGainPreamp.value = replayGainSettings.getPreamp();
|
||||||
replayGainPreamp.addEventListener('change', (e) => {
|
replayGainPreamp.addEventListener('change', (e) => { const val = parseFloat(e.target.value);
|
||||||
const val = parseFloat(e.target.value);
|
replayGainSettings.setPreamp(isNaN(val) ? 3 : val); player.applyReplayGain(); }); } Import
|
||||||
replayGainSettings.setPreamp(isNaN(val) ? 3 : val);
|
playlists with rich metadata including MusicBrainz identifiers.
|
||||||
player.applyReplayGain();
|
|
||||||
});
|
|
||||||
} Import playlists with rich metadata including MusicBrainz identifiers.
|
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<input
|
<input
|
||||||
|
|
@ -2350,7 +2347,23 @@
|
||||||
</section>
|
</section>
|
||||||
<section class="content-section" id="artist-section-in-library" style="display: none">
|
<section class="content-section" id="artist-section-in-library" style="display: none">
|
||||||
<h2 class="section-title">
|
<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
|
In Your Library
|
||||||
<use
|
<use
|
||||||
svg="!lucide/chevron-right.svg"
|
svg="!lucide/chevron-right.svg"
|
||||||
|
|
@ -3592,11 +3605,23 @@
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
id="listenbrainz-custom-url-input"
|
id="listenbrainz-custom-url-input"
|
||||||
placeholder="https://api.listenbrainz.org/1"
|
placeholder="https://api.listenbrainz.org"
|
||||||
class="template-input"
|
class="template-input"
|
||||||
style="width: 250px"
|
style="width: 250px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
getShareUrl,
|
getShareUrl,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
} from './utils.js';
|
} 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 { showNotification, downloadTrackWithMetadata, downloadAlbumAsZip, downloadPlaylistAsZip } from './downloads.js';
|
||||||
import { downloadQualitySettings } from './storage.js';
|
import { downloadQualitySettings } from './storage.js';
|
||||||
import { updateTabTitle, navigate } from './router.js';
|
import { updateTabTitle, navigate } from './router.js';
|
||||||
|
|
@ -1050,6 +1050,9 @@ export async function handleTrackAction(
|
||||||
if (libreFmSettings.isEnabled() && libreFmSettings.shouldLoveOnLike()) {
|
if (libreFmSettings.isEnabled() && libreFmSettings.shouldLoveOnLike()) {
|
||||||
scrobbler.loveTrack(item);
|
scrobbler.loveTrack(item);
|
||||||
}
|
}
|
||||||
|
if (listenBrainzSettings.isEnabled() && listenBrainzSettings.shouldLoveOnLike()) {
|
||||||
|
scrobbler.loveTrack(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update all instances of this item's like button on the page
|
// 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 {
|
export class ListenBrainzScrobbler {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.DEFAULT_API_URL = 'https://api.listenbrainz.org/1';
|
this.DEFAULT_API_URL = 'https://api.listenbrainz.org';
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
this.scrobbleTimer = null;
|
this.scrobbleTimer = null;
|
||||||
this.scrobbleThreshold = 0;
|
this.scrobbleThreshold = 0;
|
||||||
|
|
@ -12,7 +12,8 @@ export class ListenBrainzScrobbler {
|
||||||
|
|
||||||
getApiUrl() {
|
getApiUrl() {
|
||||||
const customUrl = listenBrainzSettings.getCustomUrl();
|
const customUrl = listenBrainzSettings.getCustomUrl();
|
||||||
return customUrl || this.DEFAULT_API_URL;
|
const base = customUrl || this.DEFAULT_API_URL;
|
||||||
|
return base.replace(/\/1\/?$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
|
|
@ -26,7 +27,6 @@ export class ListenBrainzScrobbler {
|
||||||
_getMetadata(track) {
|
_getMetadata(track) {
|
||||||
if (!track) return null;
|
if (!track) return null;
|
||||||
|
|
||||||
// Get the primary artist name
|
|
||||||
let artistName = 'Unknown Artist';
|
let artistName = 'Unknown Artist';
|
||||||
|
|
||||||
if (track.artist?.name) {
|
if (track.artist?.name) {
|
||||||
|
|
@ -38,10 +38,9 @@ export class ListenBrainzScrobbler {
|
||||||
artistName = typeof first === 'string' ? first : first.name || 'Unknown Artist';
|
artistName = typeof first === 'string' ? first : first.name || 'Unknown Artist';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean artist name
|
|
||||||
if (typeof artistName === 'string') {
|
if (typeof artistName === 'string') {
|
||||||
artistName = artistName
|
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();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +72,54 @@ export class ListenBrainzScrobbler {
|
||||||
return payload;
|
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) {
|
async submitListen(listenType, track, timestamp = null) {
|
||||||
if (!this.isEnabled()) return;
|
if (!this.isEnabled()) return;
|
||||||
|
|
||||||
|
|
@ -96,7 +143,7 @@ export class ListenBrainzScrobbler {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiUrl = this.getApiUrl();
|
const apiUrl = this.getApiUrl();
|
||||||
const response = await fetch(`${apiUrl}/submit-listens`, {
|
const response = await fetch(`${apiUrl}/1/submit-listens`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Token ${this.getToken()}`,
|
Authorization: `Token ${this.getToken()}`,
|
||||||
|
|
@ -106,7 +153,6 @@ export class ListenBrainzScrobbler {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// ListenBrainz doesn't always return JSON on error
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
throw new Error(`ListenBrainz API Error ${response.status}: ${text}`);
|
throw new Error(`ListenBrainz API Error ${response.status}: ${text}`);
|
||||||
}
|
}
|
||||||
|
|
@ -121,8 +167,6 @@ export class ListenBrainzScrobbler {
|
||||||
if (!this.isEnabled()) return;
|
if (!this.isEnabled()) return;
|
||||||
|
|
||||||
this.currentTrack = track;
|
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) {
|
if (!this.isScrobbling) {
|
||||||
this.hasScrobbled = false;
|
this.hasScrobbled = false;
|
||||||
}
|
}
|
||||||
|
|
@ -175,4 +219,38 @@ export class ListenBrainzScrobbler {
|
||||||
this.clearScrobbleTimer();
|
this.clearScrobbleTimer();
|
||||||
this.currentTrack = null;
|
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) {
|
async loveTrack(track) {
|
||||||
await this.lastfm.loveTrack(track);
|
await this.lastfm.loveTrack(track);
|
||||||
await this.librefm.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 lbToggle = document.getElementById('listenbrainz-enabled-toggle');
|
||||||
const lbTokenSetting = document.getElementById('listenbrainz-token-setting');
|
const lbTokenSetting = document.getElementById('listenbrainz-token-setting');
|
||||||
const lbCustomUrlSetting = document.getElementById('listenbrainz-custom-url-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 lbTokenInput = document.getElementById('listenbrainz-token-input');
|
||||||
const lbCustomUrlInput = document.getElementById('listenbrainz-custom-url-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 (lbToggle) lbToggle.checked = isEnabled;
|
||||||
if (lbTokenSetting) lbTokenSetting.style.display = isEnabled ? 'flex' : 'none';
|
if (lbTokenSetting) lbTokenSetting.style.display = isEnabled ? 'flex' : 'none';
|
||||||
if (lbCustomUrlSetting) lbCustomUrlSetting.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 (lbTokenInput) lbTokenInput.value = listenBrainzSettings.getToken();
|
||||||
if (lbCustomUrlInput) lbCustomUrlInput.value = listenBrainzSettings.getCustomUrl();
|
if (lbCustomUrlInput) lbCustomUrlInput.value = listenBrainzSettings.getCustomUrl();
|
||||||
|
if (lbLoveToggle) lbLoveToggle.checked = listenBrainzSettings.shouldLoveOnLike();
|
||||||
};
|
};
|
||||||
|
|
||||||
updateListenBrainzUI();
|
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
|
// Maloja Settings
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export const apiSettings = {
|
||||||
{ url: 'https://katze.qqdl.site', version: '2.2' },
|
{ url: 'https://katze.qqdl.site', version: '2.2' },
|
||||||
{ url: 'https://hund.qqdl.site', version: '2.2' },
|
{ url: 'https://hund.qqdl.site', version: '2.2' },
|
||||||
{ url: 'https://wolf.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;
|
this.instancesLoaded = true;
|
||||||
|
|
@ -1592,6 +1592,7 @@ export const listenBrainzSettings = {
|
||||||
ENABLED_KEY: 'listenbrainz-enabled',
|
ENABLED_KEY: 'listenbrainz-enabled',
|
||||||
TOKEN_KEY: 'listenbrainz-token',
|
TOKEN_KEY: 'listenbrainz-token',
|
||||||
CUSTOM_URL_KEY: 'listenbrainz-custom-url',
|
CUSTOM_URL_KEY: 'listenbrainz-custom-url',
|
||||||
|
LOVE_ON_LIKE_KEY: 'listenbrainz-love-on-like',
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1628,6 +1629,18 @@ export const listenBrainzSettings = {
|
||||||
setCustomUrl(url) {
|
setCustomUrl(url) {
|
||||||
localStorage.setItem(this.CUSTOM_URL_KEY, 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 = {
|
export const malojaSettings = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue