Last active
January 14, 2023 01:13
-
-
Save mooware/460b710eb7d6a9883b32064f6993597e to your computer and use it in GitHub Desktop.
HTML page to list SRL races and link to multitwitch and similar services
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |
<title>SRL Races</title> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> | |
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script> | |
<style> | |
.entrant + .entrant::before { | |
display: inline-block; | |
padding-right: .5rem; | |
color: #6c757d; | |
content: "/"; | |
} | |
.entrant { | |
display: inline-block; | |
padding-right: .5rem; | |
} | |
.entrant-list { | |
display: flex; | |
flex-wrap: wrap; | |
} | |
.game-thumbnail { | |
width: 160px; | |
height: 120px; | |
background-size: cover; | |
background-color: black; | |
} | |
.text-racestate-1 { color: #17a2b8; } | |
.text-racestate-3 { color: #28a745; } | |
</style> | |
</head> | |
<body class="py-4 bg-dark text-light"> | |
<div class="container"> | |
<div id="error" class="alert alert-warning d-none" role="alert"></div> | |
<h1>SRL Races</h1> | |
<span id="counts"></span> | |
<a id="showall" href="?showall=1">(show all)</a> | |
</div> | |
<script> | |
const SRL_API = 'https://api.speedrunslive.com/races/'; | |
const RACE_STATE_ENTRY_OPEN = 1; | |
const RACE_STATE_IN_PROGRESS = 3; | |
const RACE_STATE_COMPLETE = 4; | |
// escape strings for HTML | |
function esc(str) { | |
return new Option(str).innerHTML; | |
} | |
// format a time interval in seconds into text like "hh:mm:ss" | |
function makeTimeText(totalSeconds) { | |
var hours = Math.floor(totalSeconds / 3600); | |
var minutes = Math.floor((totalSeconds - hours * 3600) / 60); | |
var seconds = Math.floor(totalSeconds % 60); | |
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
} | |
// update live timers for races | |
function updateTimes() { | |
var now = new Date(); | |
$('.racetime').each(function (index, elem) { | |
var ts = elem.dataset.time; | |
var diff = (new Date().getTime() / 1000) - ts; | |
$(elem).text(makeTimeText(diff)); | |
}); | |
} | |
// generate html for a single race | |
function makeRaceElement(data) { | |
var entrants = []; | |
var twitchnames = []; | |
for (var key in data.entrants) { | |
var entrant = data.entrants[key]; | |
var tooltip = entrant.statetext; | |
var icon = ''; | |
if (data.state == RACE_STATE_ENTRY_OPEN && entrant.statetext == 'Ready') { | |
icon = String.fromCodePoint(0x2714); // check mark | |
} else if (entrant.statetext == 'Finished') { | |
tooltip = `Finished #${entrant.place} in ${makeTimeText(entrant.time)}`; | |
icon = String.fromCodePoint(0x1F3C1); // chequered flag | |
} else if (entrant.statetext == 'Forfeit') { | |
icon = String.fromCodePoint(0x274C); // red cross mark | |
} | |
if (entrant.message) { | |
tooltip += `, comment: ${entrant.message}`; | |
} | |
if (entrant.twitch) { | |
twitchnames.push(entrant.twitch); | |
entrants.push(`<span class="entrant" title="${esc(tooltip)}"><a href="https://twitch.tv/${esc(entrant.twitch)}" target="_blank">${esc(entrant.displayname)} ${icon}</a></span>`); | |
} else { | |
entrants.push(`<span class="entrant" title="${esc(tooltip)}">${esc(entrant.displayname)} ${icon}</span>`); | |
} | |
} | |
var racetime = ''; | |
if (data.state == RACE_STATE_IN_PROGRESS) { | |
racetime = `, <span class="racetime" data-time="${data.time}"></span>`; | |
} | |
var racestart = ''; | |
if (data.time != 0) { | |
var dt = new Date(data.time * 1000); | |
racestart = `<span class="racestart" title="started at ${dt.toLocaleString()} (local time)">${String.fromCodePoint(0x23F1)}</span>`; // stopwatch icon | |
} | |
var streamparams = twitchnames.join('/'); | |
return ` | |
<div class="race raceid-${data.id} media my-3 p-3 border-top"> | |
<div class="align-self-start mr-3 rounded game-thumbnail" style="background-image: url('https://cdn.speedrunslive.com/images/games/${data.game.abbrev}.jpg');"></div> | |
<div class="media-body"> | |
<div class="float-right"> | |
<a href="https://multistre.am/${streamparams}" target="_blank" class="d-block mb-1 btn btn-success btn-sm" role="button">multistre.am</a> | |
<a href="https://multitwitch.tv/${streamparams}" target="_blank" class="d-block mb-1 btn btn-primary btn-sm" role="button">multitwitch.tv</a> | |
<a href="https://kadgar.net/live/${streamparams}" target="_blank" class="d-block mb-1 btn btn-info btn-sm" role="button">kadgar.net</a> | |
</div> | |
<div class="headline"><a href="https://www.speedrunslive.com/race/${data.id}" target="_blank">#srl-${data.id}</a> <span class="text-racestate-${data.state}">(${data.statetext}${racetime})</span>${racestart}</div> | |
<h4 class="text-gametitle">${esc(data.game.name)}</h4> | |
<p class="text-muted">${esc(data.goal)}</p> | |
<div class="entrant-list"> | |
${entrants.join('')} | |
</div> | |
</div> | |
</div>`; | |
} | |
// update the page with race data | |
function updateRaces(data, showAll) { | |
$('.race').remove(); | |
var races = data.races; | |
races.sort(function (a, b) { | |
var statediff = a.state - b.state; | |
if (statediff != 0) { | |
return statediff; | |
} | |
return b.time - a.time; | |
}); | |
var counts = new Map(); | |
for (var key in races) { | |
counts.set(races[key].statetext, (counts.get(races[key].statetext) ?? 0) + 1); | |
if (showAll || races[key].state < RACE_STATE_COMPLETE) { | |
var html = makeRaceElement(races[key]); | |
$('.container').append(html); | |
} | |
} | |
var countsText = ''; | |
for (var [state, count] of counts) { | |
if (countsText.length != 0) { | |
countsText += ', '; | |
} | |
countsText += `${count} ${state}`; | |
} | |
if (countsText.length == 0) { | |
$('#showall').hide(); | |
countsText = 'no races'; | |
} else { | |
$('#showall').show(); | |
} | |
$('#counts').text(countsText); | |
updateTimes(); | |
window.setInterval(updateTimes, 1000); | |
} | |
fetch(SRL_API) | |
.then((response) => { | |
if (response.status != 200) | |
throw Error(response.statusText); | |
return response.json(); | |
}) | |
.then((jsonResponse) => { | |
if (Object.keys(jsonResponse).length === 0) { | |
$('#error').text('SRL API did not return any data').addClass('d-none'); | |
} else { | |
$('#error').hide(); | |
var showAll = (window.location.search == "?showall=1"); | |
updateRaces(jsonResponse, showAll); | |
console.log('races', jsonResponse); | |
} | |
}).catch((error) => { | |
$('#error').text('SRL API request failed (try https://www.speedrunslive.com/races). ' + error).removeClass('d-none'); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment