Rivales

Import races from ITRA

Drag the bookmarklet to your bookmarks bar, then click it on any ITRA race page (results or details). It discovers all modalities × the last 5 editions of that race family and lets you batch-import them.

Step 1 — install

Make your bookmarks bar visible (⌘⇧B), then drag this link onto it:

This bookmarklet posts to https://rivales.marcelfahle.net. If you're testing locally and want the production version, open this same page on your deployed rivales URL and drag the bookmarklet from there instead.

View the bookmarklet source
javascript:(async()=>{ if(document.getElementById('rivales-overlay'))return; const RIVALES="https://rivales.marcelfahle.net"; const N_YEARS=5; const RATE_MS=1000; const sleep=(ms)=>new Promise((r)=>setTimeout(r,ms)); const esc=(s)=>String(s).replace(/[&<>"']/g,(c)=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); const overlay=document.createElement('div'); overlay.id='rivales-overlay'; overlay.innerHTML='<style>'+ '#rivales-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:2147483647;display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif;color:#eee}'+ '#rivales-panel{background:#161616;border:1px solid #2a2a2a;border-radius:8px;padding:24px;max-width:760px;width:92%;max-height:88vh;overflow:auto;box-shadow:0 4px 30px rgba(0,0,0,.5)}'+ '#rivales-panel h2{margin:0 0 8px;font-size:18px}'+ '.rv-sub{color:#888;font-size:13px;margin-bottom:8px}'+ '.rv-progress{color:#a5e3aa;font-size:14px;margin:8px 0}'+ '.rv-error{color:#f87171;font-size:14px;margin:8px 0}'+ '.rv-table{width:100%;border-collapse:collapse;margin-top:8px;font-size:13px}'+ '.rv-table th,.rv-table td{padding:6px 10px;border-bottom:1px solid #222;text-align:left}'+ '.rv-table th{color:#888;font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.05em}'+ '.rv-table input[type=checkbox]{margin:0;accent-color:#4ade80}'+ '.rv-actions{display:flex;gap:8px;margin-top:16px;align-items:center}'+ '.rv-btn{padding:8px 14px;border-radius:6px;border:1px solid #2a2a2a;background:#1f1f1f;color:#eee;cursor:pointer;font-size:13px}'+ '.rv-btn:hover:not(:disabled){background:#262626}'+ '.rv-btn:disabled{opacity:.5;cursor:default}'+ '.rv-btn-primary{background:#4ade80;color:#0a0a0a;border-color:#4ade80}'+ '.rv-btn-primary:hover:not(:disabled){background:#22c55e}'+ '</style>'+ '<div id="rivales-panel">'+ '<h2>Import race family to rivales</h2>'+ '<div id="rivales-content"></div>'+ '</div>'; document.body.appendChild(overlay); const $c=overlay.querySelector('#rivales-content'); const setStatus=(html)=>{$c.innerHTML=html;}; const cancelBtn='<div class="rv-actions"><button class="rv-btn" id="rv-close">Close</button></div>'; const wireClose=()=>{const b=overlay.querySelector('#rv-close');if(b)b.onclick=()=>overlay.remove();}; if(!/\/Races\/Race(?:Details|Results)\//i.test(location.href)){ setStatus('<div class="rv-error">Open an ITRA race-details or race-results page first.</div>'+cancelBtn); wireClose();return; } setStatus('<div class="rv-progress">Loading race details…</div>'); let detailDoc; if(location.href.includes('/Races/RaceResults/')){ try{ const r=await fetch(location.href.replace('/RaceResults/','/RaceDetails/'),{credentials:'include'}); const html=await r.text(); detailDoc=new DOMParser().parseFromString(html,'text/html'); }catch(e){ setStatus('<div class="rv-error">Could not load race details: '+esc(e.message)+'</div>'+cancelBtn); wireClose();return; } } else { detailDoc=document; } const parseModalities=(doc)=>{ const out=[];const seen=new Set(); doc.querySelectorAll('a[href*="/Races/RaceDetails/"]').forEach((a)=>{ const h=a.getAttribute('href');if(!h)return; const m=h.match(/^\/Races\/RaceDetails\/([^/]+)\/(\d{4})\/(\d+)$/); if(!m)return; if(seen.has(m[3]))return;seen.add(m[3]); out.push({name:a.textContent.trim().replace(/\s+/g,' '),href:h,year:Number(m[2]),raceId:m[3],modalitySlug:m[1]}); }); return out; }; const parseEditions=(doc)=>{ const sel=doc.getElementById('select_id')||doc.querySelector('select[onchange*="EventPreviousEdition"]'); if(!sel)return[]; return Array.from(sel.options) .filter((o)=>o.value&&/^\d+$/.test(o.value)&&/^\d{4}$/.test(o.text.trim())) .map((o)=>({year:Number(o.text.trim()),repId:o.value})); }; const currentModalities=parseModalities(detailDoc); if(currentModalities.length===0){ setStatus('<div class="rv-error">No modality tabs found — page format may have changed.</div>'+cancelBtn); wireClose();return; } const currentYear=currentModalities[0].year; const editions=parseEditions(detailDoc).sort((a,b)=>b.year-a.year); const eventName=(detailDoc.querySelector('h1')||{}).textContent?(detailDoc.querySelector('h1').textContent.trim()):'this event'; const matrix=currentModalities.map((m)=>({...m})); const priorYears=editions.filter((e)=>e.year!==currentYear).slice(0,N_YEARS-1); for(let i=0;i<priorYears.length;i++){ const ed=priorYears[i]; setStatus('<div class="rv-progress">Discovering edition '+(i+1)+' of '+priorYears.length+' ('+ed.year+')…</div>'); await sleep(RATE_MS); try{ const r=await fetch('/Races/RaceDetails/'+ed.repId,{credentials:'include'}); if(!r.ok)continue; const html=await r.text(); const yd=new DOMParser().parseFromString(html,'text/html'); const ymods=parseModalities(yd); for(const ym of ymods){ if(!matrix.find((x)=>x.raceId===ym.raceId))matrix.push(ym); } }catch(e){/*continue*/} } matrix.sort((a,b)=>b.year-a.year||a.name.localeCompare(b.name)); const yearsCovered=Array.from(new Set(matrix.map((m)=>m.year))).sort((a,b)=>b-a); let rows=''; matrix.forEach((m,i)=>{ rows+='<tr><td><input type="checkbox" class="rv-cb" data-i="'+i+'" checked></td><td>'+m.year+'</td><td>'+esc(m.name)+'</td></tr>'; }); setStatus( '<div class="rv-sub">'+esc(eventName)+'</div>'+ '<p style="margin:8px 0;font-size:14px">Found <strong>'+matrix.length+'</strong> races across '+yearsCovered.length+' year'+(yearsCovered.length===1?'':'s')+' ('+yearsCovered[yearsCovered.length-1]+'–'+yearsCovered[0]+').</p>'+ '<table class="rv-table"><thead><tr><th><input type="checkbox" id="rv-all" checked></th><th>Year</th><th>Race</th></tr></thead><tbody>'+rows+'</tbody></table>'+ '<div class="rv-actions">'+ '<button class="rv-btn rv-btn-primary" id="rv-go">Import selected</button>'+ '<button class="rv-btn" id="rv-close">Cancel</button>'+ '</div>'+ '<div id="rv-status"></div>' ); wireClose(); overlay.querySelector('#rv-all').onchange=(e)=>{ overlay.querySelectorAll('.rv-cb').forEach((cb)=>{cb.checked=e.target.checked;}); }; overlay.querySelector('#rv-go').onclick=async()=>{ const checked=Array.from(overlay.querySelectorAll('.rv-cb:checked')).map((cb)=>matrix[Number(cb.dataset.i)]); if(checked.length===0){ overlay.querySelector('#rv-status').innerHTML='<div class="rv-error">Pick at least one race.</div>'; return; } const popup=window.open('about:blank','_rivales_import','width=760,height=640'); if(!popup){ overlay.querySelector('#rv-status').innerHTML='<div class="rv-error">Popup blocked. Allow popups for itra.run, then click Import again.</div>'; return; } popup.document.write('<!doctype html><html><head><title>Importing to rivales…</title></head><body style="background:#0a0a0a;color:#eee;font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><div style="text-align:center"><div style="color:#4ade80;font-size:1.25rem;margin-bottom:.5rem">⏳ Importing</div><div style="color:#888;font-size:.875rem">Fetching race data… this tab updates when done.</div></div></body></html>'); popup.document.close(); overlay.querySelector('#rv-go').disabled=true; overlay.querySelector('#rv-close').disabled=true; const $s=overlay.querySelector('#rv-status'); const items=[]; let sessionExpired=false; for(let i=0;i<checked.length;i++){ const r=checked[i]; $s.innerHTML='<div class="rv-progress">Fetching '+(i+1)+' of '+checked.length+': '+esc(r.name)+' '+r.year+'…</div>'; if(i>0)await sleep(RATE_MS); try{ const resultsHref=r.href.replace('/RaceDetails/','/RaceResults/'); const fr=await fetch(resultsHref,{credentials:'include'}); if(!fr.ok){console.warn('skip',r.name,fr.status);continue;} const html=await fr.text(); if(/Subscribe to an ITRA/i.test(html)){sessionExpired=true;break;} items.push({url:'https://itra.run'+resultsHref,html:html}); }catch(e){console.warn('skip',r.name,e);} } if(sessionExpired){ $s.innerHTML='<div class="rv-error">⚠ ITRA session expired. The page is showing the subscription upsell instead of scores.<br>1. Reload <a href="https://itra.run/" target="_blank" rel="noopener">itra.run</a> and sign in again.<br>2. Come back to this race page.<br>3. Click Import again.</div>'; try{popup.close();}catch(e){} overlay.querySelector('#rv-go').disabled=false; overlay.querySelector('#rv-close').disabled=false; return; } if(items.length===0){ $s.innerHTML='<div class="rv-error">No races could be fetched.</div>'; overlay.querySelector('#rv-go').disabled=false; overlay.querySelector('#rv-close').disabled=false; try{popup.close();}catch(e){} return; } $s.innerHTML='<div class="rv-progress">Submitting '+items.length+' race'+(items.length===1?'':'s')+' to rivales…</div>'; const form=document.createElement('form'); form.method='POST';form.action=RIVALES+'/api/itra-import-batch';form.target='_rivales_import'; form.enctype='application/x-www-form-urlencoded';form.style.display='none'; for(const it of items){ const u=document.createElement('input');u.type='hidden';u.name='url';u.value=it.url;form.appendChild(u); const h=document.createElement('input');h.type='hidden';h.name='html';h.value=it.html;form.appendChild(h); } document.body.appendChild(form); form.submit(); setTimeout(()=>{form.remove();overlay.remove();},1500); }; })();

Step 2 — use it

  1. Log in to itra.run so the score column is visible in race results.
  2. Open any race page (URL contains /Races/RaceDetails/ or /Races/RaceResults/).
  3. Click the bookmarklet. An overlay appears, discovers the race family (last 5 editions × all modalities), and shows a checklist. Uncheck anything you don't want, hit Import selected.
  4. A popup confirms the batch result with per-race success/failure.

The bookmarklet reads the page and its sibling editions/modalities in your browser (so your ITRA cookies travel automatically), then posts the harvested HTML plus the page URLs to https://rivales.marcelfahle.net. The cookie itself never leaves your browser.

Imported so far · 216 race-editions · 315 modalities

Race familyYearDistancesCountryFinishersImported
Ultra Trail Del Mar Al Cielo Del Ponient202235k · 57kES1044/25/2026, 2:51:33 PM
Uty Trail Yecla202261kES754/25/2026, 2:51:33 PM
Mamova202243kES944/25/2026, 2:51:33 PM
Espadan Trail Events202242kESscheduled4/25/2026, 2:51:33 PM
Transgrancanaria202261kES6364/25/2026, 2:51:32 PM
Ultra Trail Tarragona Sport Hg202247kES1114/25/2026, 2:51:32 PM
Héroes Contra Duchenne202251kES334/25/2026, 2:51:32 PM
Caldela S Marathons202237kESscheduled4/25/2026, 2:51:32 PM
La Osera202251kES454/25/2026, 2:51:16 PM
Marató Penyagolosa Xodos202242kESscheduled4/25/2026, 2:51:16 PM
Rioja Ultratrail202238kESscheduled4/25/2026, 2:51:16 PM
Spain Backyard Ultra202247kES444/25/2026, 2:51:16 PM
Los Montes De Vitoria202262kES8154/25/2026, 2:51:15 PM
Trail Ulldeter202243kES734/25/2026, 2:51:15 PM
Chandrexa Trail202243kES364/25/2026, 2:51:15 PM
Bimbache Trail202232kESscheduled4/25/2026, 2:51:15 PM
Os Foratos De Lomenás202253kES1304/25/2026, 2:51:14 PM
European Athletics Off Road Running Championships El Paso202247kES644/25/2026, 2:51:14 PM
Trail Catllaràs202240kESscheduled4/25/2026, 2:51:14 PM
Aguas De Teror Trail Desafío De Los Picos202238kES1984/25/2026, 2:51:14 PM
Rialp Matxicots202260kES2134/25/2026, 2:51:13 PM
Maximum Revolcadores202254kES384/25/2026, 2:51:13 PM
Ultra Montana Palentina202243kES804/25/2026, 2:51:13 PM
Alcudia De Veo Trail202246kES174/25/2026, 2:51:13 PM
Trail Ribeira Sacra202248kES2714/25/2026, 2:51:13 PM
Trepitja Garrotxa Solidària202253kESscheduled4/25/2026, 2:51:12 PM
Territorio Mentiras202235k · 56kESscheduled4/25/2026, 2:51:12 PM
Siyasa Gran Trail202250k · 64kES1184/25/2026, 2:51:12 PM
Seronda Redes Trail202243kES374/25/2026, 2:51:12 PM
Lll La Donaira Eco Mountain Marathon202239kESscheduled4/25/2026, 2:51:11 PM
Caras Nortes Skyrace202242kES964/25/2026, 2:51:11 PM
Ultra Maraton Costa De Almeria202243kESscheduled4/25/2026, 2:49:49 PM
Quiroga Trail Castelor Trail Do Lor202352kESscheduled4/25/2026, 2:49:48 PM
Trail Las Palomas Edicion Especial202339kES684/25/2026, 2:49:48 PM
Vertical Trail Villaverde De Guadalimar202337kES644/25/2026, 2:49:48 PM
Muntanyes De Prades Epic Trail Costa Daurada202345k · 60kES1864/25/2026, 2:49:48 PM
Desafio El Torcal202340kES3324/25/2026, 2:49:48 PM
Trail Costa Brava202343kES144/25/2026, 2:49:47 PM
Carrera Alto Sil202353kES874/25/2026, 2:49:47 PM
Trail Fuentealta Vilaflor202353kES294/25/2026, 2:49:47 PM
Cursa De Muntanya Del Pa Amb Tomàquet202341kES584/25/2026, 2:49:47 PM
Trail De Primavera Confrides202341kES2394/25/2026, 2:49:46 PM
Berga Trail202344kES634/25/2026, 2:49:46 PM
Trail Cap De Creus202343kES444/25/2026, 2:49:46 PM
Festival Cavalls Del Vent202351kES144/25/2026, 2:49:46 PM
Desafíosomiedo202346kES3164/25/2026, 2:49:46 PM
Half Marathon Des Sables Fuerteventura202359kESscheduled4/25/2026, 2:49:45 PM
Trail La Herradura De Campoo202336kESscheduled4/25/2026, 2:49:45 PM
Ultra Montana Palentina202341k · 44kES694/25/2026, 2:49:45 PM
Carrera Y Marcha De Rabosa202341kES464/25/2026, 2:49:45 PM

← Back to Picks