From ff75d45ff0d4709960bccaaf3adbaf1a23b4b063 Mon Sep 17 00:00:00 2001 From: SansGuidon Date: Mon, 12 May 2025 07:20:34 +0000 Subject: [PATCH] add(userscripts) scrobbler for RTBF live radios tested with LastFM, ListenBrainz and most live radios on websites which rely on similar radio player technology. tested on Brave Browser Version 1.77.97 Chromium: 135.0.7049.84 (Official Build) (arm64), with TamperMonkey 5.3.3 --- tampermonkey/rtbf_scrobbler.js | 213 +++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 tampermonkey/rtbf_scrobbler.js diff --git a/tampermonkey/rtbf_scrobbler.js b/tampermonkey/rtbf_scrobbler.js new file mode 100644 index 0000000..d563508 --- /dev/null +++ b/tampermonkey/rtbf_scrobbler.js @@ -0,0 +1,213 @@ +// ==UserScript== +// @name RTBF Scrobbler v1.4 +// @namespace https://github.com/SansGuidon +// @version 1.4 +// @description Auto-scrobble RTBF → Last.fm & ListenBrainz; top-frame only, main container indicator, omit empty release_name +// @match https://www.rtbf.be/radio/liveradio/* +// @grant GM_xmlhttpRequest +// @grant GM_setValue +// @grant GM_getValue +// @grant GM_registerMenuCommand +// @grant GM_openInTab +// @run-at document-idle +// @connect ws.audioscrobbler.com +// @connect api.listenbrainz.org +// @require https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js +// ==/UserScript== + +(function(){ + if (window.self !== window.top) return; + 'use strict'; + + GM_registerMenuCommand('Scrobbler Config', configure); + GM_registerMenuCommand('Get Last.fm Session Key', generateSession); + + function configure(){ + GM_setValue('lfm_apiKey', prompt('Last.fm API Key', GM_getValue('lfm_apiKey',''))); + GM_setValue('lfm_apiSecret', prompt('Last.fm API Secret', GM_getValue('lfm_apiSecret',''))); + GM_setValue('lfm_sessionKey',prompt('Last.fm Session Key',GM_getValue('lfm_sessionKey',''))); + GM_setValue('lb_token', prompt('ListenBrainz Token', GM_getValue('lb_token',''))); + console.log('[Scrobbler] config saved'); + alert('Config saved.'); + } + + function generateSession(){ + const apiKey = GM_getValue('lfm_apiKey'); + const apiSecret = GM_getValue('lfm_apiSecret'); + if(!apiKey||!apiSecret) return alert('Configure API key & secret first.'); + const cb = 'http://localhost/'; + const url = `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${encodeURIComponent(cb)}`; + typeof GM_openInTab==='function' + ? GM_openInTab(url,{active:true,insert:true}) + : window.open(url,'_blank'); + setTimeout(()=>{ + const token = prompt('Paste the “token” from callback URL:'); + if(!token) return; + const sig = md5(`api_key${apiKey}methodauth.getSessiontoken${token}${apiSecret}`); + const sUrl = `https://ws.audioscrobbler.com/2.0/?method=auth.getSession&api_key=${apiKey}&token=${token}&api_sig=${sig}&format=json`; + GM_xmlhttpRequest({ + method:'GET', url:sUrl, + onload(res){ + try { + const d = JSON.parse(res.responseText); + const sk = d.session && d.session.key; + if(sk){ + GM_setValue('lfm_sessionKey',sk); + console.log('[Scrobbler] session key saved',sk); + return alert('Last.fm session key saved.'); + } + throw new Error(res.responseText); + } catch(e){ + console.error('[Scrobbler] session error',e); + alert('Failed to get session key.'); + } + }, + onerror(err){ + console.error('[Scrobbler] request error',err); + alert('Network error.'); + } + }); + },1000); + } + + // indicator in
if present + const container = document.querySelector('main') || document.body; + if (container !== document.body) container.style.position = 'relative'; + const ind = document.createElement('div'); + Object.assign(ind.style,{ + position: container===document.body ? 'fixed' : 'absolute', + bottom: '10px', + right: '10px', + padding: '6px 10px', + background: '#555', + color: '#fff', + fontFamily: 'monospace', + fontSize: '12px', + whiteSpace: 'pre', + borderRadius:'4px', + zIndex: '9999' + }); + ind.id = 'scrobble-indicator'; + ind.textContent = 'idle'; + container.appendChild(ind); + + function ui(msg, ok = true){ + ind.textContent = msg; + ind.style.background = ok ? '#27ae60' : '#e74c3c'; + } + + function getCfg(){ + const apiKey = GM_getValue('lfm_apiKey'); + const apiSecret = GM_getValue('lfm_apiSecret'); + const sk = GM_getValue('lfm_sessionKey'); + const lbToken = GM_getValue('lb_token'); + if(!apiKey||!apiSecret||!sk||!lbToken){ + console.log('[Scrobbler] not configured'); + ui('not configured', false); + return null; + } + return { apiKey, apiSecret, sk, lbToken }; + } + + async function fetchTrack(retries = 3){ + const cfg = document.body.dataset.config; + if(!cfg) throw new Error('no config'); + const { v2Config:{ id: rpId } } = JSON.parse(cfg); + const url = `https://core-search.radioplayer.cloud/056/qp/v4/events/?rpId=${rpId}`; + for(let i=0;isetTimeout(r,2000*(i+1))); + else throw err; + } + } + } + + function gmRequest(opts){ + return new Promise((resolve,reject)=>{ + GM_xmlhttpRequest(Object.assign({},opts,{ + onload: xhr=>resolve(xhr), + onerror: err=>reject(err) + })); + }); + } + + async function postWithRetry(opts, retries=2){ + for(let i=0;isetTimeout(r,2000*(i+1))); + else ui('scrobble failed', false); + } + } + } + + async function scrobble(track={}, creds){ + const artist = track.artistName; + const name = track.name; + const ts = Math.floor(Date.now()/1000); + + ui(`scrobbling\n${artist} – ${name}`, true); + + // Last.fm + const p = { method:'track.scrobble', api_key:creds.apiKey, artist, track:name, album:'', timestamp:ts, sk:creds.sk }; + const sig = md5(Object.keys(p).sort().map(k=>k+p[k]).join('')+creds.apiSecret); + const body = Object.entries({...p, api_sig:sig, format:'json'}) + .map(([k,v])=>`${k}=${encodeURIComponent(v)}`).join('&'); + await postWithRetry({ + method:'POST', + url:'https://ws.audioscrobbler.com/2.0/', + headers:{'Content-Type':'application/x-www-form-urlencoded'}, + data:body + }); + ui(`Last.fm OK\n…`, true); + + // ListenBrainz: omit empty release_name + const metadata = { artist_name:artist, track_name:name }; + const payload = { + listen_type:'single', + payload:[{ + track_metadata: metadata, + listened_at: ts + }] + }; + await postWithRetry({ + method:'POST', + url:'https://api.listenbrainz.org/1/submit-listens', + headers:{'Content-Type':'application/json','Authorization':'Token '+creds.lbToken}, + data:JSON.stringify(payload) + }); + ui(`Last.fm OK\nLB OK`, true); + } + + function isPlaying(){ + const a = document.querySelector('audio'); + return a && !a.paused; + } + + let last = GM_getValue('lastTrack',''); + setInterval(async ()=>{ + const creds = getCfg(); + if(!creds) return; + let t; + try { t = await fetchTrack(); } + catch { console.log('[Scrobbler] fetch error'); return; } + if(!t.artistName){ console.log('[Scrobbler] no track'); return; } + if(!isPlaying()){ console.log('[Scrobbler] paused'); return; } + const title = `${t.artistName} – ${t.name}`; + if(title === last){ console.log('[Scrobbler] waiting'); return; } + last = title; GM_setValue('lastTrack', last); + try { await scrobble(t, creds); } + catch(e){ console.error('[Scrobbler] scrobble error',e); } + }, 15000); + +})(); \ No newline at end of file