// ==UserScript== // @name RTBF Scrobbler v1.5 // @namespace https://gitea.zoemp.be/SansGuidon // @version 1.5 // @description Auto-scrobble RTBF → Last.fm & ListenBrainz; config panel, error display, force-scrobble button // @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'; // Menu commands GM_registerMenuCommand('Scrobbler Config', showConfigPanel); // Config panel function showConfigPanel() { if (document.getElementById('scrobble-config')) return; const overlay = document.createElement('div'); overlay.id = 'scrobble-config'; Object.assign(overlay.style, { position:'fixed', top:0, left:0, right:0, bottom:0, background:'rgba(0,0,0,0.6)', zIndex:10000, display:'flex', alignItems:'center', justifyContent:'center' }); const panel = document.createElement('div'); Object.assign(panel.style, { background:'#fff', padding:'20px', borderRadius:'8px', width:'350px', boxShadow:'0 0 10px rgba(0,0,0,0.5)', fontFamily:'sans-serif', fontSize:'14px', color:'#333' }); panel.innerHTML = `

Scrobbler Settings

Last.fm API App →

ListenBrainz Token →

`; overlay.appendChild(panel); document.body.appendChild(overlay); panel.querySelector('#cf-close').onclick = () => overlay.remove(); panel.querySelector('#cf-save').onclick = () => { GM_setValue('lfm_apiKey', panel.querySelector('#cf-lfm-key').value.trim()); GM_setValue('lfm_apiSecret', panel.querySelector('#cf-lfm-secret').value.trim()); GM_setValue('lfm_sessionKey', panel.querySelector('#cf-lfm-session').value.trim()); GM_setValue('lb_token', panel.querySelector('#cf-lb-token').value.trim()); alert('Settings saved'); overlay.remove(); }; panel.querySelector('#cf-gen-session').onclick = generateSession; panel.querySelector('#cf-force').onclick = async () => { const track = await fetchTrack(); const cfg = getCfg(); if (track.artistName && cfg) await scrobble(track, cfg); }; } // Generate Last.fm session key flow function generateSession(){ const apiKey = GM_getValue('lfm_apiKey','').trim(); const apiSecret = GM_getValue('lfm_apiSecret','').trim(); if (!apiKey || !apiSecret) return ui('⚠ configure LF API first', false); const callback = 'http://localhost/'; const authUrl = `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${encodeURIComponent(callback)}`; (typeof GM_openInTab==='function'? GM_openInTab(authUrl,{active:true}): window.open(authUrl,'_blank')); setTimeout(()=>{ const token = prompt('Paste LAST.FM token from callback URL'); if (!token) return; const sig = md5(`api_key${apiKey}methodauth.getSessiontoken${token}${apiSecret}`); const url = `https://ws.audioscrobbler.com/2.0/?method=auth.getSession&api_key=${apiKey}&token=${token}&api_sig=${sig}&format=json`; GM_xmlhttpRequest({ method:'GET', url, onload(res){ try { const d = JSON.parse(res.responseText); const sk = d.session.key; GM_setValue('lfm_sessionKey', sk); ui('✔ LF session saved', true); } catch(e){ ui('⚠ session invalid', false); console.error(e); } }, onerror(err){ ui('⚠ network error', false); console.error(err); } }); }, 500); } // UI indicator 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-wrap', 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'; } // Config fetch function getCfg(){ const a=GM_getValue('lfm_apiKey','').trim(); const s=GM_getValue('lfm_apiSecret','').trim(); const k=GM_getValue('lfm_sessionKey','').trim(); const l=GM_getValue('lb_token','').trim(); if (!a||!s||!k||!l) { ui('⚠ not configured', false); return null; } return { apiKey:a, apiSecret:s, sk:k, lbToken:l }; } // Fetch current track async function fetchTrack(){ const cfg = JSON.parse(document.body.dataset.config||'{}'); const id = cfg.v2Config && cfg.v2Config.id; if (!id) throw 'no config'; const res = await fetch(`https://core-search.radioplayer.cloud/056/qp/v4/events/?rpId=${id}`); if (!res.ok) throw res.status; const { results:{previous=[], now} } = await res.json(); return [...previous, now].pop()||{}; } // XHR helper function gmReq(o){ return new Promise((rs,rj)=> GM_xmlhttpRequest(Object.assign(o,{ onload:x=>rs(x), onerror:e=>rj(e) })) ); } async function post(url, opts){ const x = await gmReq(Object.assign({url,method:'POST'},opts)); return {status:x.status,text:x.responseText}; } // Scrobble async function scrobble(track, cfg){ const art=track.artistName, nm=track.name, ts=Math.floor(Date.now()/1e3); ui(`▶ ${art} – ${nm}`); // Last.fm const p={method:'track.scrobble',api_key:cfg.apiKey,artist:art,track:nm,album:'',timestamp:ts,sk:cfg.sk}; const sig=md5(Object.keys(p).sort().map(k=>k+p[k]).join('')+cfg.apiSecret); const body=Object.entries({...p,api_sig:sig,format:'json'}).map(([k,v])=>`${k}=${encodeURIComponent(v)}`).join('&'); const lf = await post('https://ws.audioscrobbler.com/2.0/',{headers:{'Content-Type':'application/x-www-form-urlencoded'},data:body}); console.log('[LF]',lf.status,lf.text); if (lf.status===403) { ui('⚠ invalid LF session', false); setTimeout(showConfigPanel,300); return; } ui(`Last.fm: ${lf.status}`); // LB const payload={listen_type:'single',payload:[{track_metadata:{artist_name:art,track_name:nm},listened_at:ts}]}; const lb = await post('https://api.listenbrainz.org/1/submit-listens',{headers:{'Content-Type':'application/json','Authorization':'Token '+cfg.lbToken},data:JSON.stringify(payload)}); console.log('[LB]',lb.status,lb.text); ui(`LF:${lf.status} LB:${lb.status}`, lb.status===200); } // Playback check function isPlaying(){ const a=document.querySelector('audio'); return a&&!a.paused; } let last=GM_getValue('lastTrack',''); setInterval(async ()=>{ const cfg=getCfg(); if(!cfg) return; let tr; try{ tr=await fetchTrack(); } catch(e){ ui('⚠ fetch error',false); console.error(e); return; } if(!tr.artistName){ ui('⏹ no track'); return; } if(!isPlaying()){ ui('⏸ paused'); return; } const title=`${tr.artistName} – ${tr.name}`; if(title===last){ ui('⏳ waiting'); return; } last=title; GM_setValue('lastTrack', last); await scrobble(tr, cfg); },15000); })();