aboutsummaryrefslogtreecommitdiffstats
path: root/server/shared.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/shared.ts')
-rw-r--r--server/shared.ts74
1 files changed, 40 insertions, 34 deletions
diff --git a/server/shared.ts b/server/shared.ts
index 9744de9..8b4afc4 100644
--- a/server/shared.ts
+++ b/server/shared.ts
@@ -1,7 +1,11 @@
-import { createHmac, timingSafeEqual } from "crypto";
+import { createHash, createHmac, timingSafeEqual } from "crypto";
import { songs } from "./data/songs";
import { musicVideos } from "./data/mvs";
+const DAILY_START_DATE = "2026-06-01T00:00:00Z";
+const DAILY_START_MS = Date.parse(DAILY_START_DATE);
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
function getSalt(): string {
return process.env.VITE_HEARDLE_SALT ?? "changeme";
@@ -46,54 +50,56 @@ export function getUtcDate(): string {
return new Date().toISOString().slice(0, 10);
}
-function hashString(str: string): number {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- hash = (hash * 31 + str.charCodeAt(i)) >>> 0;
+function daysSinceStart(date: string): number {
+ const ms = Date.parse(date.length === 10 ? `${date}T00:00:00Z` : date);
+ if (Number.isNaN(ms)) {
+ throw new Error(`Invalid date: ${date}`);
}
- return hash;
+ return Math.floor((ms - DAILY_START_MS) / MS_PER_DAY);
}
-export function pickSong(date: string): Song {
- const seed = hashString(date);
- const index = seed % songs.length;
- return songs[index];
+function seededPRNG(seed: string): () => number {
+ let counter = 0;
+ return () => {
+ counter++;
+ const hash = createHash("sha256")
+ .update(`${seed}:${counter}`)
+ .digest();
+ const int = hash.readUInt32BE(0);
+ return int / 0x100000000;
+ };
}
-
-function getLastNDates(n: number): string[] {
- const dates: string[] = [];
- for (let i = 1; i <= n; i++) {
- const d = new Date();
- d.setUTCDate(d.getUTCDate() - i);
- dates.push(d.toISOString().slice(0, 10));
+function shuffleSongs<T>(seed: string, list: readonly T[]): T[] {
+ const result = list.slice();
+ const rng = seededPRNG(seed);
+ for (let i = result.length - 1; i > 0; i--) {
+ const j = Math.floor(rng() * (i + 1));
+ [result[i], result[j]] = [result[j], result[i]];
}
- return dates;
+ return result;
+}
+
+export const DAILY_SONG_ORDER: Song[] = shuffleSongs(getSalt(), songs);
+export const DAILY_MV_ORDER: Song[] = shuffleSongs(getSalt() + "-mv", musicVideos);
+
+export function pickSong(date: string): Song {
+ const day = daysSinceStart(date);
+ const index = ((day % DAILY_SONG_ORDER.length) + DAILY_SONG_ORDER.length) % DAILY_SONG_ORDER.length;
+ return DAILY_SONG_ORDER[index];
}
export function getDailySong(today: string): Song {
- const recentSongs = new Set(
- getLastNDates(30).map(d => pickSong(d).youtubeId)
- );
- let candidate = pickSong(today);
- let guard = 0;
- while (recentSongs.has(candidate.youtubeId) && guard < songs.length) {
- const rerollSeed = hashString(today + ":" + guard);
- const index = rerollSeed % songs.length;
- candidate = songs[index];
- guard++;
- }
- return candidate;
+ return pickSong(today);
}
export function pickMusicVideo(date: string): Song {
- const seed = hashString(date);
- const index = seed % musicVideos.length;
- return musicVideos[index];
+ const day = daysSinceStart(date);
+ const index = ((day % DAILY_MV_ORDER.length) + DAILY_MV_ORDER.length) % DAILY_MV_ORDER.length;
+ return DAILY_MV_ORDER[index];
}
export function getDailyMusicVideo(today: string): Song {
- // potentially use deterministic no-repeat here too, for now this is fine
return pickMusicVideo(today);
}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage