First commit

This commit is contained in:
2025-09-23 01:38:39 -04:00
parent 33cc11c568
commit a1cfe5ca9c
55 changed files with 2252 additions and 195 deletions

95
scrobbler.js Normal file
View 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' };
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.';
});
})();