First commit
This commit is contained in:
95
scrobbler.js
Normal file
95
scrobbler.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/* @licstart The following is the entire license notice for the JavaScript code in this file.
|
||||
Copyleft (🄯) 2025 James Osborne
|
||||
|
||||
This file is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
Affero General Public License as published by the Free Software Foundation, either version 3 of
|
||||
the License, or (at your option) any later version.
|
||||
|
||||
This file is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along with this program.
|
||||
If not, see <https://www.gnu.org/licenses/>. @licend The above is the entire license notice for
|
||||
the JavaScript code in this file. */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const CONFIG = {
|
||||
user: 'incentive.jb',
|
||||
smallLines: 7,
|
||||
lbBase: 'https://api.listenbrainz.org/1/',
|
||||
timeoutMs: 8000
|
||||
};
|
||||
const TOTAL = 1 + CONFIG.smallLines;
|
||||
|
||||
const widget = document.getElementById('scrobble-widget');
|
||||
if (!widget) return;
|
||||
|
||||
const urlJoin = (a, b) => a.replace(/\/+$/, '') + '/' + b.replace(/^\/+/, '');
|
||||
|
||||
const HTML_ESC = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
||||
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => HTML_ESC[c]);
|
||||
|
||||
async function fetchListens() {
|
||||
const url = urlJoin(
|
||||
CONFIG.lbBase,
|
||||
`user/${encodeURIComponent(CONFIG.user)}/listens?count=${TOTAL}`
|
||||
);
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), CONFIG.timeoutMs);
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { cache: 'no-store', signal: ctrl.signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const j = await res.json();
|
||||
return (j && (j.payload?.listens || j.listens)) || [];
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
fetchListens()
|
||||
.then((listens) => {
|
||||
if (!listens.length) {
|
||||
widget.textContent = 'No recent scrobbles.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize to { artist, title } safely
|
||||
const rows = listens.map((l) => {
|
||||
const tm = l.track_metadata || l.track || {};
|
||||
const artist =
|
||||
tm.artist_name ?? tm.artist?.name ?? tm.artist ?? 'Unknown Artist';
|
||||
const title =
|
||||
tm.track_name ?? tm.title ?? tm.track?.title ?? tm.track ?? 'Unknown Track';
|
||||
return { artist: esc(artist), title: esc(title) };
|
||||
});
|
||||
|
||||
const top = `
|
||||
<div class="scrobble-top scrobble-row">
|
||||
<span class="artist">${rows[0].artist}</span>
|
||||
<span class="title"><em>${rows[0].title}</em></span>
|
||||
</div>`;
|
||||
|
||||
const more = rows
|
||||
.slice(1, 1 + CONFIG.smallLines)
|
||||
.map(
|
||||
(r) => `
|
||||
<li class="scrobble-row">
|
||||
<span class="artist">${r.artist}</span>
|
||||
<span class="title"><em>${r.title}</em></span>
|
||||
</li>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
widget.innerHTML = top + `<ul class="scrobble-more">${more}</ul>`;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('Scrobbles error:', err);
|
||||
widget.textContent = 'Scrobbles unavailable.';
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user