commit 7813a44af0a778e9c3e24c743a8c9aab8a618acd Author: KoenDR06 Date: Fri Dec 26 01:04:35 2025 +0100 init diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b89dbdb --- /dev/null +++ b/LICENSE @@ -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. diff --git a/public/index.css b/public/index.css new file mode 100644 index 0000000..5cd2865 --- /dev/null +++ b/public/index.css @@ -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; +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..d23d7de --- /dev/null +++ b/public/index.html @@ -0,0 +1,39 @@ + + + + + + + + + +
+
+
+
+
+ +
+
+

+

+

+
+ +
+ +
+
+
+

Trivia

+
    +
  • +
  • +
  • +
+
+ +
+
+ + diff --git a/public/index.js b/public/index.js new file mode 100644 index 0000000..ea53c0e --- /dev/null +++ b/public/index.js @@ -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 += ` +
+ +
+

${song.track.title}

+

${song.track.artist}

+

${broadcastDT.getHours().toString().padStart(2, "0")}:${broadcastDT.getMinutes().toString().padStart(2, "0")}

+
+
+ ` + } +} + +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}.`; + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..d0e82bb --- /dev/null +++ b/server.js @@ -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") +})