Skip to content

Instantly share code, notes, and snippets.

@mooware
Last active January 14, 2023 01:13
Show Gist options
  • Save mooware/460b710eb7d6a9883b32064f6993597e to your computer and use it in GitHub Desktop.
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
<!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/bootstrap@4.5.3/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/bootstrap@4.5.3/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