aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/daily.ts100
-rw-r--r--server/data/mvs.ts122
-rw-r--r--server/shared.ts14
3 files changed, 236 insertions, 0 deletions
diff --git a/server/daily.ts b/server/daily.ts
index cfc9d6a..f3eb9d8 100644
--- a/server/daily.ts
+++ b/server/daily.ts
@@ -11,6 +11,7 @@ import {
verifySessionToken,
verifyStateSignature,
getDailySong,
+ getDailyMusicVideo,
} from "./shared";
export const dailyRouter = Router();
@@ -42,6 +43,25 @@ dailyRouter.get("/today", (_req, res) => {
});
});
+dailyRouter.get("/todayMV", (_req, res) => {
+ const date = getUtcDate();
+ const mv = getDailyMusicVideo(date);
+
+ const initialState: SignedState = {
+ date,
+ currentTry: 0,
+ didGuess: false,
+ guesses: [],
+ };
+
+ res.json({
+ date,
+ data: obfuscateSong(mv, date),
+ sessionToken: buildSessionToken(date),
+ initialSig: signState(initialState),
+ });
+});
+
dailyRouter.post("/guess", guessLimiter, (req, res) => {
const today = getUtcDate();
const body = req.body as {
@@ -121,3 +141,83 @@ dailyRouter.post("/guess", guessLimiter, (req, res) => {
guessState: nextGuess.state,
});
});
+
+dailyRouter.post("/guessMV", guessLimiter, (req, res) => {
+ const today = getUtcDate();
+ const body = req.body as {
+ sessionToken?: unknown;
+ state?: unknown;
+ sig?: unknown;
+ guess?: unknown;
+ };
+
+ if (!verifySessionToken(body.sessionToken, today)) {
+ res.status(401).json({ error: "Invalid session token." });
+ return;
+ }
+
+ const state = normalizeSignedState(body.state);
+ if (!state) {
+ res.status(400).json({ error: "Invalid state payload." });
+ return;
+ }
+
+ if (state.date !== today) {
+ res.status(409).json({ error: "State date is not valid for today." });
+ return;
+ }
+
+ if (!verifyStateSignature(state, body.sig)) {
+ res.status(403).json({ error: "State signature mismatch." });
+ return;
+ }
+
+ if (state.didGuess || state.currentTry >= 6) {
+ res.status(409).json({ error: "Round already finished." });
+ return;
+ }
+
+ let nextGuess: GuessEntry;
+ const guess = body.guess as { artist?: unknown; name?: unknown } | null | undefined;
+
+ if (guess == null) {
+ nextGuess = {
+ state: "Skipped",
+ };
+ } else if (typeof guess.artist === "string" && typeof guess.name === "string") {
+ const solution = getDailyMusicVideo(today);
+
+ if (guess.artist === solution.artist && guess.name === solution.name) {
+ nextGuess = {
+ song: { artist: guess.artist, name: guess.name },
+ state: "Correct",
+ };
+ } else if (guess.artist === solution.artist) {
+ nextGuess = {
+ song: { artist: guess.artist, name: guess.name },
+ state: "PartiallyCorrect",
+ };
+ } else {
+ nextGuess = {
+ song: { artist: guess.artist, name: guess.name },
+ state: "Incorrect",
+ };
+ }
+ } else {
+ res.status(400).json({ error: "Invalid guess payload." });
+ return;
+ }
+
+ const nextState: SignedState = {
+ date: state.date,
+ currentTry: Math.min(6, state.currentTry + 1),
+ didGuess: state.didGuess || nextGuess.state === "Correct",
+ guesses: [...state.guesses, nextGuess],
+ };
+
+ res.json({
+ state: nextState,
+ sig: signState(nextState),
+ guessState: nextGuess.state,
+ });
+});
diff --git a/server/data/mvs.ts b/server/data/mvs.ts
new file mode 100644
index 0000000..74a1d00
--- /dev/null
+++ b/server/data/mvs.ts
@@ -0,0 +1,122 @@
+export const musicVideos = [
+ { artist: "aespa", name: "Supernova", youtubeId: "phuiiNCxRMg" },
+ { artist: "aespa", name: "Spicy", youtubeId: "Os_heh8vPfs" },
+ { artist: "ILLIT", name: "Magnetic", youtubeId: "Vk5-c_v4gMU" },
+ { artist: "aespa", name: "Whiplash", youtubeId: "jWQx2f-CErU" },
+ { artist: "i-dle", name: "Senorita", youtubeId: "2cevbhEqQF4" },
+ { artist: "LE SSERAFIM", name: "CRAZY", youtubeId: "n6B5gQXlB-0" },
+ { artist: "KATSEYE", name: "Gameboy", youtubeId: "-bC4iak3kxg" },
+ { artist: "i-dle", name: "I Want That", youtubeId: "Iewisp9KYRw" },
+ { artist: "i-dle", name: "Nxde", youtubeId: "fCO7f0SmrDc" },
+ { artist: "TWICE", name: "What is Love?", youtubeId: "i0p1bmr0EmE" },
+ { artist: "NewJeans", name: "OMG", youtubeId: "sVTy_wmn5SU" },
+ { artist: "ITZY", name: "WANNABE", youtubeId: "fE2h3lGlOsk" },
+ { artist: "i-dle", name: "LATATA", youtubeId: "p4RIhcY7V3c" },
+ { artist: "STAYC", name: "GPT", youtubeId: "DskqpUrvlmw" },
+ { artist: "XG", name: "LEFT RIGHT", youtubeId: "T6YVgEpRU6Q" },
+ { artist: "i-dle", name: "Good Thing", youtubeId: "hAONx6nuEgI" },
+ { artist: "LE SSERAFIM", name: "HOT", youtubeId: "r9AEGPB6qIU" },
+ { artist: "XG", name: "GALA", youtubeId: "8cEdndsuzSE" },
+ { artist: "Hearts2Hearts", name: "FOCUS", youtubeId: "Ur7aK4FvK-U" },
+ { artist: "LE SSERAFIM", name: "DIFFERENT", youtubeId: "HFZUAXhdnHk" },
+ { artist: "MINNIE", name: "HER", youtubeId: "UTHqh7Vpa64" },
+ { artist: "YEJI (ITZY)", name: "Air", youtubeId: "TamDOXponnM" },
+ { artist: "i-dle", name: "DUMDi DUMDi", youtubeId: "HPQ5mqovXHo" },
+ { artist: "aespa", name: "Drama", youtubeId: "D8VEhcPeSlc" },
+ { artist: "i-dle", name: "Klaxon", youtubeId: "rTKqSmX9XhQ" },
+ { artist: "IVE", name: "I AM", youtubeId: "6ZUIwj3FgUY" },
+ { artist: "IVE", name: "BANG BANG", youtubeId: "9qkpcLK422o" },
+ { artist: "aespa", name: "Next Level", youtubeId: "4TWR90KJl84" },
+ { artist: "i-dle", name: "TOMBOY", youtubeId: "Jh4QFaPmdss" },
+ { artist: "NMIXX", name: "Love Me Like This", youtubeId: "EDnwWcFpObo" },
+ { artist: "LE SSERAFIM", name: "ANTIFRAGILE", youtubeId: "pyf8cbqyfPs" },
+ { artist: "ITZY", name: "Motto", youtubeId: "1jKnI_ib8c8" },
+ { artist: "i-dle", name: "Mono", youtubeId: "DYgE3SGPEqk" },
+ { artist: "NewJeans", name: "How Sweet", youtubeId: "Q3K0TOvTOno" },
+ { artist: "aespa", name: "Rich Man", youtubeId: "5oQVTnq-UKk" },
+ { artist: "ILLIT", name: "Do the Dance", youtubeId: "negtrQu5mTA" },
+ { artist: "MEOVV", name: "BURNING UP", youtubeId: "hlULtigNNao" },
+ { artist: "ITZY", name: "Girls Will Be Girls", youtubeId: "1KhOhW_O8-k" },
+ { artist: "ILLIT", name: "Lucky Girl Syndrome", youtubeId: "UCmgGZbfjmk" },
+ { artist: "TWICE", name: "THIS IS FOR", youtubeId: "eHHQaoEW30Q" },
+ { artist: "BLACKPINK", name: "GO", youtubeId: "2GJfWMYCWY0" },
+ { artist: "MIYEON", name: "Drive", youtubeId: "35lirBqwgTs" },
+ { artist: "KATSEYE", name: "Debut", youtubeId: "bYg6aMDQ_TA" },
+ { artist: "FIFTY FIFTY", name: "Cupid", youtubeId: "Qc7_zRjH808" },
+ { artist: "NMIXX", name: "Young, Dumb, Stupid", youtubeId: "5eh6Vj_vVg4" },
+ { artist: "NMIXX", name: "DICE", youtubeId: "p1bjnyDqI9k" },
+ { artist: "ITZY", name: "Cheshire", youtubeId: "zugAhfd2r0g" },
+ { artist: "ITZY", name: "DALLA DALLA", youtubeId: "pNfTK39k55U" },
+ { artist: "ITZY", name: "LOCO", youtubeId: "MjCZfZfucEc" },
+ { artist: "XG", name: "NEW DANCE", youtubeId: "LAZlfCjXPb0" },
+ { artist: "BLACKPINK", name: "DDU-DU DDU-DU", youtubeId: "IHNzOHi8sJs" },
+ { artist: "BLACKPINK", name: "AS IF IT'S YOUR LAST", youtubeId: "Amq-qlqbjYA" },
+ { artist: "BLACKPINK", name: "STAY", youtubeId: "FzVR_fymZw4" },
+ { artist: "NMIXX", name: "Party O’Clock", youtubeId: "Rd2wppggYxo" },
+ { artist: "IVE", name: "XOXZ", youtubeId: "B1ShLiq3EVc" },
+ { artist: "IVE", name: "BLACKHOLE", youtubeId: "1Lmy7qwmSMc" },
+ { artist: "IVE", name: "After LIKE", youtubeId: "F0B7HDiY-10" },
+ { artist: "IVE", name: "REBEL HEART", youtubeId: "g36q0ZLvygQ" },
+ { artist: "IVE", name: "Kitsch", youtubeId: "pG6iaOMV46I" },
+ { artist: "IVE", name: "Supernova Love", youtubeId: "fyk6vjwI3wc" },
+ { artist: "aespa", name: "Armageddon", youtubeId: "nFYwcndNuOY" },
+ { artist: "tripleS", name: "Girls Never Die", youtubeId: "2tda_TCjz8w" },
+ { artist: "tripleS", name: "Baby Flower", youtubeId: "6q8UMVXhXsE" },
+ { artist: "tripleS", name: "Rising", youtubeId: "3TQd2ahq6oU" },
+ { artist: "tripleS", name: "Are You Alive", youtubeId: "pTycfmVzdl8" },
+ { artist: "tripleS", name: "Cameo Love", youtubeId: "With0YDpuWQ" },
+ { artist: "STAYC", name: "2:LOVE", youtubeId: "HgewvBTOJwU" },
+ { artist: "STAYC", name: "MWUAH", youtubeId: "udOf9JGMA7U" },
+ { artist: "STAYC", name: "BEBE", youtubeId: "z-xfGoabprU" },
+ { artist: "STAYC", name: "I WANT IT", youtubeId: "W8ml4ryPwic" },
+ { artist: "Hearts2Hearts", name: "Lemon Tang", youtubeId: "1VqxWNwgf5Q" },
+ { artist: "Hearts2Hearts", name: "The Chase", youtubeId: "kxUA2wwYiME" },
+ { artist: "Hearts2Hearts", name: "RUDE!", youtubeId: "F7sGJVUrkjQ" },
+ { artist: "Hearts2Hearts", name: "STYLE", youtubeId: "n7kFRxFIPrI" },
+ { artist: "Hearts2Hearts", name: "Butterflies", youtubeId: "hJ9Wp3PO3c8" },
+ { artist: "TWICE", name: "LIKEY", youtubeId: "V2hlQkVJZhE" },
+ { artist: "TWICE", name: "Talk that Talk", youtubeId: "k6jqx9kZgPM" },
+ { artist: "TWICE", name: "FANCY", youtubeId: "kOHB85vDuow" },
+ { artist: "Red Velvet", name: "Cosmic", youtubeId: "FyG21rXCxlY" },
+ { artist: "Red Velvet", name: "Chill Kill", youtubeId: "xlyrt5eAtKI" },
+ { artist: "Red Velvet", name: "Psycho", youtubeId: "uR8Mrt1IpXg" },
+ { artist: "Red Velvet", name: "Peek-A-Boo", youtubeId: "6uJf2IT2Zh8" },
+ { artist: "Red Velvet", name: "Russian Roulette", youtubeId: "QslJYDX3o8s" },
+ { artist: "Red Velvet", name: "Feel My Rhythm", youtubeId: "R9At2ICm4LQ" },
+ { artist: "BABYMONSTER", name: "WE GO UP", youtubeId: "wlHwjkYpSr0" },
+ { artist: "BABYMONSTER", name: "PSYCHO", youtubeId: "yd_uG3TtREs" },
+ { artist: "BABYMONSTER", name: "SUGAR HONEY ICE TEA", youtubeId: "naoGk-Zjc1s" },
+ { artist: "BABYMONSTER", name: "DRIP", youtubeId: "Zp-Jhuhq0bQ" },
+ { artist: "BABYMONSTER", name: "Really Like You", youtubeId: "XShaIZs7J7M" },
+ { artist: "BABYMONSTER", name: "SHEESH", youtubeId: "2wA_b6YHjqQ" },
+ { artist: "BABYMONSTER", name: "SUPA DUPA LUV", youtubeId: "SbdOIdg2McI" },
+ { artist: "fromis_9", name: "Supersonic", youtubeId: "0LiQp7y8Wwc" },
+ { artist: "fromis_9", name: "DM", youtubeId: "4gXmClk8rKI" },
+ { artist: "fromis_9", name: "LIKE YOU BETTER", youtubeId: "r1QapH1SIAI" },
+ { artist: "fromis_9", name: "#menow", youtubeId: "QrYIOjs7K8E" },
+ { artist: "fromis_9", name: "LOVE BOMB", youtubeId: "-SK6cvkK4c0" },
+ { artist: "fromis_9", name: "FUN!", youtubeId: "zsRyyLtcXho" },
+ { artist: "ifeye", name: "NERDY", youtubeId: "I_msu2-4UAU" },
+ { artist: "ifeye", name: "r u ok?", youtubeId: "tGMy0z1akLs" },
+ { artist: "ifeye", name: "Hazy (Daisy)", youtubeId: "e-bwzm8-xnU" },
+ { artist: "RESCENE", name: "Deja Vu", youtubeId: "ZbO9PBdFRdc" },
+ { artist: "RESCENE", name: "LOVE ATTACK", youtubeId: "9XttLI0oH0I" },
+ { artist: "RESCENE", name: "Bloom", youtubeId: "MC6-82GRK5I" },
+ { artist: "RESCENE", name: "Runaway", youtubeId: "rsZwrTNklos" },
+ { artist: "RESCENE", name: "Glow Up", youtubeId: "h0xUtrb_JBc" },
+ { artist: "RESCENE", name: "Pinball", youtubeId: "B8JJ8RNM-60" },
+ { artist: "ILLIT", name: "It’s Me", youtubeId: "bMhDJ0S0OBA" },
+ { artist: "ILLIT", name: "jellyous", youtubeId: "GkG60kISnfc" },
+ { artist: "ILLIT", name: "NOT CUTE ANYMORE", youtubeId: "x_RYZsOfpKY" },
+ { artist: "ILLIT", name: "ICONIC BY MISTAKE", youtubeId: "27C4pfRsf9g" },
+ { artist: "ILLIT", name: "Cherish (My Love)", youtubeId: "tbDGl7jEazA" },
+ { artist: "ablume", name: "Echo", youtubeId: "EtecXqZnSDA" },
+ { artist: "izna", name: "BEEP", youtubeId: "wLKpQpC3-Sw" },
+ { artist: "izna", name: "IZNA", youtubeId: "d3mqW9wqqx0" },
+ { artist: "izna", name: "SIGN", youtubeId: "88GkYKvnvvI" },
+ { artist: "NiziU", name: "Too Bad", youtubeId: "b_GqrwmptWo" },
+ { artist: "NiziU", name: "Make you happy", youtubeId: "QW28YKqdxe0" },
+ { artist: "NiziU", name: "HEARTIS", youtubeId: "gRfDY9WrGwk" },
+ { artist: "NiziU", name: "♡Emotion", youtubeId: "1brvTNG4Caw" },
+ { artist: "NiziU", name: "Chopstick", youtubeId: "nCjmXHsRJNY" },
+]
diff --git a/server/shared.ts b/server/shared.ts
index 3b5344c..9744de9 100644
--- a/server/shared.ts
+++ b/server/shared.ts
@@ -1,5 +1,7 @@
import { createHmac, timingSafeEqual } from "crypto";
import { songs } from "./data/songs";
+import { musicVideos } from "./data/mvs";
+
function getSalt(): string {
return process.env.VITE_HEARDLE_SALT ?? "changeme";
@@ -58,6 +60,7 @@ export function pickSong(date: string): Song {
return songs[index];
}
+
function getLastNDates(n: number): string[] {
const dates: string[] = [];
for (let i = 1; i <= n; i++) {
@@ -83,6 +86,17 @@ export function getDailySong(today: string): Song {
return candidate;
}
+export function pickMusicVideo(date: string): Song {
+ const seed = hashString(date);
+ const index = seed % musicVideos.length;
+ return musicVideos[index];
+}
+
+export function getDailyMusicVideo(today: string): Song {
+ // potentially use deterministic no-repeat here too, for now this is fine
+ return pickMusicVideo(today);
+}
+
function signValue(prefix: string, value: string): string {
return createHmac("sha256", getSigningSecret())
.update(`${prefix}:${value}`)
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage