init
This commit is contained in:
commit
7813a44af0
5 changed files with 359 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Koen de Ruiter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
118
public/index.css
Normal file
118
public/index.css
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
@import"https://fonts.googleapis.com/css?family=Work+Sans:400,500,600,700,800,900";
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #262626;
|
||||||
|
margin: 20px;
|
||||||
|
color: #ffffff;
|
||||||
|
overflow: clip;
|
||||||
|
font-family:Work Sans,sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: columns;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Left column --- */
|
||||||
|
.song {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#song-image {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#song-diff {
|
||||||
|
font-size: 2rem;
|
||||||
|
padding: 16px;
|
||||||
|
min-width: 80px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid black;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 10px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#song-diff.stale {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#song-diff.risen {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
#song-diff.fallen {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Middle column --- */
|
||||||
|
.left-divide {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-stats {
|
||||||
|
margin-left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-info p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Right column --- */
|
||||||
|
#queue {
|
||||||
|
width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item {
|
||||||
|
display: flex;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item .song-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.queue-item .song-artist {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-left: 20px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.queue-item .song-time {
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.queue-right-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-image {
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-item.playing {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
}
|
||||||
39
public/index.html
Normal file
39
public/index.html
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="/index.js"></script>
|
||||||
|
<link rel="stylesheet" href="/index.css"></link>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="columns">
|
||||||
|
<div>
|
||||||
|
<div class="left-divide">
|
||||||
|
<div class="current-column">
|
||||||
|
<div class="song">
|
||||||
|
<img id="song-image">
|
||||||
|
<div id="song-diff"></div>
|
||||||
|
</div>
|
||||||
|
<h1 id="title"></h1>
|
||||||
|
<h2 id="artist"></h2>
|
||||||
|
<h3 id="position"></h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="song-stats">
|
||||||
|
<canvas id="graph" width=500 height=500></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="artist-info">
|
||||||
|
<h2>Trivia</h2>
|
||||||
|
<ul>
|
||||||
|
<li id="trivia-songcount"></li>
|
||||||
|
<li id="trivia-first"></li>
|
||||||
|
<li id="trivia-last"></li>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="queue"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
143
public/index.js
Normal file
143
public/index.js
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
window.onload = async (e) => {
|
||||||
|
const res = await (await fetch("https://www.nporadio2.nl/api/charts/npo-radio-2-top-2000-van-2025-12-25", {})).json();
|
||||||
|
window.ranking = res.positions;
|
||||||
|
|
||||||
|
const offset = +window.location.search.slice(1);
|
||||||
|
window.offset = offset;
|
||||||
|
|
||||||
|
const now = Date.now() - offset * 1000;
|
||||||
|
const curr = window.ranking.find(it => it.broadcastUnixTime < now);
|
||||||
|
var current = curr.position.current;
|
||||||
|
update(curr)
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now() - offset * 1000;
|
||||||
|
const curr = window.ranking.find(it => it.broadcastUnixTime < now);
|
||||||
|
|
||||||
|
if (curr.position.current != current) {
|
||||||
|
current = curr.position.current;
|
||||||
|
update(curr);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
function update(curr) {
|
||||||
|
console.log("Updating");
|
||||||
|
|
||||||
|
document.querySelector("#song-image").src = curr.track.coverUrl;
|
||||||
|
|
||||||
|
const diff = document.querySelector("#song-diff");
|
||||||
|
diff.innerText = curr.position.label;
|
||||||
|
diff.classList = curr.position.type;
|
||||||
|
|
||||||
|
document.querySelector("#title").innerText = curr.track.title;
|
||||||
|
document.querySelector("#artist").innerText = curr.track.artist;
|
||||||
|
document.querySelector("#position").innerText = `Nummer ${curr.position.current}`;
|
||||||
|
|
||||||
|
getGraph(curr.track.artist, curr.track.title);
|
||||||
|
queue(curr.position.current);
|
||||||
|
getTrivia(curr)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGraph(artist, title) {
|
||||||
|
const res = await (await fetch(`/graph/${artist.replace("?", "")}/${title}`, {})).json();
|
||||||
|
|
||||||
|
const width = 500;
|
||||||
|
const labelSpace = 50;
|
||||||
|
const barWidth = (width - labelSpace) / res.length;
|
||||||
|
const height = 500;
|
||||||
|
|
||||||
|
const cv = document.querySelector("#graph");
|
||||||
|
const ctx = cv.getContext('2d');
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
for (let i = 0; i <= 2000; i += 200) {
|
||||||
|
const fraction = (i / 2000) * height;
|
||||||
|
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = "#808080";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.moveTo(labelSpace, fraction);
|
||||||
|
ctx.lineTo(width, fraction);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.font = "20px sans-serif";
|
||||||
|
ctx.fillStyle = "#808080";
|
||||||
|
ctx.fillText(i.toString(), 0, fraction);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < res.length; i++) {
|
||||||
|
const position = res[i].position;
|
||||||
|
const fraction = (position / 2000) * height;
|
||||||
|
|
||||||
|
// ctx.fillRect(barWidth * i, 0, barWidth, position / 10)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.arc(labelSpace + barWidth * i + 10, fraction, 2.5, 0, 2*Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < res.length-1; i++) {
|
||||||
|
const curr = res[i].position;
|
||||||
|
const next = res[i+1].position;
|
||||||
|
const currFraction = (curr / 2000) * height;
|
||||||
|
const nextFraction = (next / 2000) * height;
|
||||||
|
|
||||||
|
// ctx.fillRect(barWidth * i, 0, barWidth, position / 10)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = "white";
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.moveTo(labelSpace + barWidth * i + 10, currFraction);
|
||||||
|
ctx.lineTo(labelSpace + barWidth * (i+1) + 10, nextFraction);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queue(currPos) {
|
||||||
|
const upcoming = window.ranking.filter(it => currPos - it.position.current < 10 && currPos - it.position.current >= -1)
|
||||||
|
|
||||||
|
const queueEl = document.querySelector('#queue')
|
||||||
|
queueEl.innerHTML = "";
|
||||||
|
|
||||||
|
for (const song of upcoming.reverse()) {
|
||||||
|
const broadcastDT = new Date(song.broadcastUnixTime - offset * 1000);
|
||||||
|
queueEl.innerHTML += `
|
||||||
|
<div class="queue-item${song.position.current == currPos ? " playing" : ""}">
|
||||||
|
<img class="queue-image" src="${song.track.coverUrl}">
|
||||||
|
<div class="queue-right-wrapper">
|
||||||
|
<p class="song-title">${song.track.title}</p>
|
||||||
|
<p class="song-artist">${song.track.artist}</p>
|
||||||
|
<p class="song-time">${broadcastDT.getHours().toString().padStart(2, "0")}:${broadcastDT.getMinutes().toString().padStart(2, "0")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTrivia(curr) {
|
||||||
|
const res = await (await fetch(`/trivia/${curr.track.artist.replace("?", "")}`, {})).json();
|
||||||
|
|
||||||
|
document.querySelector("#trivia-songcount").innerText = `${curr.track.artist} staat ${res.length} keer in de lijst.`;
|
||||||
|
|
||||||
|
const artistsSongs = res.map(function (it) {
|
||||||
|
return {
|
||||||
|
position: window.ranking.find(rank => rank.track.title == it.title)?.position?.current,
|
||||||
|
title: it.title
|
||||||
|
}
|
||||||
|
}).filter(it => it.position != undefined).sort((a,b) => a.position - b.position)
|
||||||
|
|
||||||
|
|
||||||
|
if (artistsSongs[0].position == curr.position.current) {
|
||||||
|
document.querySelector("#trivia-first").innerText = `Dit is het hoogste nummer van ${curr.track.artist} in de lijst.`;
|
||||||
|
} else {
|
||||||
|
document.querySelector("#trivia-first").innerText = `Het hoogste nummer van ${curr.track.artist} is ${artistsSongs[0].title}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistsSongs.at(-1).position == curr.position.current) {
|
||||||
|
document.querySelector("#trivia-last").innerText = `Dit is het laagste nummer van ${curr.track.artist} in de lijst.`;
|
||||||
|
} else {
|
||||||
|
document.querySelector("#trivia-last").innerText = `Het laagste nummer van ${curr.track.artist} is ${artistsSongs.at(-1).title}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
server.js
Normal file
38
server.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
const express = require('express')
|
||||||
|
const path = require('path')
|
||||||
|
const app = express()
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
app.use(express.static('public'))
|
||||||
|
app.use(express.urlencoded())
|
||||||
|
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.sendFile(path.resolve('./index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/graph/:artist/:title', async (req, res) => {
|
||||||
|
const artist = req.params.artist;
|
||||||
|
const title = req.params.title;
|
||||||
|
|
||||||
|
const data = await (await fetch(`https://nporadio2.nl/api/statistics/search/tracks/${artist}`, {})).json();
|
||||||
|
|
||||||
|
const id = data.find(it => it.title == title).id
|
||||||
|
|
||||||
|
const standings = await (await fetch(`https://nporadio2.nl/api/statistics/positions/track/${id}`, {})).json();
|
||||||
|
|
||||||
|
res.send(standings);
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/trivia/:artist', async (req, res) => {
|
||||||
|
const artist = req.params.artist;
|
||||||
|
|
||||||
|
const data = await (await fetch(`https://nporadio2.nl/api/statistics/search/tracks/${artist}`, {})).json();
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
res.send(data);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
app.listen(3000, () => {
|
||||||
|
console.log("Listening on port 3000")
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue