From 41c93d40dc94781de4e5ad6af67f350c580e0af6 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 14 Jun 2025 04:34:28 -0600
Subject: [PATCH 001/108] idk most of the indie game heardle shit
---
development.sh | 4 +-
src/components/Tunicwilds/Tunicwilds.svelte | 511 ++++++++++++++++++++
src/database/factories.ts | 11 +-
src/database/schema.ts | 19 +
src/pages/api/tunicwilds.ts | 139 ++++++
src/pages/tunicwilds.astro | 36 ++
src/server/thingUtils.ts | 3 +-
7 files changed, 720 insertions(+), 3 deletions(-)
create mode 100644 src/components/Tunicwilds/Tunicwilds.svelte
create mode 100644 src/pages/api/tunicwilds.ts
create mode 100644 src/pages/tunicwilds.astro
diff --git a/development.sh b/development.sh
index 92ebeb5..c4b34ba 100755
--- a/development.sh
+++ b/development.sh
@@ -20,15 +20,17 @@ export SOUNDS_RUN_AFTER_UPLOAD=
export SOUNDS_UPLOAD_DIRECTORY='dev/sounds'
export WORDS_RUN_AFTER_UPLOAD=
export WORDS_UPLOAD_DIRECTORY='dev/words'
+export TUNICWILDS_DIRECTORY='dev/tunicwilds'
export PUBLIC_DIRECTORY='public'
# Clean up files from last run
-rm -rf "$database" "$SIGHTS_UPLOAD_DIRECTORY" "$SOUNDS_UPLOAD_DIRECTORY" "$WORDS_UPLOAD_DIRECTORY"
+rm -rf "$database" "$SIGHTS_UPLOAD_DIRECTORY" "$SOUNDS_UPLOAD_DIRECTORY" "$WORDS_UPLOAD_DIRECTORY" "$TUNICWILDS_DIRECTORY"
# Create directories
mkdir -p "$SIGHTS_UPLOAD_DIRECTORY"
mkdir -p "$SOUNDS_UPLOAD_DIRECTORY"
mkdir -p "$WORDS_UPLOAD_DIRECTORY"
+mkdir -p "$TUNICWILDS_DIRECTORY"
# Create database
node db/migrate.mjs "$database"
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
new file mode 100644
index 0000000..32e059f
--- /dev/null
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -0,0 +1,511 @@
+
+
+
+
+
+
+
+
+
+ {#if gameWon || gameLost}
+
+
+
"{songData.title}"
+
+ from {songData.game}
+
+
+ Composed by {songData.composer}
+
+
+
+ {#if gameWon}
+
+
Nice
+
+ You guessed it in {guesses.length} attempt{guesses.length !==
+ 1
+ ? "s"
+ : ""}!
+
+
+ {:else}
+
+
π Game Over
+
Better luck next time!
+
+ {/if}
+
+
+
+
+
+ {/if}
+
+
+
+
+
+ Clip length: {getCurrentClipLength()}s
+
+
+ Attempt {guesses.length + 1} of {maxGuesses}
+
+
+
+
+
+
+
+
+ {#if showGameHint && !gameWon && !gameLost}
+
+ π‘ Hint: This song is from
+ {songData.game}
+
+ {/if}
+
+
+
+
+
+
+
+
+ {#if !gameWon && !gameLost}
+
+ {/if}
+
+
+ {#if guesses.length > 0}
+
+
Your Guesses:
+
+ {#each guesses as guess, index}
+ {@const isCorrect =
+ guess.toLowerCase() === songData.title.toLowerCase()}
+ {@const guessedSong = songList.find(
+ (song) =>
+ song.title.toLowerCase() === guess.toLowerCase(),
+ )}
+
+
+
+
+
{guess}
+ {#if guessedSong}
+
+ {guessedSong.game}
+
+ {/if}
+
+
+ {clipLengths[index]}s clip
+
+
+
+ {/each}
+
+
+ {/if}
+
+
+
diff --git a/src/database/factories.ts b/src/database/factories.ts
index e70abd6..0fcee37 100644
--- a/src/database/factories.ts
+++ b/src/database/factories.ts
@@ -1,6 +1,6 @@
import { faker } from "@faker-js/faker";
import defineFactory from "./defineFactory";
-import { Action, ActionItem, Member, Motion, Session, Sight, Sound, Ticket, Word } from "./schema";
+import { Action, ActionItem, Member, Motion, Session, Sight, Sound, Ticket, Tunicwild, Word } from "./schema";
function unique(fn: () => T): () => T {
const used = new Set();
@@ -89,3 +89,12 @@ export const WordFactory = defineFactory(Word, {
date: unique(() => faker.date.recent()),
tags,
});
+
+export const TunicwildFactory = defineFactory(Tunicwild, {
+ memberDiscord: () => { throw new Error("Not implemented"); },
+ composer: () => faker.person.firstName(),
+ title: () => faker.music.songName(),
+ game: () => faker.commerce.productName(),
+ releaseDate: () => faker.date.past(),
+ extraHint: () => faker.lorem.sentence(),
+});
\ No newline at end of file
diff --git a/src/database/schema.ts b/src/database/schema.ts
index 9f4f58d..f4ca626 100644
--- a/src/database/schema.ts
+++ b/src/database/schema.ts
@@ -64,6 +64,7 @@ export const MemberRelations = relations(Member, ({ many }) => ({
sounds: many(Sound),
tickets: many(Ticket),
words: many(Word),
+ tunicwilds: many(Tunicwild),
}));
export const Motion = sqliteTable("Motion", {
@@ -162,3 +163,21 @@ export const WordRelations = relations(Word, ({ one }) => ({
references: [Member.discord],
}),
}));
+
+
+export const Tunicwild = sqliteTable("Tunicwild", {
+ id: integer().primaryKey({ autoIncrement: true }),
+ memberDiscord: text().notNull().references(() => Member.discord),
+ composer: text().notNull(),
+ title: text().notNull(),
+ game: text().notNull(),
+ releaseDate: date().notNull(),
+ extraHint: text().notNull(),
+});
+
+export const TunicwildRelations = relations(Tunicwild, ({ one }) => ({
+ member: one(Member, {
+ fields: [Tunicwild.memberDiscord],
+ references: [Member.discord],
+ }),
+}));
\ No newline at end of file
diff --git a/src/pages/api/tunicwilds.ts b/src/pages/api/tunicwilds.ts
new file mode 100644
index 0000000..450c03c
--- /dev/null
+++ b/src/pages/api/tunicwilds.ts
@@ -0,0 +1,139 @@
+import { LibsqlError } from "@libsql/client";
+import type { APIRoute } from "astro";
+import { eq, type InferSelectModel } from "drizzle-orm";
+import { jsonError, jsonResponse } from "../../server/responses";
+import { mkdir, writeFile } from "fs/promises";
+import { createWriteStream, ReadStream } from "fs";
+import { Marked } from "marked";
+import { baseUrl as markedBaseUrl } from "marked-base-url";
+import { markedEmoji } from "marked-emoji";
+import { execFileSync } from "child_process";
+import { finished } from "stream/promises";
+import { thingDeletion, thingGet } from "../../server/thingUtils";
+import { serverHTMLPurify } from "../../components/DOMPurify/server";
+import { getMap } from "../../data/emoji";
+import db, { retryIfDbBusy } from "../../database/db";
+import { Member, Tunicwild } from "../../database/schema";
+
+export const prerender = false;
+
+
+export const GET: APIRoute = async (context) => thingGet(context, "tunicwilds");
+
+export const POST: APIRoute = async (context) => {
+ if (!process.env.TUNICWILDS_DIRECTORY) {
+ return jsonError("TUNICWILDS_DIRECTORY not set", 500);
+ }
+
+ let formData: FormData;
+ try {
+ formData = await context.request.formData();
+ } catch {
+ return jsonError("Request body must be form data");
+ }
+
+ const discord = formData.get("discord");
+ const composer = formData.get("composer");
+ const title = formData.get("title");
+ const game = formData.get("game");
+ const releaseDate = formData.get("releaseDate");
+ const extraHint = formData.get("extraHint");
+ const file = formData.get("file") as File;
+
+ // Form validation
+ if (
+ typeof discord !== "string" ||
+ typeof composer !== "string" ||
+ typeof title !== "string" ||
+ typeof game !== "string" ||
+ typeof releaseDate !== "string" ||
+ typeof extraHint !== "string" ||
+ !(file instanceof File)
+ ) {
+ return jsonError("Invalid form params");
+ }
+ const releaseDateParsed = new Date(releaseDate);
+ if (isNaN(releaseDateParsed.getTime())) {
+ return jsonError("Invalid release date");
+ }
+ if (releaseDateParsed.getTime() > Date.now()) {
+ return jsonError("Release date cannot be in the future");
+ }
+
+ if (file.size > fileSizeLimit) {
+ return jsonError("File size exceeds limit of 1 MB");
+ }
+
+ const member = await db
+ .select()
+ .from(Member)
+ .where(eq(Member.discord, discord))
+ .get();
+ if (!member) {
+ return jsonError("Member does not exist");
+ }
+
+ if (member.deleted) {
+ return jsonError("Member has been deleted");
+ }
+
+ if (composer.length > 2 ** 10) {
+ return jsonError("Composer name is too long");
+ }
+ if (title.length > 2 ** 10) {
+ return jsonError("Title is too long");
+ }
+ if (game.length > 2 ** 10) {
+ return jsonError("Game name is too long");
+ }
+ if (extraHint.length > 2 ** 10) {
+ return jsonError("Extra hint is too long");
+ }
+
+ // File validation
+ if (!file.name.endsWith(".ogg") && !file.name.endsWith(".opus") && !file.name.endsWith(".wav") && !file.name.endsWith(".mp3")) {
+ return jsonError("File must be an OGG, Opus, WAV, or MP3 file");
+ }
+
+ // Store to DB
+ let tunicwild: InferSelectModel;
+ try {
+ tunicwild = await retryIfDbBusy(() =>
+ db
+ .insert(Tunicwild)
+ .values({
+ memberDiscord: discord,
+ composer,
+ title,
+ game,
+ releaseDate: releaseDateParsed,
+ extraHint,
+ })
+ .returning()
+ .get()
+ );
+ } catch (error) {
+ if (error instanceof LibsqlError) {
+ if (error.code === "SQLITE_CONSTRAINT_UNIQUE") {
+ return jsonError("Word already exists");
+ }
+
+ if (error.code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
+ return jsonError(
+ "Invalid Discord ID; member does not exist; probably needs to join first",
+ );
+ }
+ }
+
+ throw error;
+ }
+
+ // Upload files
+ ReadStream.fromWeb(file.stream()).pipe(
+ createWriteStream(`${process.env.TUNICWILDS_DIRECTORY}/${tunicwild.id}.${file.name.split('.').pop()}`),
+ )
+};
+
+export const PUT: APIRoute = async (context) => thingDeletion(context, "tunicwilds", false);
+
+export const DELETE: APIRoute = async (context) => thingDeletion(context, "tunicwilds", true);
\ No newline at end of file
diff --git a/src/pages/tunicwilds.astro b/src/pages/tunicwilds.astro
new file mode 100644
index 0000000..e6c361b
--- /dev/null
+++ b/src/pages/tunicwilds.astro
@@ -0,0 +1,36 @@
+---
+import Layout from "../layouts/Layout.astro";
+import TunicwildsApp from "../components/Tunicwilds/Tunicwilds.svelte";
+import db from "../database/db";
+import { Tunicwild } from "../database/schema";
+
+export const prerender = false;
+
+if (!process.env.TUNICWILDS_DIRECTORY) {
+ Astro.response.status = 500;
+ Astro.response.statusText =
+ "TUNICWILDS_DIRECTORY environment variable is not set";
+}
+
+const startDate = new Date("2025-06-14");
+const date = new Date();
+
+const songList = await db.select().from(Tunicwild);
+
+// Get the song for today's challenge
+// Really shitty placeholder
+const songData =
+ songList[
+ Math.floor(
+ ((date.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) %
+ songList.length
+ )
+ ];
+---
+
+
+
+
diff --git a/src/server/thingUtils.ts b/src/server/thingUtils.ts
index 21177bc..69ed14a 100644
--- a/src/server/thingUtils.ts
+++ b/src/server/thingUtils.ts
@@ -4,7 +4,7 @@ import { and, eq } from "drizzle-orm";
import { jsonError, jsonResponse } from "./responses";
import { paginationQuery, parseNumberCursor } from "./pagination";
import db, { retryIfDbBusy } from "../database/db";
-import { Action, Motion, Sight, Sound, Word } from "../database/schema";
+import { Action, Motion, Sight, Sound, Tunicwild, Word } from "../database/schema";
const thingTypeToTable = {
words: Word,
@@ -12,6 +12,7 @@ const thingTypeToTable = {
motions: Motion,
sights: Sight,
actions: Action,
+ tunicwilds: Tunicwild,
};
type ThingType = keyof typeof thingTypeToTable;
const thingTypes = Object.keys(thingTypeToTable);
From 63756a36bef64b8dd3ad7929065a2f3693681c0c Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 14 Jun 2025 04:41:13 -0600
Subject: [PATCH 002/108] obv fixes
---
src/database/schema.ts | 1 +
src/pages/api/tunicwilds.ts | 17 +++--------------
2 files changed, 4 insertions(+), 14 deletions(-)
diff --git a/src/database/schema.ts b/src/database/schema.ts
index f4ca626..c4284f2 100644
--- a/src/database/schema.ts
+++ b/src/database/schema.ts
@@ -173,6 +173,7 @@ export const Tunicwild = sqliteTable("Tunicwild", {
game: text().notNull(),
releaseDate: date().notNull(),
extraHint: text().notNull(),
+ deleted: integer({ mode: "boolean" }).default(false).notNull(),
});
export const TunicwildRelations = relations(Tunicwild, ({ one }) => ({
diff --git a/src/pages/api/tunicwilds.ts b/src/pages/api/tunicwilds.ts
index 450c03c..44449e8 100644
--- a/src/pages/api/tunicwilds.ts
+++ b/src/pages/api/tunicwilds.ts
@@ -2,22 +2,13 @@ import { LibsqlError } from "@libsql/client";
import type { APIRoute } from "astro";
import { eq, type InferSelectModel } from "drizzle-orm";
import { jsonError, jsonResponse } from "../../server/responses";
-import { mkdir, writeFile } from "fs/promises";
import { createWriteStream, ReadStream } from "fs";
-import { Marked } from "marked";
-import { baseUrl as markedBaseUrl } from "marked-base-url";
-import { markedEmoji } from "marked-emoji";
-import { execFileSync } from "child_process";
-import { finished } from "stream/promises";
import { thingDeletion, thingGet } from "../../server/thingUtils";
-import { serverHTMLPurify } from "../../components/DOMPurify/server";
-import { getMap } from "../../data/emoji";
import db, { retryIfDbBusy } from "../../database/db";
import { Member, Tunicwild } from "../../database/schema";
export const prerender = false;
-
export const GET: APIRoute = async (context) => thingGet(context, "tunicwilds");
export const POST: APIRoute = async (context) => {
@@ -60,10 +51,6 @@ export const POST: APIRoute = async (context) => {
return jsonError("Release date cannot be in the future");
}
- if (file.size > fileSizeLimit) {
- return jsonError("File size exceeds limit of 1 MB");
- }
-
const member = await db
.select()
.from(Member)
@@ -131,7 +118,9 @@ export const POST: APIRoute = async (context) => {
// Upload files
ReadStream.fromWeb(file.stream()).pipe(
createWriteStream(`${process.env.TUNICWILDS_DIRECTORY}/${tunicwild.id}.${file.name.split('.').pop()}`),
- )
+ );
+
+ return jsonResponse(tunicwild);
};
export const PUT: APIRoute = async (context) => thingDeletion(context, "tunicwilds", false);
From 3c6534510bbed058682dc466792b224452968fde Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 14 Jun 2025 04:41:17 -0600
Subject: [PATCH 003/108] migration
---
db/migration/019-add-tunicwilds.sql | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 db/migration/019-add-tunicwilds.sql
diff --git a/db/migration/019-add-tunicwilds.sql b/db/migration/019-add-tunicwilds.sql
new file mode 100644
index 0000000..4ca9999
--- /dev/null
+++ b/db/migration/019-add-tunicwilds.sql
@@ -0,0 +1,11 @@
+CREATE TABLE "Tunicwild" (
+ "id" integer PRIMARY KEY AUTOINCREMENT,
+ "memberDiscord" text,
+ "composer" text NOT NULL,
+ "title" text NOT NULL,
+ "game" text NOT NULL,
+ "releaseDate" text NOT NULL,
+ "extraHint" text NOT NULL,
+ "deleted" integer NOT NULL DEFAULT 0,
+ FOREIGN KEY ("memberDiscord") REFERENCES "Member" ("discord")
+);
From 1b9995694b2b3495fc99089e5e33ac5271ce54ba Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 14 Jun 2025 04:49:50 -0600
Subject: [PATCH 004/108] only load svelte component if songdata exists
---
src/pages/tunicwilds.astro | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/tunicwilds.astro b/src/pages/tunicwilds.astro
index e6c361b..37463b5 100644
--- a/src/pages/tunicwilds.astro
+++ b/src/pages/tunicwilds.astro
@@ -32,5 +32,5 @@ const songData =
title="Tunicwilds"
description="CAN YOU GUESS THE SONG? CAN YOU GUESS THE GAME? CAN YOU GUESS THE TUNICWILDS?"
>
-
+ {songData && }
From e78fe3437b7e6904fe569c8b428f90327e775db6 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sun, 15 Jun 2025 02:15:41 -0600
Subject: [PATCH 005/108] move to folder and add check for existing song for
game
---
.../{tunicwilds.ts => tunicwilds/index.ts} | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
rename src/pages/api/{tunicwilds.ts => tunicwilds/index.ts} (86%)
diff --git a/src/pages/api/tunicwilds.ts b/src/pages/api/tunicwilds/index.ts
similarity index 86%
rename from src/pages/api/tunicwilds.ts
rename to src/pages/api/tunicwilds/index.ts
index 44449e8..40c1482 100644
--- a/src/pages/api/tunicwilds.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -1,11 +1,11 @@
import { LibsqlError } from "@libsql/client";
import type { APIRoute } from "astro";
import { eq, type InferSelectModel } from "drizzle-orm";
-import { jsonError, jsonResponse } from "../../server/responses";
+import { jsonError, jsonResponse } from "../../../server/responses";
import { createWriteStream, ReadStream } from "fs";
-import { thingDeletion, thingGet } from "../../server/thingUtils";
-import db, { retryIfDbBusy } from "../../database/db";
-import { Member, Tunicwild } from "../../database/schema";
+import { thingDeletion, thingGet } from "../../../server/thingUtils";
+import db, { retryIfDbBusy } from "../../../database/db";
+import { Member, Tunicwild } from "../../../database/schema";
export const prerender = false;
@@ -82,6 +82,17 @@ export const POST: APIRoute = async (context) => {
return jsonError("File must be an OGG, Opus, WAV, or MP3 file");
}
+ // Check if a Tunicwild with the same title and game already exists
+ const existingTunicwild = await db
+ .select()
+ .from(Tunicwild)
+ .where(eq(Tunicwild.title, title))
+ .and(eq(Tunicwild.game, game))
+ .get();
+ if (existingTunicwild) {
+ return jsonError("A Tunicwild with the same title and game already exists");
+ }
+
// Store to DB
let tunicwild: InferSelectModel;
try {
From e9f609f1279fe15215f0345ff844d95aadf79d33 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sun, 15 Jun 2025 02:21:53 -0600
Subject: [PATCH 006/108] fix query
---
src/pages/api/tunicwilds/index.ts | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts
index 40c1482..9a7137e 100644
--- a/src/pages/api/tunicwilds/index.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -1,6 +1,6 @@
import { LibsqlError } from "@libsql/client";
import type { APIRoute } from "astro";
-import { eq, type InferSelectModel } from "drizzle-orm";
+import { and, eq, type InferSelectModel } from "drizzle-orm";
import { jsonError, jsonResponse } from "../../../server/responses";
import { createWriteStream, ReadStream } from "fs";
import { thingDeletion, thingGet } from "../../../server/thingUtils";
@@ -86,8 +86,12 @@ export const POST: APIRoute = async (context) => {
const existingTunicwild = await db
.select()
.from(Tunicwild)
- .where(eq(Tunicwild.title, title))
- .and(eq(Tunicwild.game, game))
+ .where(
+ and(
+ eq(Tunicwild.title, title),
+ eq(Tunicwild.game, game)
+ )
+ )
.get();
if (existingTunicwild) {
return jsonError("A Tunicwild with the same title and game already exists");
From ca87328dfb19b3aa1e03e9312cb05f30134142fc Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sun, 15 Jun 2025 02:23:09 -0600
Subject: [PATCH 007/108] metadata endpoint with pagination
---
src/pages/api/tunicwilds/metadata.ts | 40 ++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)
create mode 100644 src/pages/api/tunicwilds/metadata.ts
diff --git a/src/pages/api/tunicwilds/metadata.ts b/src/pages/api/tunicwilds/metadata.ts
new file mode 100644
index 0000000..cb5066a
--- /dev/null
+++ b/src/pages/api/tunicwilds/metadata.ts
@@ -0,0 +1,40 @@
+import type { APIRoute } from "astro";
+import { eq, type SQLWrapper } from "drizzle-orm";
+import { jsonError } from "../../../server/responses";
+import { Tunicwild } from "../../../database/schema";
+import { paginationQuery, parseNumberCursor } from "../../../server/pagination";
+
+export const prerender = false;
+
+export const GET: APIRoute = async ({ url }) => {
+ const conditions: SQLWrapper[] = [];
+ const params = url.searchParams;
+
+ const game = params.get("game");
+ if (game)
+ conditions.push(eq(Tunicwild.game, game));
+
+ const composer = params.get("composer");
+ if (composer)
+ conditions.push(eq(Tunicwild.composer, composer));
+
+ const title = params.get("title");
+ if (title)
+ conditions.push(eq(Tunicwild.title, title));
+
+ const releaseDate = params.get("releaseDate");
+ if (releaseDate) {
+ const date = new Date(releaseDate);
+ if (isNaN(date.getTime()))
+ return jsonError("Invalid release date");
+ conditions.push(eq(Tunicwild.releaseDate, date));
+ }
+
+ return paginationQuery(
+ params,
+ Tunicwild,
+ "id",
+ parseNumberCursor,
+ ...conditions,
+ );
+}
\ No newline at end of file
From eaa3b87da1cc8ad33839ac6bf98c29f6096dc8fc Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Mon, 16 Jun 2025 21:02:16 -0600
Subject: [PATCH 008/108] bypass hmac for tunicwilds too
---
src/middleware.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/middleware.ts b/src/middleware.ts
index dde5b8f..ddf586e 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -9,7 +9,11 @@ const checkHmacForApi = defineMiddleware(async (context, next) => {
return next();
}
- if (context.url.pathname.startsWith("/api/action") && context.request.method === "GET") {
+ if (
+ (
+ context.url.pathname.startsWith("/api/action") ||
+ context.url.pathname.startsWith("/api/tunicwild")
+ ) && context.request.method === "GET") {
return next();
}
From 10a243f3b07598e1e7f38b97aa08d1482b69062c Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Tue, 17 Jun 2025 17:35:53 -0600
Subject: [PATCH 009/108] add tunicwildsurl env
---
development.sh | 1 +
1 file changed, 1 insertion(+)
diff --git a/development.sh b/development.sh
index c4b34ba..c0686aa 100755
--- a/development.sh
+++ b/development.sh
@@ -11,6 +11,7 @@ database='dev/latex.db'
export CORPORATE_URL=
export DATABASE_URL="file:./$database"
+export TUNICWILDS_URL=
export DIGITALOCEAN_DNS_TOKEN=
export LATEX_WEB_SYSTEMD_UNIT=
export SECRET_HMAC_KEY='dev'
From 5e398d6ecef282294b09be87c76314d0441183b9 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Tue, 17 Jun 2025 18:29:58 -0600
Subject: [PATCH 010/108] remove member tunicwild relation and add seed
---
db/migration/019-add-tunicwilds.sql | 3 ---
src/database/factories.ts | 1 -
src/database/schema.ts | 12 +-----------
src/database/seed.ts | 4 +++-
src/pages/api/tunicwilds/index.ts | 27 ++-------------------------
src/server/thingUtils.ts | 3 +--
6 files changed, 7 insertions(+), 43 deletions(-)
diff --git a/db/migration/019-add-tunicwilds.sql b/db/migration/019-add-tunicwilds.sql
index 4ca9999..e978c57 100644
--- a/db/migration/019-add-tunicwilds.sql
+++ b/db/migration/019-add-tunicwilds.sql
@@ -1,11 +1,8 @@
CREATE TABLE "Tunicwild" (
"id" integer PRIMARY KEY AUTOINCREMENT,
- "memberDiscord" text,
"composer" text NOT NULL,
"title" text NOT NULL,
"game" text NOT NULL,
"releaseDate" text NOT NULL,
"extraHint" text NOT NULL,
- "deleted" integer NOT NULL DEFAULT 0,
- FOREIGN KEY ("memberDiscord") REFERENCES "Member" ("discord")
);
diff --git a/src/database/factories.ts b/src/database/factories.ts
index 0fcee37..4d85dcd 100644
--- a/src/database/factories.ts
+++ b/src/database/factories.ts
@@ -91,7 +91,6 @@ export const WordFactory = defineFactory(Word, {
});
export const TunicwildFactory = defineFactory(Tunicwild, {
- memberDiscord: () => { throw new Error("Not implemented"); },
composer: () => faker.person.firstName(),
title: () => faker.music.songName(),
game: () => faker.commerce.productName(),
diff --git a/src/database/schema.ts b/src/database/schema.ts
index c4284f2..d2cd1b6 100644
--- a/src/database/schema.ts
+++ b/src/database/schema.ts
@@ -64,7 +64,6 @@ export const MemberRelations = relations(Member, ({ many }) => ({
sounds: many(Sound),
tickets: many(Ticket),
words: many(Word),
- tunicwilds: many(Tunicwild),
}));
export const Motion = sqliteTable("Motion", {
@@ -167,18 +166,9 @@ export const WordRelations = relations(Word, ({ one }) => ({
export const Tunicwild = sqliteTable("Tunicwild", {
id: integer().primaryKey({ autoIncrement: true }),
- memberDiscord: text().notNull().references(() => Member.discord),
composer: text().notNull(),
title: text().notNull(),
game: text().notNull(),
releaseDate: date().notNull(),
extraHint: text().notNull(),
- deleted: integer({ mode: "boolean" }).default(false).notNull(),
-});
-
-export const TunicwildRelations = relations(Tunicwild, ({ one }) => ({
- member: one(Member, {
- fields: [Tunicwild.memberDiscord],
- references: [Member.discord],
- }),
-}));
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/src/database/seed.ts b/src/database/seed.ts
index 95dc4fe..3ca5547 100644
--- a/src/database/seed.ts
+++ b/src/database/seed.ts
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
-import { ActionFactory, ActionItemFactory, MemberFactory, MotionFactory, SightFactory, SoundFactory, WordFactory } from "./factories";
+import { ActionFactory, ActionItemFactory, MemberFactory, MotionFactory, SightFactory, SoundFactory, TunicwildFactory, WordFactory } from "./factories";
export default async function seed() {
const members = await new MemberFactory().count(20).create();
@@ -43,4 +43,6 @@ export default async function seed() {
await new WordFactory().count(10).create({
memberDiscord: () => faker.helpers.arrayElement(members.map((member) => member.discord)),
});
+
+ await new TunicwildFactory().count(10).create();
}
diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts
index 9a7137e..1f18ee0 100644
--- a/src/pages/api/tunicwilds/index.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -3,14 +3,11 @@ import type { APIRoute } from "astro";
import { and, eq, type InferSelectModel } from "drizzle-orm";
import { jsonError, jsonResponse } from "../../../server/responses";
import { createWriteStream, ReadStream } from "fs";
-import { thingDeletion, thingGet } from "../../../server/thingUtils";
import db, { retryIfDbBusy } from "../../../database/db";
-import { Member, Tunicwild } from "../../../database/schema";
+import { Tunicwild } from "../../../database/schema";
export const prerender = false;
-export const GET: APIRoute = async (context) => thingGet(context, "tunicwilds");
-
export const POST: APIRoute = async (context) => {
if (!process.env.TUNICWILDS_DIRECTORY) {
return jsonError("TUNICWILDS_DIRECTORY not set", 500);
@@ -23,7 +20,6 @@ export const POST: APIRoute = async (context) => {
return jsonError("Request body must be form data");
}
- const discord = formData.get("discord");
const composer = formData.get("composer");
const title = formData.get("title");
const game = formData.get("game");
@@ -33,7 +29,6 @@ export const POST: APIRoute = async (context) => {
// Form validation
if (
- typeof discord !== "string" ||
typeof composer !== "string" ||
typeof title !== "string" ||
typeof game !== "string" ||
@@ -51,19 +46,6 @@ export const POST: APIRoute = async (context) => {
return jsonError("Release date cannot be in the future");
}
- const member = await db
- .select()
- .from(Member)
- .where(eq(Member.discord, discord))
- .get();
- if (!member) {
- return jsonError("Member does not exist");
- }
-
- if (member.deleted) {
- return jsonError("Member has been deleted");
- }
-
if (composer.length > 2 ** 10) {
return jsonError("Composer name is too long");
}
@@ -104,7 +86,6 @@ export const POST: APIRoute = async (context) => {
db
.insert(Tunicwild)
.values({
- memberDiscord: discord,
composer,
title,
game,
@@ -136,8 +117,4 @@ export const POST: APIRoute = async (context) => {
);
return jsonResponse(tunicwild);
-};
-
-export const PUT: APIRoute = async (context) => thingDeletion(context, "tunicwilds", false);
-
-export const DELETE: APIRoute = async (context) => thingDeletion(context, "tunicwilds", true);
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/src/server/thingUtils.ts b/src/server/thingUtils.ts
index 69ed14a..21177bc 100644
--- a/src/server/thingUtils.ts
+++ b/src/server/thingUtils.ts
@@ -4,7 +4,7 @@ import { and, eq } from "drizzle-orm";
import { jsonError, jsonResponse } from "./responses";
import { paginationQuery, parseNumberCursor } from "./pagination";
import db, { retryIfDbBusy } from "../database/db";
-import { Action, Motion, Sight, Sound, Tunicwild, Word } from "../database/schema";
+import { Action, Motion, Sight, Sound, Word } from "../database/schema";
const thingTypeToTable = {
words: Word,
@@ -12,7 +12,6 @@ const thingTypeToTable = {
motions: Motion,
sights: Sight,
actions: Action,
- tunicwilds: Tunicwild,
};
type ThingType = keyof typeof thingTypeToTable;
const thingTypes = Object.keys(thingTypeToTable);
From e58bb1b968d0fce44fea9d7031dfafc2dd6227b7 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Tue, 17 Jun 2025 18:32:52 -0600
Subject: [PATCH 011/108] endpoint for list of games
---
src/pages/api/tunicwilds/games.ts | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 src/pages/api/tunicwilds/games.ts
diff --git a/src/pages/api/tunicwilds/games.ts b/src/pages/api/tunicwilds/games.ts
new file mode 100644
index 0000000..8b74260
--- /dev/null
+++ b/src/pages/api/tunicwilds/games.ts
@@ -0,0 +1,16 @@
+import type { APIRoute } from "astro";
+import db from "../../../database/db";
+import { jsonResponse } from "../../../server/responses";
+import { Tunicwild } from "../../../database/schema";
+
+export const prerender = false;
+
+export const GET: APIRoute = async () => {
+ return jsonResponse(await db
+ .select({
+ game: Tunicwild.game
+ })
+ .from(Tunicwild)
+ .groupBy(Tunicwild.game)
+ .orderBy(Tunicwild.game));
+};
\ No newline at end of file
From 02e4c3b8ef6051ecfdac85f2612540c318d8ce19 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Tue, 17 Jun 2025 18:34:12 -0600
Subject: [PATCH 012/108] mvoe tunicwilds/metadata GET to tunicwilds GET
---
src/pages/api/tunicwilds/index.ts | 36 ++++++++++++++++++++++++-
src/pages/api/tunicwilds/metadata.ts | 40 ----------------------------
2 files changed, 35 insertions(+), 41 deletions(-)
delete mode 100644 src/pages/api/tunicwilds/metadata.ts
diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts
index 1f18ee0..5be05ca 100644
--- a/src/pages/api/tunicwilds/index.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -1,13 +1,47 @@
import { LibsqlError } from "@libsql/client";
import type { APIRoute } from "astro";
-import { and, eq, type InferSelectModel } from "drizzle-orm";
+import { and, eq, type InferSelectModel, type SQLWrapper } from "drizzle-orm";
import { jsonError, jsonResponse } from "../../../server/responses";
import { createWriteStream, ReadStream } from "fs";
import db, { retryIfDbBusy } from "../../../database/db";
import { Tunicwild } from "../../../database/schema";
+import { paginationQuery, parseNumberCursor } from "../../../server/pagination";
export const prerender = false;
+export const GET: APIRoute = async ({ url }) => {
+ const conditions: SQLWrapper[] = [];
+ const params = url.searchParams;
+
+ const game = params.get("game");
+ if (game)
+ conditions.push(eq(Tunicwild.game, game));
+
+ const composer = params.get("composer");
+ if (composer)
+ conditions.push(eq(Tunicwild.composer, composer));
+
+ const title = params.get("title");
+ if (title)
+ conditions.push(eq(Tunicwild.title, title));
+
+ const releaseDate = params.get("releaseDate");
+ if (releaseDate) {
+ const date = new Date(releaseDate);
+ if (isNaN(date.getTime()))
+ return jsonError("Invalid release date");
+ conditions.push(eq(Tunicwild.releaseDate, date));
+ }
+
+ return paginationQuery(
+ params,
+ Tunicwild,
+ "id",
+ parseNumberCursor,
+ ...conditions,
+ );
+}
+
export const POST: APIRoute = async (context) => {
if (!process.env.TUNICWILDS_DIRECTORY) {
return jsonError("TUNICWILDS_DIRECTORY not set", 500);
diff --git a/src/pages/api/tunicwilds/metadata.ts b/src/pages/api/tunicwilds/metadata.ts
deleted file mode 100644
index cb5066a..0000000
--- a/src/pages/api/tunicwilds/metadata.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { APIRoute } from "astro";
-import { eq, type SQLWrapper } from "drizzle-orm";
-import { jsonError } from "../../../server/responses";
-import { Tunicwild } from "../../../database/schema";
-import { paginationQuery, parseNumberCursor } from "../../../server/pagination";
-
-export const prerender = false;
-
-export const GET: APIRoute = async ({ url }) => {
- const conditions: SQLWrapper[] = [];
- const params = url.searchParams;
-
- const game = params.get("game");
- if (game)
- conditions.push(eq(Tunicwild.game, game));
-
- const composer = params.get("composer");
- if (composer)
- conditions.push(eq(Tunicwild.composer, composer));
-
- const title = params.get("title");
- if (title)
- conditions.push(eq(Tunicwild.title, title));
-
- const releaseDate = params.get("releaseDate");
- if (releaseDate) {
- const date = new Date(releaseDate);
- if (isNaN(date.getTime()))
- return jsonError("Invalid release date");
- conditions.push(eq(Tunicwild.releaseDate, date));
- }
-
- return paginationQuery(
- params,
- Tunicwild,
- "id",
- parseNumberCursor,
- ...conditions,
- );
-}
\ No newline at end of file
From 5ddeda0394f3c8339105e40132d6686723a7dbc3 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Tue, 17 Jun 2025 21:52:03 -0600
Subject: [PATCH 013/108] idk refactor endpoint for batch + single uploads
---
src/pages/api/tunicwilds/index.ts | 156 ++++++++++++++++++++++--------
1 file changed, 115 insertions(+), 41 deletions(-)
diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts
index 5be05ca..4654343 100644
--- a/src/pages/api/tunicwilds/index.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -43,7 +43,7 @@ export const GET: APIRoute = async ({ url }) => {
}
export const POST: APIRoute = async (context) => {
- if (!process.env.TUNICWILDS_DIRECTORY) {
+ if (process.env.NODE_ENV === "development" && !process.env.TUNICWILDS_DIRECTORY) {
return jsonError("TUNICWILDS_DIRECTORY not set", 500);
}
@@ -61,56 +61,81 @@ export const POST: APIRoute = async (context) => {
const extraHint = formData.get("extraHint");
const file = formData.get("file") as File;
- // Form validation
- if (
- typeof composer !== "string" ||
- typeof title !== "string" ||
- typeof game !== "string" ||
- typeof releaseDate !== "string" ||
- typeof extraHint !== "string" ||
- !(file instanceof File)
- ) {
- return jsonError("Invalid form params");
+ if (typeof composer !== "string" || !composer.trim()) {
+ return jsonError("Composer is required");
}
+ if (typeof title !== "string" || !title.trim()) {
+ return jsonError("Title is required");
+ }
+ if (typeof game !== "string" || !game.trim()) {
+ return jsonError("Game is required");
+ }
+ if (typeof releaseDate !== "string" || !releaseDate.trim()) {
+ return jsonError("Release date is required");
+ }
+ if (typeof extraHint !== "string") {
+ return jsonError("Extra hint is required");
+ }
+ if (!(file instanceof File) || file.size === 0) {
+ return jsonError("Valid audio file is required");
+ }
+
+ // Parse and validate release date
const releaseDateParsed = new Date(releaseDate);
if (isNaN(releaseDateParsed.getTime())) {
- return jsonError("Invalid release date");
+ return jsonError("Invalid release date format. Use YYYY-MM-DD or ISO format");
}
if (releaseDateParsed.getTime() > Date.now()) {
return jsonError("Release date cannot be in the future");
}
- if (composer.length > 2 ** 10) {
- return jsonError("Composer name is too long");
+ // Length validation with better limits
+ const MAX_LENGTH = 2 ** 10;
+ if (composer.length > MAX_LENGTH) {
+ return jsonError(`Composer name too long (max ${MAX_LENGTH} characters)`);
+ }
+ if (title.length > MAX_LENGTH) {
+ return jsonError(`Title too long (max ${MAX_LENGTH} characters)`);
}
- if (title.length > 2 ** 10) {
- return jsonError("Title is too long");
+ if (game.length > MAX_LENGTH) {
+ return jsonError(`Game name too long (max ${MAX_LENGTH} characters)`);
}
- if (game.length > 2 ** 10) {
- return jsonError("Game name is too long");
+ if (extraHint.length > MAX_LENGTH) {
+ return jsonError(`Extra hint too long (max ${MAX_LENGTH} characters)`);
}
- if (extraHint.length > 2 ** 10) {
- return jsonError("Extra hint is too long");
+
+ // File validation with size limits
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
+ if (file.size > MAX_FILE_SIZE) {
+ return jsonError("File too large (max 50MB)");
}
- // File validation
- if (!file.name.endsWith(".ogg") && !file.name.endsWith(".opus") && !file.name.endsWith(".wav") && !file.name.endsWith(".mp3")) {
+ // More robust file type checking
+ const allowedExtensions = [".ogg", ".opus", ".wav", ".mp3"];
+ const allowedMimeTypes = ["audio/ogg", "audio/opus", "audio/wav", "audio/mpeg", "audio/mp3"];
+
+ const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
+ const isValidExtension = allowedExtensions.includes(fileExtension);
+ const isValidMimeType = allowedMimeTypes.includes(file.type.toLowerCase());
+
+ if (!isValidExtension && !isValidMimeType) {
return jsonError("File must be an OGG, Opus, WAV, or MP3 file");
}
- // Check if a Tunicwild with the same title and game already exists
+ // Check for duplicates with case-insensitive comparison
const existingTunicwild = await db
.select()
.from(Tunicwild)
.where(
and(
- eq(Tunicwild.title, title),
- eq(Tunicwild.game, game)
+ eq(Tunicwild.title, title.trim()),
+ eq(Tunicwild.game, game.trim())
)
)
.get();
+
if (existingTunicwild) {
- return jsonError("A Tunicwild with the same title and game already exists");
+ return jsonError(`A song with title "${title}" already exists in game "${game}"`);
}
// Store to DB
@@ -120,11 +145,11 @@ export const POST: APIRoute = async (context) => {
db
.insert(Tunicwild)
.values({
- composer,
- title,
- game,
+ composer: composer.trim(),
+ title: title.trim(),
+ game: game.trim(),
releaseDate: releaseDateParsed,
- extraHint,
+ extraHint: extraHint.trim(),
})
.returning()
.get()
@@ -132,23 +157,72 @@ export const POST: APIRoute = async (context) => {
} catch (error) {
if (error instanceof LibsqlError) {
if (error.code === "SQLITE_CONSTRAINT_UNIQUE") {
- return jsonError("Word already exists");
+ return jsonError("A song with this title and game already exists");
}
-
if (error.code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
- return jsonError(
- "Invalid Discord ID; member does not exist; probably needs to join first",
- );
+ return jsonError("Invalid foreign key constraint");
}
}
-
- throw error;
+ console.error("Database error:", error);
+ return jsonError("Failed to save song to database", 500);
}
- // Upload files
- ReadStream.fromWeb(file.stream()).pipe(
- createWriteStream(`${process.env.TUNICWILDS_DIRECTORY}/${tunicwild.id}.${file.name.split('.').pop()}`),
- );
+ // Upload files with better error handling
+ try {
+ if (process.env.NODE_ENV === "development") {
+ // Get file extension properly
+ const extension = fileExtension.substring(1); // Remove the dot
+ const filePath = `${process.env.TUNICWILDS_DIRECTORY}/${tunicwild.id}.${extension}`;
+
+ const fileStream = file.stream();
+ const writeStream = createWriteStream(filePath);
+
+ // Convert web stream to node stream properly
+ const reader = fileStream.getReader();
+ const pump = async () => {
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ writeStream.write(Buffer.from(value));
+ }
+ writeStream.end();
+ } catch (error) {
+ writeStream.destroy();
+ throw error;
+ }
+ };
+
+ await pump();
+ } else {
+ const uploadResponse = await fetch(
+ `${process.env.TUNICWILDS_URL}/upload/${encodeURIComponent(game)}/${encodeURIComponent(title)}`,
+ {
+ method: "POST",
+ body: file.stream(),
+ headers: {
+ "Content-Type": file.type || "audio/mpeg",
+ "Content-Length": file.size.toString(),
+ },
+ }
+ );
+
+ if (!uploadResponse.ok) {
+ // Rollback database entry if file upload fails
+ await db.delete(Tunicwild).where(eq(Tunicwild.id, tunicwild.id));
+ return jsonError(`Failed to upload file: ${uploadResponse.statusText}`, 500);
+ }
+ }
+ } catch (error) {
+ console.error("File upload error:", error);
+ // Rollback database entry if file upload fails
+ try {
+ await db.delete(Tunicwild).where(eq(Tunicwild.id, tunicwild.id));
+ } catch (rollbackError) {
+ console.error("Failed to rollback database entry:", rollbackError);
+ }
+ return jsonError("Failed to upload audio file", 500);
+ }
return jsonResponse(tunicwild);
};
\ No newline at end of file
From 8c81b87133cb5b91f31ba42abbc37bffcc30048a Mon Sep 17 00:00:00 2001
From: clayton
Date: Tue, 17 Jun 2025 22:46:59 -0700
Subject: [PATCH 014/108] Rename vars
---
development.sh | 10 +++++-----
src/pages/api/tunicwilds/index.ts | 10 +++++-----
src/pages/tunicwilds.astro | 4 ++--
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/development.sh b/development.sh
index c0686aa..9010260 100755
--- a/development.sh
+++ b/development.sh
@@ -11,27 +11,27 @@ database='dev/latex.db'
export CORPORATE_URL=
export DATABASE_URL="file:./$database"
-export TUNICWILDS_URL=
export DIGITALOCEAN_DNS_TOKEN=
export LATEX_WEB_SYSTEMD_UNIT=
+export PUBLIC_DIRECTORY='public'
export SECRET_HMAC_KEY='dev'
export SIGHTS_RUN_AFTER_UPLOAD=
export SIGHTS_UPLOAD_DIRECTORY='dev/sights'
export SOUNDS_RUN_AFTER_UPLOAD=
export SOUNDS_UPLOAD_DIRECTORY='dev/sounds'
+export TUNICWILDS_UPLOAD_DIRECTORY='dev/tunicwilds'
+export TUNICWILDS_UPLOAD_URL=
export WORDS_RUN_AFTER_UPLOAD=
export WORDS_UPLOAD_DIRECTORY='dev/words'
-export TUNICWILDS_DIRECTORY='dev/tunicwilds'
-export PUBLIC_DIRECTORY='public'
# Clean up files from last run
-rm -rf "$database" "$SIGHTS_UPLOAD_DIRECTORY" "$SOUNDS_UPLOAD_DIRECTORY" "$WORDS_UPLOAD_DIRECTORY" "$TUNICWILDS_DIRECTORY"
+rm -rf "$database" "$SIGHTS_UPLOAD_DIRECTORY" "$SOUNDS_UPLOAD_DIRECTORY" "$WORDS_UPLOAD_DIRECTORY" "$TUNICWILDS_UPLOAD_DIRECTORY"
# Create directories
mkdir -p "$SIGHTS_UPLOAD_DIRECTORY"
mkdir -p "$SOUNDS_UPLOAD_DIRECTORY"
mkdir -p "$WORDS_UPLOAD_DIRECTORY"
-mkdir -p "$TUNICWILDS_DIRECTORY"
+mkdir -p "$TUNICWILDS_UPLOAD_DIRECTORY"
# Create database
node db/migrate.mjs "$database"
diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts
index 4654343..f0c6437 100644
--- a/src/pages/api/tunicwilds/index.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -43,8 +43,8 @@ export const GET: APIRoute = async ({ url }) => {
}
export const POST: APIRoute = async (context) => {
- if (process.env.NODE_ENV === "development" && !process.env.TUNICWILDS_DIRECTORY) {
- return jsonError("TUNICWILDS_DIRECTORY not set", 500);
+ if (process.env.NODE_ENV === "development" && !process.env.TUNICWILDS_UPLOAD_DIRECTORY) {
+ return jsonError("TUNICWILDS_UPLOAD_DIRECTORY not set", 500);
}
let formData: FormData;
@@ -172,7 +172,7 @@ export const POST: APIRoute = async (context) => {
if (process.env.NODE_ENV === "development") {
// Get file extension properly
const extension = fileExtension.substring(1); // Remove the dot
- const filePath = `${process.env.TUNICWILDS_DIRECTORY}/${tunicwild.id}.${extension}`;
+ const filePath = `${process.env.TUNICWILDS_UPLOAD_DIRECTORY}/${tunicwild.id}.${extension}`;
const fileStream = file.stream();
const writeStream = createWriteStream(filePath);
@@ -196,7 +196,7 @@ export const POST: APIRoute = async (context) => {
await pump();
} else {
const uploadResponse = await fetch(
- `${process.env.TUNICWILDS_URL}/upload/${encodeURIComponent(game)}/${encodeURIComponent(title)}`,
+ `${process.env.TUNICWILDS_UPLOAD_URL}/upload/${encodeURIComponent(game)}/${encodeURIComponent(title)}`,
{
method: "POST",
body: file.stream(),
@@ -225,4 +225,4 @@ export const POST: APIRoute = async (context) => {
}
return jsonResponse(tunicwild);
-};
\ No newline at end of file
+};
diff --git a/src/pages/tunicwilds.astro b/src/pages/tunicwilds.astro
index 37463b5..e4db278 100644
--- a/src/pages/tunicwilds.astro
+++ b/src/pages/tunicwilds.astro
@@ -6,10 +6,10 @@ import { Tunicwild } from "../database/schema";
export const prerender = false;
-if (!process.env.TUNICWILDS_DIRECTORY) {
+if (!process.env.TUNICWILDS_UPLOAD_DIRECTORY) {
Astro.response.status = 500;
Astro.response.statusText =
- "TUNICWILDS_DIRECTORY environment variable is not set";
+ "TUNICWILDS_UPLOAD_DIRECTORY environment variable is not set";
}
const startDate = new Date("2025-06-14");
From 2cc0f3253d31a6f39b4e803424d156288f2d45d0 Mon Sep 17 00:00:00 2001
From: clayton
Date: Tue, 17 Jun 2025 22:58:11 -0700
Subject: [PATCH 015/108] Check for both env vars
---
src/pages/api/tunicwilds/index.ts | 8 ++++++--
src/pages/tunicwilds.astro | 10 ++++++----
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts
index f0c6437..c23e322 100644
--- a/src/pages/api/tunicwilds/index.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -43,8 +43,12 @@ export const GET: APIRoute = async ({ url }) => {
}
export const POST: APIRoute = async (context) => {
- if (process.env.NODE_ENV === "development" && !process.env.TUNICWILDS_UPLOAD_DIRECTORY) {
- return jsonError("TUNICWILDS_UPLOAD_DIRECTORY not set", 500);
+ if (
+ import.meta.env.DEV
+ ? !process.env.TUNICWILDS_UPLOAD_DIRECTORY
+ : !process.env.TUNICWILDS_UPLOAD_URL
+ ) {
+ return jsonError("TUNICWILDS env not set", 500);
}
let formData: FormData;
diff --git a/src/pages/tunicwilds.astro b/src/pages/tunicwilds.astro
index e4db278..b894998 100644
--- a/src/pages/tunicwilds.astro
+++ b/src/pages/tunicwilds.astro
@@ -6,10 +6,12 @@ import { Tunicwild } from "../database/schema";
export const prerender = false;
-if (!process.env.TUNICWILDS_UPLOAD_DIRECTORY) {
- Astro.response.status = 500;
- Astro.response.statusText =
- "TUNICWILDS_UPLOAD_DIRECTORY environment variable is not set";
+if (
+ import.meta.env.DEV
+ ? !process.env.TUNICWILDS_UPLOAD_DIRECTORY
+ : !process.env.TUNICWILDS_UPLOAD_URL
+) {
+ return new Response("TUNICWILDS env not set", { status: 500 });
}
const startDate = new Date("2025-06-14");
From da2c6e530d41c4f711daa69de65a17355374d72b Mon Sep 17 00:00:00 2001
From: clayton
Date: Tue, 17 Jun 2025 22:58:27 -0700
Subject: [PATCH 016/108] client:load for TunicwildsApp
---
src/pages/tunicwilds.astro | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/tunicwilds.astro b/src/pages/tunicwilds.astro
index b894998..a41ee3c 100644
--- a/src/pages/tunicwilds.astro
+++ b/src/pages/tunicwilds.astro
@@ -34,5 +34,5 @@ const songData =
title="Tunicwilds"
description="CAN YOU GUESS THE SONG? CAN YOU GUESS THE GAME? CAN YOU GUESS THE TUNICWILDS?"
>
- {songData && }
+ {songData && }
From ab5b2f9f080acbaa680a7a0c8e03db04c50df9dc Mon Sep 17 00:00:00 2001
From: clayton
Date: Wed, 18 Jun 2025 13:02:49 -0700
Subject: [PATCH 017/108] Better type for songList and fetch songData from
client
---
src/components/Tunicwilds/Tunicwilds.svelte | 303 ++++++++++----------
1 file changed, 159 insertions(+), 144 deletions(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index 32e059f..d90995e 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -1,22 +1,15 @@
@@ -193,6 +207,34 @@
+ {#if showSongList}
+
+
+
Song List
+
+ {#each Object.entries(gameGroupedSongList) as [game, songs]}
+
+ {/each}
+
+
+ {/if}
+
{#if songData == null}
Loading today's song...
{:else}
@@ -383,6 +425,18 @@
margin-bottom: 0.5rem;
}
+ .song-list-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
.game-over {
backdrop-filter: blur(10px);
padding: 1.5rem;
From b175e3e31dc627cbd7c0e1c50d1b0bff85ed7875 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Thu, 19 Jun 2025 00:57:14 -0600
Subject: [PATCH 029/108] remove extra comma
---
db/migration/019-add-tunicwilds.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/db/migration/019-add-tunicwilds.sql b/db/migration/019-add-tunicwilds.sql
index 5a35430..e9b52a4 100644
--- a/db/migration/019-add-tunicwilds.sql
+++ b/db/migration/019-add-tunicwilds.sql
@@ -5,5 +5,5 @@ CREATE TABLE "Tunicwild" (
"game" text NOT NULL,
"releaseDate" text NOT NULL,
"officialLink" text NOT NULL,
- "extraHint" text NOT NULL,
+ "extraHint" text NOT NULL
);
From db02058bd9f2fc0b4088f5b70279016fe0679530 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Thu, 19 Jun 2025 01:03:04 -0600
Subject: [PATCH 030/108] error handling
---
src/components/Tunicwilds/Tunicwilds.svelte | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index 4a3ea20..c8a5b34 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -22,6 +22,7 @@
let songData = $state() as SongData & { audioUrl: string };
let guesses: ({ title: string; game: string } | false)[] = $state([]); // False for skips
let currentGuess = $state("");
+ let error = $state("");
let gameWon = $state(false);
let gameLost = $state(false);
let isPlaying = $state(false);
@@ -69,7 +70,12 @@
else showDropdown = false;
});
- getSongData().then((value) => (songData = value));
+ getSongData()
+ .then((value) => (songData = value))
+ .catch((err) => {
+ console.error("Failed to fetch song data:", err);
+ error = `${err.message}`;
+ });
function getSongData(): Promise {
const adjustedTimestamp =
@@ -235,7 +241,9 @@
{/if}
- {#if songData == null}
+ {#if error}
+ Error: {error}
+ {:else if songData == null}
Loading today's song...
{:else}
From 3c2e9811e791af1143bee191858d6e38b2f2f759 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Thu, 19 Jun 2025 01:08:38 -0600
Subject: [PATCH 031/108] things list addition
---
src/pages/things.astro | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/things.astro b/src/pages/things.astro
index 635c38b..53ae30c 100644
--- a/src/pages/things.astro
+++ b/src/pages/things.astro
@@ -12,6 +12,7 @@ import Layout from "../layouts/Layout.astro";
features/content
From ab68464445d1f438006c5f0921d56c18bfd2b116 Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 00:19:41 -0700
Subject: [PATCH 032/108] Add route to submit new guess
---
src/pages/tunicwilds/today.ts | 53 ++++++++++++++++++++++++++++++++++-
1 file changed, 52 insertions(+), 1 deletion(-)
diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts
index e3aa38f..5f99c3d 100644
--- a/src/pages/tunicwilds/today.ts
+++ b/src/pages/tunicwilds/today.ts
@@ -10,7 +10,7 @@ declare global {
interface SessionData {
tunicwilds?: Record;
}
}
@@ -75,3 +75,54 @@ export const GET: APIRoute = async (context) => {
return jsonResponse(responseBody);
};
+
+export const POST: APIRoute = async (context) => {
+ const params = await context.request.json();
+
+ if (
+ typeof params.timestamp !== "number" ||
+ (params.guess != null && typeof params.guess !== "number")
+ ) {
+ return jsonError("Invalid body");
+ }
+
+ const date = new Date(params.timestamp);
+
+ if (Number.isNaN(date.getTime())) {
+ return jsonError("Invalid timestamp");
+ }
+
+ // TODO map from date strings to song IDs
+ const dailyTunicwildIds: Record = {};
+
+ const tunicwildDateString = date.toISOString().slice(0, 10);
+ const tunicwildId = dailyTunicwildIds[tunicwildDateString];
+
+ if (tunicwildId == null) {
+ return jsonError("Invalid date. Check if your system clock is set correctly");
+ }
+
+ let tunicwildsSession = context.locals.session.data.tunicwilds?.[tunicwildDateString];
+
+ if (tunicwildsSession == null) {
+ context.locals.session.data.tunicwilds ??= {};
+ tunicwildsSession = context.locals.session.data.tunicwilds[tunicwildDateString] = {
+ complete: false,
+ guesses: [],
+ };
+ }
+
+ if (params.guess === tunicwildId) {
+ tunicwildsSession.complete = true;
+ } else if (params.guess != null && !await db.$count(Tunicwild, eq(Tunicwild.id, params.guess))) {
+ return jsonError("Invalid guess");
+ }
+
+ tunicwildsSession.guesses.push(params.guess);
+
+ const url = new URL(context.url);
+ url.searchParams.set("timestamp", params.timestamp.toString());
+
+ // TODO maybe 302 instead
+ return context.rewrite(url);
+};
From b69addc76f6e4008976b25a41d65405ca34f613d Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 00:20:15 -0700
Subject: [PATCH 033/108] Return session in tunicwilds today response
---
src/pages/tunicwilds/today.ts | 41 ++++++++++++++++++++---------------
1 file changed, 24 insertions(+), 17 deletions(-)
diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts
index 5f99c3d..eb852fa 100644
--- a/src/pages/tunicwilds/today.ts
+++ b/src/pages/tunicwilds/today.ts
@@ -48,28 +48,35 @@ export const GET: APIRoute = async (context) => {
throw new Error("Missing daily tunicwild in database");
}
- const tunicwildsSession = context.locals.session.data.tunicwilds?.[tunicwildDateString];
- const responseBody: Partial> & { audioUrl: string } = {
- audioUrl: getAudioUrl(tunicwild.id, audioLengths[tunicwildsSession?.guesses.length ?? 0] ?? audioLengths[0]),
+ const tunicwildsSession = context.locals.session.data.tunicwilds?.[tunicwildDateString] ?? {
+ complete: false,
+ guesses: [],
+ };
+ const responseBody: {
+ session: Required["tunicwilds"][string];
+ tunicwild: Partial> & { audioUrl: string };
+ } = {
+ session: tunicwildsSession,
+ tunicwild: {
+ audioUrl: getAudioUrl(tunicwild.id, audioLengths[tunicwildsSession.guesses.length] ?? audioLengths[0]),
+ },
};
- if (tunicwildsSession != null) {
- if (tunicwildsSession.complete) {
- Object.assign(responseBody, tunicwild);
- } else {
- const guessCount = tunicwildsSession.guesses.length;
+ if (tunicwildsSession.complete) {
+ Object.assign(responseBody.tunicwild, tunicwild);
+ } else {
+ const guessCount = tunicwildsSession.guesses.length;
- if (guessCount >= 3) {
- responseBody.releaseDate = tunicwild.releaseDate;
- }
+ if (guessCount >= 3) {
+ responseBody.tunicwild.releaseDate = tunicwild.releaseDate;
+ }
- if (guessCount >= 4) {
- responseBody.extraHint = tunicwild.extraHint;
- }
+ if (guessCount >= 4) {
+ responseBody.tunicwild.extraHint = tunicwild.extraHint;
+ }
- if (guessCount >= 5) {
- responseBody.game = tunicwild.game;
- }
+ if (guessCount >= 5) {
+ responseBody.tunicwild.game = tunicwild.game;
}
}
From e2da7e0e8466952c86e33b0b699ea918cad19c8e Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 00:28:54 -0700
Subject: [PATCH 034/108] Fix songlist columns & type
---
src/components/Tunicwilds/Tunicwilds.svelte | 2 +-
src/pages/tunicwilds.astro | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index c8a5b34..910a730 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -4,7 +4,7 @@
type SongData = Pick<
InferSelectModel,
- "composer" | "game" | "title" | "officialLink"
+ "composer" | "game" | "id" | "officialLink" | "title"
>;
const { songList }: { songList: SongData[] } = $props();
diff --git a/src/pages/tunicwilds.astro b/src/pages/tunicwilds.astro
index 47f9f92..95e7c6e 100644
--- a/src/pages/tunicwilds.astro
+++ b/src/pages/tunicwilds.astro
@@ -16,8 +16,10 @@ if (
const songList = await db
.select({
+ id: Tunicwild.id,
composer: Tunicwild.composer,
game: Tunicwild.game,
+ officialLink: Tunicwild.officialLink,
title: Tunicwild.title,
})
.from(Tunicwild);
From b7a6e3a1dddc84b4490861169bb9824599e9c46e Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 00:54:32 -0700
Subject: [PATCH 035/108] Complete tunicwild session after 6 guesses
---
src/pages/tunicwilds/today.ts | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts
index eb852fa..674c4d3 100644
--- a/src/pages/tunicwilds/today.ts
+++ b/src/pages/tunicwilds/today.ts
@@ -109,6 +109,14 @@ export const POST: APIRoute = async (context) => {
return jsonError("Invalid date. Check if your system clock is set correctly");
}
+ if (
+ params.guess != null &&
+ params.guess !== tunicwildId &&
+ !await db.$count(Tunicwild, eq(Tunicwild.id, params.guess))
+ ) {
+ return jsonError("Invalid guess");
+ }
+
let tunicwildsSession = context.locals.session.data.tunicwilds?.[tunicwildDateString];
if (tunicwildsSession == null) {
@@ -119,14 +127,12 @@ export const POST: APIRoute = async (context) => {
};
}
- if (params.guess === tunicwildId) {
+ tunicwildsSession.guesses.push(params.guess);
+
+ if (params.guess === tunicwildId || tunicwildsSession.guesses.length >= 6) {
tunicwildsSession.complete = true;
- } else if (params.guess != null && !await db.$count(Tunicwild, eq(Tunicwild.id, params.guess))) {
- return jsonError("Invalid guess");
}
- tunicwildsSession.guesses.push(params.guess);
-
const url = new URL(context.url);
url.searchParams.set("timestamp", params.timestamp.toString());
From a26a643b31b68a13f17dd76fdd5aaa9f7376b75c Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 00:56:22 -0700
Subject: [PATCH 036/108] Don't allow submitting more guesses than max
---
src/pages/tunicwilds/today.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts
index 674c4d3..b8267fe 100644
--- a/src/pages/tunicwilds/today.ts
+++ b/src/pages/tunicwilds/today.ts
@@ -127,6 +127,10 @@ export const POST: APIRoute = async (context) => {
};
}
+ if (tunicwildsSession.complete || tunicwildsSession.guesses.length >= 6) {
+ return jsonError("Tunicwild has already been completed");
+ }
+
tunicwildsSession.guesses.push(params.guess);
if (params.guess === tunicwildId || tunicwildsSession.guesses.length >= 6) {
From 3d0c48a8460fee67ab7859779a498355f6172340 Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 01:03:47 -0700
Subject: [PATCH 037/108] Track win state instead of completed
---
src/pages/tunicwilds/today.ts | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts
index b8267fe..af167f8 100644
--- a/src/pages/tunicwilds/today.ts
+++ b/src/pages/tunicwilds/today.ts
@@ -9,8 +9,8 @@ export const prerender = false;
declare global {
interface SessionData {
tunicwilds?: Record;
}
}
@@ -49,8 +49,8 @@ export const GET: APIRoute = async (context) => {
}
const tunicwildsSession = context.locals.session.data.tunicwilds?.[tunicwildDateString] ?? {
- complete: false,
guesses: [],
+ result: null,
};
const responseBody: {
session: Required["tunicwilds"][string];
@@ -62,7 +62,7 @@ export const GET: APIRoute = async (context) => {
},
};
- if (tunicwildsSession.complete) {
+ if (tunicwildsSession.result != null) {
Object.assign(responseBody.tunicwild, tunicwild);
} else {
const guessCount = tunicwildsSession.guesses.length;
@@ -122,19 +122,21 @@ export const POST: APIRoute = async (context) => {
if (tunicwildsSession == null) {
context.locals.session.data.tunicwilds ??= {};
tunicwildsSession = context.locals.session.data.tunicwilds[tunicwildDateString] = {
- complete: false,
guesses: [],
+ result: null,
};
}
- if (tunicwildsSession.complete || tunicwildsSession.guesses.length >= 6) {
+ if (tunicwildsSession.result != null || tunicwildsSession.guesses.length >= 6) {
return jsonError("Tunicwild has already been completed");
}
tunicwildsSession.guesses.push(params.guess);
- if (params.guess === tunicwildId || tunicwildsSession.guesses.length >= 6) {
- tunicwildsSession.complete = true;
+ if (params.guess === tunicwildId) {
+ tunicwildsSession.result = true;
+ } else if (tunicwildsSession.guesses.length >= 6) {
+ tunicwildsSession.result = false;
}
const url = new URL(context.url);
From bb4df99880277322e625bb93f7dfd988633fbded Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 15:53:15 -0700
Subject: [PATCH 038/108] Add command to update daily tunicwild state
---
development.sh | 12 ++-
src/bin/start-tunicwild-day.ts | 156 +++++++++++++++++++++++++++++++++
2 files changed, 167 insertions(+), 1 deletion(-)
create mode 100644 src/bin/start-tunicwild-day.ts
diff --git a/development.sh b/development.sh
index 9010260..6245c64 100755
--- a/development.sh
+++ b/development.sh
@@ -19,18 +19,28 @@ export SIGHTS_RUN_AFTER_UPLOAD=
export SIGHTS_UPLOAD_DIRECTORY='dev/sights'
export SOUNDS_RUN_AFTER_UPLOAD=
export SOUNDS_UPLOAD_DIRECTORY='dev/sounds'
+export TUNICWILDS_DAILY_FILE=dev/tunicwilds-daily.json
+export TUNICWILDS_RENDERED_DIRECTORY=dev/tunicwilds-rendered
export TUNICWILDS_UPLOAD_DIRECTORY='dev/tunicwilds'
export TUNICWILDS_UPLOAD_URL=
export WORDS_RUN_AFTER_UPLOAD=
export WORDS_UPLOAD_DIRECTORY='dev/words'
# Clean up files from last run
-rm -rf "$database" "$SIGHTS_UPLOAD_DIRECTORY" "$SOUNDS_UPLOAD_DIRECTORY" "$WORDS_UPLOAD_DIRECTORY" "$TUNICWILDS_UPLOAD_DIRECTORY"
+rm -rf \
+ "$database" \
+ "$SIGHTS_UPLOAD_DIRECTORY" \
+ "$SOUNDS_UPLOAD_DIRECTORY" \
+ "$WORDS_UPLOAD_DIRECTORY" \
+ "$TUNICWILDS_DAILY_FILE" \
+ "$TUNICWILDS_RENDERED_DIRECTORY" \
+ "$TUNICWILDS_UPLOAD_DIRECTORY"
# Create directories
mkdir -p "$SIGHTS_UPLOAD_DIRECTORY"
mkdir -p "$SOUNDS_UPLOAD_DIRECTORY"
mkdir -p "$WORDS_UPLOAD_DIRECTORY"
+mkdir -p "$TUNICWILDS_RENDERED_DIRECTORY"
mkdir -p "$TUNICWILDS_UPLOAD_DIRECTORY"
# Create database
diff --git a/src/bin/start-tunicwild-day.ts b/src/bin/start-tunicwild-day.ts
new file mode 100644
index 0000000..0233195
--- /dev/null
+++ b/src/bin/start-tunicwild-day.ts
@@ -0,0 +1,156 @@
+import { execFileSync } from "node:child_process";
+import { randomBytes } from "node:crypto";
+import { readFile, unlink, writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { notInArray, sql } from "drizzle-orm";
+import db from "../database/db";
+import { Tunicwild } from "../database/schema";
+
+if (!process.env.TUNICWILDS_DAILY_FILE) {
+ console.error("TUNICWILDS_DAILY_FILE not set");
+ process.exit(1);
+}
+
+if (!process.env.TUNICWILDS_RENDERED_DIRECTORY) {
+ console.error("TUNICWILDS_RENDERED_DIRECTORY not set");
+ process.exit(1);
+}
+
+if (!process.env.TUNICWILDS_UPLOAD_URL && !process.env.TUNICWILDS_UPLOAD_DIRECTORY) {
+ console.error("TUNICWILDS_UPLOAD_* not set");
+ process.exit(1);
+}
+
+interface DailyInfo {
+ active: {
+ audioFilenames: string[];
+ date: string;
+ id: number;
+ }[];
+ recentIds: number[];
+ startDate: string;
+}
+
+const audioLengths = [1, 2, 4, 8, 16, 32] as const;
+
+async function renderAudio(id: number): Promise {
+ let audioBuffer: NodeJS.ArrayBufferView;
+ if (process.env.TUNICWILDS_UPLOAD_URL) {
+ const response = await fetch(`${process.env.TUNICWILDS_UPLOAD_URL}/${id}`);
+ audioBuffer = await response.bytes();
+ } else if (process.env.TUNICWILDS_UPLOAD_DIRECTORY) {
+ audioBuffer = await readFile(join(process.env.TUNICWILDS_UPLOAD_DIRECTORY, id.toString()));
+ } else {
+ throw new Error();
+ }
+
+ const durationSeconds = Number.parseFloat(execFileSync("ffprobe", [
+ "-v", "quiet",
+ "-show_entries", "format=duration",
+ "-output_format", "csv=p=0",
+ "pipe:",
+ ], {
+ encoding: "utf8",
+ input: audioBuffer,
+ stdio: ["pipe", "pipe", "ignore"],
+ }));
+
+ const filenames = audioLengths.map(() => `${randomBytes(16).toString("hex")}.mp3`);
+ const maxAudioLength = audioLengths[audioLengths.length - 1]!;
+ const maxAudioLengthFilename = filenames[audioLengths.length - 1]!;
+
+ execFileSync("ffmpeg", [
+ "-v", "quiet",
+ "-ss", Math.max(0, Math.floor((durationSeconds - maxAudioLength) * Math.random())).toString(),
+ "-t", maxAudioLength.toString(),
+ "-i", "pipe:",
+ "-map", "0:a:0",
+ "-map_metadata", "-1",
+ join(process.env.TUNICWILDS_RENDERED_DIRECTORY!, maxAudioLengthFilename),
+ ], {
+ input: audioBuffer,
+ stdio: ["pipe", "ignore", "ignore"],
+ });
+
+ for (let i = 0; i < audioLengths.length - 1; i++) {
+ execFileSync("ffmpeg", [
+ "-v", "quiet",
+ "-t", audioLengths[i]!.toString(),
+ "-i", join(process.env.TUNICWILDS_RENDERED_DIRECTORY!, maxAudioLengthFilename),
+ join(process.env.TUNICWILDS_RENDERED_DIRECTORY!, filenames[i]!),
+ ], {
+ stdio: "ignore",
+ });
+ }
+
+ return filenames;
+}
+
+// From "dist/server/bin/"
+process.chdir(resolve(import.meta.dirname, "../../.."));
+
+let dailyInfo: DailyInfo;
+try {
+ dailyInfo = JSON.parse(await readFile(process.env.TUNICWILDS_DAILY_FILE, "utf8"));
+} catch {
+ dailyInfo = {
+ active: [],
+ recentIds: [],
+ startDate: new Date().toISOString().slice(0, 10),
+ };
+}
+
+if (dailyInfo.active.length === 0) {
+ const tunicwilds = await db
+ .select()
+ .from(Tunicwild)
+ .orderBy(sql`RANDOM()`)
+ .limit(3);
+
+ if (tunicwilds.length !== 3) {
+ console.error("Not enough Tunicwilds in database");
+ process.exit(1);
+ }
+
+ for (let i = 0; i < tunicwilds.length; i++) {
+ dailyInfo.active.push({
+ audioFilenames: await renderAudio(tunicwilds[i]!.id),
+ date: new Date(Date.now() + (i - 1) * 1000 * 60 * 60 * 24).toISOString().slice(0, 10),
+ id: tunicwilds[i]!.id,
+ });
+ dailyInfo.recentIds.push(tunicwilds[i]!.id);
+ }
+} else {
+ const tunicwild = await db
+ .select()
+ .from(Tunicwild)
+ .where(notInArray(Tunicwild.id, dailyInfo.recentIds))
+ .orderBy(sql`RANDOM()`)
+ .get();
+
+ if (tunicwild == null) {
+ console.error("Not enough Tunicwilds in database");
+ process.exit(1);
+ }
+
+ dailyInfo.active.push({
+ audioFilenames: await renderAudio(tunicwild.id),
+ date: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString().slice(0, 10),
+ id: tunicwild.id,
+ });
+ dailyInfo.recentIds.push(tunicwild.id);
+
+ if (dailyInfo.active.length > 3) {
+ const dropped = dailyInfo.active.shift()!;
+
+ await Promise.all(dropped.audioFilenames.map(
+ (filename) => unlink(join(process.env.TUNICWILDS_RENDERED_DIRECTORY!, filename)),
+ ));
+ }
+
+ if (dailyInfo.recentIds.length > 30) {
+ dailyInfo.recentIds.shift();
+ }
+}
+
+await writeFile(process.env.TUNICWILDS_DAILY_FILE, JSON.stringify(dailyInfo));
From c1ebc87fb148c372d254ac0eb1486cdb9304dfec Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Thu, 19 Jun 2025 19:45:54 -0600
Subject: [PATCH 039/108] date initialized in const
---
src/components/Tunicwilds/Tunicwilds.svelte | 22 ++++++++++++---------
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index 910a730..e3cee09 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -7,6 +7,8 @@
"composer" | "game" | "id" | "officialLink" | "title"
>;
+ const date = new Date();
+
const { songList }: { songList: SongData[] } = $props();
const gameGroupedSongList = $derived(
songList.reduce(
@@ -77,13 +79,16 @@
error = `${err.message}`;
});
- function getSongData(): Promise {
+ async function getSongData(): Promise {
const adjustedTimestamp =
- Date.now() - new Date().getTimezoneOffset() * 60 * 1000;
+ date.getTime() - new Date().getTimezoneOffset() * 60 * 1000;
+
+ const res = await fetch(
+ `/tunicwilds/today?timestamp=${adjustedTimestamp}`,
+ ).then((res) => res.json());
- return fetch(
- `/api/tunicwilds/today?timestamp=${adjustedTimestamp}`,
- ).then((response) => response.json());
+ if (res.error) throw new Error(res.error);
+ return res;
}
function playClip() {
@@ -158,7 +163,6 @@
}
function shareResult() {
- const gameNumber = new Date().getDate();
const attempts = gameWon ? guesses.length : "X";
const squares = guesses
.map((guess) =>
@@ -177,7 +181,7 @@
)
.join("");
- const shareText = `Tunicwilds #${gameNumber} ${attempts}/${maxGuesses}\n\n${squares}`;
+ const shareText = `Tunicwilds ${date.toLocaleDateString()} ${attempts}/${maxGuesses}\n\n${squares}`;
if (navigator.share) navigator.share({ text: shareText });
else {
@@ -207,9 +211,9 @@
From f89c3f25ff1060df7b23d6b98b338160acf83861 Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 23:43:33 -0700
Subject: [PATCH 040/108] Fix today endpoint for new dailyfile format
---
src/pages/tunicwilds/today.ts | 35 ++++++++++++++++++++---------------
1 file changed, 20 insertions(+), 15 deletions(-)
diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts
index af167f8..4e71462 100644
--- a/src/pages/tunicwilds/today.ts
+++ b/src/pages/tunicwilds/today.ts
@@ -3,9 +3,14 @@ import { jsonError, jsonResponse } from "../../server/responses";
import db from "../../database/db";
import { Tunicwild } from "../../database/schema";
import { eq, type InferSelectModel } from "drizzle-orm";
+import { readFile } from "node:fs/promises";
export const prerender = false;
+if (!process.env.TUNICWILDS_DAILY_FILE) {
+ throw new Error("TUNICWILDS_DAILY_FILE not set");
+}
+
declare global {
interface SessionData {
tunicwilds?: Record {
@@ -28,20 +37,18 @@ export const GET: APIRoute = async (context) => {
return jsonError("Invalid timestamp");
}
- // TODO map from date strings to song IDs
- const dailyTunicwildIds: Record = {};
-
+ const dailyInfo: DailyInfo = JSON.parse(await readFile(process.env.TUNICWILDS_DAILY_FILE!, "utf8"));
const tunicwildDateString = date.toISOString().slice(0, 10);
- const tunicwildId = dailyTunicwildIds[tunicwildDateString];
+ const tunicwildInfo = dailyInfo.active.find((info) => info.date === tunicwildDateString);
- if (tunicwildId == null) {
+ if (tunicwildInfo == null) {
return jsonError("Invalid date. Check if your system clock is set correctly");
}
const tunicwild = await db
.select()
.from(Tunicwild)
- .where(eq(Tunicwild.id, tunicwildId))
+ .where(eq(Tunicwild.id, tunicwildInfo.id))
.get();
if (tunicwild == null) {
@@ -58,7 +65,7 @@ export const GET: APIRoute = async (context) => {
} = {
session: tunicwildsSession,
tunicwild: {
- audioUrl: getAudioUrl(tunicwild.id, audioLengths[tunicwildsSession.guesses.length] ?? audioLengths[0]),
+ audioUrl: `/tunicwilds-rendered/${tunicwildInfo.audioFilenames[tunicwildsSession.guesses.length]}`,
},
};
@@ -99,11 +106,9 @@ export const POST: APIRoute = async (context) => {
return jsonError("Invalid timestamp");
}
- // TODO map from date strings to song IDs
- const dailyTunicwildIds: Record = {};
-
+ const dailyInfo: DailyInfo = JSON.parse(await readFile(process.env.TUNICWILDS_DAILY_FILE!, "utf8"));
const tunicwildDateString = date.toISOString().slice(0, 10);
- const tunicwildId = dailyTunicwildIds[tunicwildDateString];
+ const tunicwildId = dailyInfo.active.find((info) => info.date === tunicwildDateString)?.id;
if (tunicwildId == null) {
return jsonError("Invalid date. Check if your system clock is set correctly");
From a6ccd1d4e65d3edb64bb1d8adf0b8a8def3e1b17 Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 23:43:42 -0700
Subject: [PATCH 041/108] Add todo
---
src/pages/tunicwilds/today.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts
index 4e71462..ba45684 100644
--- a/src/pages/tunicwilds/today.ts
+++ b/src/pages/tunicwilds/today.ts
@@ -144,6 +144,8 @@ export const POST: APIRoute = async (context) => {
tunicwildsSession.result = false;
}
+ // TODO delete older games from session
+
const url = new URL(context.url);
url.searchParams.set("timestamp", params.timestamp.toString());
From 5bbcc547a7f4001d37ab71233e9a18ca27e1b7f0 Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 23:43:56 -0700
Subject: [PATCH 042/108] Redirect instead of rewrite after guess submit
---
src/pages/tunicwilds/today.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts
index ba45684..df57e5e 100644
--- a/src/pages/tunicwilds/today.ts
+++ b/src/pages/tunicwilds/today.ts
@@ -149,6 +149,5 @@ export const POST: APIRoute = async (context) => {
const url = new URL(context.url);
url.searchParams.set("timestamp", params.timestamp.toString());
- // TODO maybe 302 instead
- return context.rewrite(url);
+ return context.redirect(url.toString(), 302);
};
From 8df713c109edde4c75e0039cd0fc4d705406045f Mon Sep 17 00:00:00 2001
From: clayton
Date: Thu, 19 Jun 2025 23:48:10 -0700
Subject: [PATCH 043/108] Serve tunicwilds rendered files in dev server
---
src/middleware.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/middleware.ts b/src/middleware.ts
index ddf586e..6332a96 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -63,6 +63,10 @@ const serveUploadedFilesInDev = defineMiddleware(async (context, next) => {
return new Response("SOUNDS_UPLOAD_DIRECTORY not set", { status: 500 });
}
+ if (!process.env.TUNICWILDS_RENDERED_DIRECTORY) {
+ return new Response("TUNICWILDS_RENDERED_DIRECTORY not set", { status: 500 });
+ }
+
if (!process.env.WORDS_UPLOAD_DIRECTORY) {
return new Response("WORDS_UPLOAD_DIRECTORY not set", { status: 500 });
}
@@ -70,6 +74,7 @@ const serveUploadedFilesInDev = defineMiddleware(async (context, next) => {
const rewrites = [
["/sights-uploads/", process.env.SIGHTS_UPLOAD_DIRECTORY + "/"],
["/sounds-uploads/", process.env.SOUNDS_UPLOAD_DIRECTORY + "/"],
+ ["/tunicwilds-rendered/", process.env.TUNICWILDS_RENDERED_DIRECTORY + "/"],
["/words-uploads/", process.env.WORDS_UPLOAD_DIRECTORY + "/"],
] as const;
From 9ee0d6edd05cbe0bd8df06e56b41f54f0674bc39 Mon Sep 17 00:00:00 2001
From: clayton
Date: Fri, 20 Jun 2025 00:14:45 -0700
Subject: [PATCH 044/108] Copy codec when clipping mp3
---
src/bin/start-tunicwild-day.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/bin/start-tunicwild-day.ts b/src/bin/start-tunicwild-day.ts
index 0233195..83f3d6f 100644
--- a/src/bin/start-tunicwild-day.ts
+++ b/src/bin/start-tunicwild-day.ts
@@ -77,6 +77,7 @@ async function renderAudio(id: number): Promise {
"-v", "quiet",
"-t", audioLengths[i]!.toString(),
"-i", join(process.env.TUNICWILDS_RENDERED_DIRECTORY!, maxAudioLengthFilename),
+ "-c", "copy",
join(process.env.TUNICWILDS_RENDERED_DIRECTORY!, filenames[i]!),
], {
stdio: "ignore",
From e8fafe71098e46c9c4dd922571a3b23acfa27631 Mon Sep 17 00:00:00 2001
From: clayton
Date: Fri, 20 Jun 2025 00:15:05 -0700
Subject: [PATCH 045/108] Fix tunicwilds upload spec
---
src/pages/api/tunicwilds/index.ts | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts
index db3423e..df7cb6c 100644
--- a/src/pages/api/tunicwilds/index.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -179,9 +179,7 @@ export const POST: APIRoute = async (context) => {
// Upload files with better error handling
try {
if (process.env.NODE_ENV === "development") {
- // Get file extension properly
- const extension = fileExtension.substring(1); // Remove the dot
- const filePath = `${process.env.TUNICWILDS_UPLOAD_DIRECTORY}/${tunicwild.id}.${extension}`;
+ const filePath = `${process.env.TUNICWILDS_UPLOAD_DIRECTORY}/${tunicwild.id}`;
const fileStream = file.stream();
const writeStream = createWriteStream(filePath);
@@ -205,9 +203,9 @@ export const POST: APIRoute = async (context) => {
await pump();
} else {
const uploadResponse = await fetch(
- `${process.env.TUNICWILDS_UPLOAD_URL}/upload/${encodeURIComponent(game)}/${encodeURIComponent(title)}`,
+ `${process.env.TUNICWILDS_UPLOAD_URL}/upload/${tunicwild.id}`,
{
- method: "POST",
+ method: "PUT",
body: file.stream(),
headers: {
"Content-Type": file.type || "audio/mpeg",
From e09bc3c3b4a45927ca284ce47101d899a258526b Mon Sep 17 00:00:00 2001
From: clayton
Date: Fri, 20 Jun 2025 00:21:16 -0700
Subject: [PATCH 046/108] Same env checks in upload route as daily script
---
src/pages/api/tunicwilds/index.ts | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts
index df7cb6c..efdd06d 100644
--- a/src/pages/api/tunicwilds/index.ts
+++ b/src/pages/api/tunicwilds/index.ts
@@ -43,12 +43,8 @@ export const GET: APIRoute = async ({ url }) => {
}
export const POST: APIRoute = async (context) => {
- if (
- import.meta.env.DEV
- ? !process.env.TUNICWILDS_UPLOAD_DIRECTORY
- : !process.env.TUNICWILDS_UPLOAD_URL
- ) {
- return jsonError("TUNICWILDS env not set", 500);
+ if (!process.env.TUNICWILDS_UPLOAD_URL && !process.env.TUNICWILDS_UPLOAD_DIRECTORY) {
+ return jsonError("TUNICWILDS_UPLOAD_* not set", 500);
}
let formData: FormData;
@@ -178,7 +174,7 @@ export const POST: APIRoute = async (context) => {
// Upload files with better error handling
try {
- if (process.env.NODE_ENV === "development") {
+ if (process.env.TUNICWILDS_UPLOAD_DIRECTORY) {
const filePath = `${process.env.TUNICWILDS_UPLOAD_DIRECTORY}/${tunicwild.id}`;
const fileStream = file.stream();
@@ -201,7 +197,7 @@ export const POST: APIRoute = async (context) => {
};
await pump();
- } else {
+ } else if (process.env.TUNICWILDS_UPLOAD_URL) {
const uploadResponse = await fetch(
`${process.env.TUNICWILDS_UPLOAD_URL}/upload/${tunicwild.id}`,
{
@@ -219,6 +215,8 @@ export const POST: APIRoute = async (context) => {
await db.delete(Tunicwild).where(eq(Tunicwild.id, tunicwild.id));
return jsonError(`Failed to upload file: ${uploadResponse.statusText}`, 500);
}
+ } else {
+ return jsonError("", 500);
}
} catch (error) {
console.error("File upload error:", error);
From e8a0a7f458b48858c5d000617c5878eca4863d5a Mon Sep 17 00:00:00 2001
From: clayton
Date: Fri, 20 Jun 2025 00:26:47 -0700
Subject: [PATCH 047/108] Add capability for development.sh to run arbitrary
commands
---
development.sh | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/development.sh b/development.sh
index 6245c64..2ccae5c 100755
--- a/development.sh
+++ b/development.sh
@@ -26,6 +26,10 @@ export TUNICWILDS_UPLOAD_URL=
export WORDS_RUN_AFTER_UPLOAD=
export WORDS_UPLOAD_DIRECTORY='dev/words'
+if test $# -gt 0; then
+ exec "$@"
+fi
+
# Clean up files from last run
rm -rf \
"$database" \
From a92acf4ba562513398fd17c270509fafc893c088 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 00:03:54 -0600
Subject: [PATCH 048/108] add quotes
---
development.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/development.sh b/development.sh
index 2ccae5c..09ed8ac 100755
--- a/development.sh
+++ b/development.sh
@@ -19,8 +19,8 @@ export SIGHTS_RUN_AFTER_UPLOAD=
export SIGHTS_UPLOAD_DIRECTORY='dev/sights'
export SOUNDS_RUN_AFTER_UPLOAD=
export SOUNDS_UPLOAD_DIRECTORY='dev/sounds'
-export TUNICWILDS_DAILY_FILE=dev/tunicwilds-daily.json
-export TUNICWILDS_RENDERED_DIRECTORY=dev/tunicwilds-rendered
+export TUNICWILDS_DAILY_FILE='dev/tunicwilds-daily.json'
+export TUNICWILDS_RENDERED_DIRECTORY='dev/tunicwilds-rendered'
export TUNICWILDS_UPLOAD_DIRECTORY='dev/tunicwilds'
export TUNICWILDS_UPLOAD_URL=
export WORDS_RUN_AFTER_UPLOAD=
From e1593f3fdabb1775703305c3111eb592f54911f2 Mon Sep 17 00:00:00 2001
From: clayton
Date: Sat, 21 Jun 2025 01:28:58 -0700
Subject: [PATCH 049/108] Run command on files after rendering
---
development.sh | 1 +
src/bin/start-tunicwild-day.ts | 10 ++++++++++
2 files changed, 11 insertions(+)
diff --git a/development.sh b/development.sh
index 09ed8ac..cecd31b 100755
--- a/development.sh
+++ b/development.sh
@@ -21,6 +21,7 @@ export SOUNDS_RUN_AFTER_UPLOAD=
export SOUNDS_UPLOAD_DIRECTORY='dev/sounds'
export TUNICWILDS_DAILY_FILE='dev/tunicwilds-daily.json'
export TUNICWILDS_RENDERED_DIRECTORY='dev/tunicwilds-rendered'
+export TUNICWILDS_RUN_AFTER_RENDER=
export TUNICWILDS_UPLOAD_DIRECTORY='dev/tunicwilds'
export TUNICWILDS_UPLOAD_URL=
export WORDS_RUN_AFTER_UPLOAD=
diff --git a/src/bin/start-tunicwild-day.ts b/src/bin/start-tunicwild-day.ts
index 83f3d6f..2841378 100644
--- a/src/bin/start-tunicwild-day.ts
+++ b/src/bin/start-tunicwild-day.ts
@@ -84,6 +84,16 @@ async function renderAudio(id: number): Promise {
});
}
+ if (process.env.TUNICWILDS_RUN_AFTER_RENDER) {
+ execFileSync(
+ process.env.TUNICWILDS_RUN_AFTER_RENDER,
+ filenames.map(
+ (filename) => join(process.env.TUNICWILDS_RENDERED_DIRECTORY!, filename),
+ ),
+ { stdio: "ignore" },
+ );
+ }
+
return filenames;
}
From 96435d5035c508dc70ff1c7055bb602c640c7688 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 03:32:09 -0600
Subject: [PATCH 050/108] plcaeholder audio files for tunicwilds
---
src/database/seed.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/database/seed.ts b/src/database/seed.ts
index 3ca5547..7874a30 100644
--- a/src/database/seed.ts
+++ b/src/database/seed.ts
@@ -1,5 +1,6 @@
import { faker } from "@faker-js/faker";
import { ActionFactory, ActionItemFactory, MemberFactory, MotionFactory, SightFactory, SoundFactory, TunicwildFactory, WordFactory } from "./factories";
+import { writeFile } from "fs/promises";
export default async function seed() {
const members = await new MemberFactory().count(20).create();
@@ -45,4 +46,10 @@ export default async function seed() {
});
await new TunicwildFactory().count(10).create();
+
+ // Create 10 audio files in dev/tunicwilds with placeholder audio
+ const res = await fetch("https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3")
+ .then((response) => response.bytes());
+ for (let i = 0; i < 10; i++)
+ await writeFile(`dev/tunicwilds/${i + 1}`, res);
}
From a7d965dd681b5980a5e094ab6961179ba4f4a7de Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 03:33:08 -0600
Subject: [PATCH 051/108] remove errors event hog ti doesnt work yet
---
src/components/Tunicwilds/Tunicwilds.svelte | 94 +++++++++++++--------
1 file changed, 60 insertions(+), 34 deletions(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index e3cee09..e689477 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -21,9 +21,15 @@
),
);
- let songData = $state() as SongData & { audioUrl: string };
- let guesses: ({ title: string; game: string } | false)[] = $state([]); // False for skips
- let currentGuess = $state("");
+ let songData = $state() as {
+ session: Required["tunicwilds"][string];
+ tunicwild: Partial> & {
+ audioUrl: string;
+ };
+ };
+ let guesses: ({ id: number; title: string; game: string } | false)[] =
+ $state([]); // False for skips
+ let currentGuess = $state({ id: -1, guess: "" });
let error = $state("");
let gameWon = $state(false);
let gameLost = $state(false);
@@ -41,13 +47,15 @@
const filterProperties = ["title", "game", "composer"] as const;
const filteredSongs = $derived(
- currentGuess.trim().length >= 2
+ currentGuess.guess.trim()
? songList
.filter((song) =>
filterProperties.some((property) =>
song[property]
.toLowerCase()
- .includes(currentGuess.trim().toLowerCase()),
+ .includes(
+ currentGuess.guess.trim().toLowerCase(),
+ ),
),
)
.sort((a, b) => {
@@ -68,7 +76,7 @@
);
$effect(() => {
- if (currentGuess.trim()) showDropdown = true;
+ if (currentGuess.guess.trim()) showDropdown = true;
else showDropdown = false;
});
@@ -79,7 +87,12 @@
error = `${err.message}`;
});
- async function getSongData(): Promise {
+ async function getSongData(): Promise<{
+ session: Required["tunicwilds"][string];
+ tunicwild: Partial> & {
+ audioUrl: string;
+ };
+ }> {
const adjustedTimestamp =
date.getTime() - new Date().getTimezoneOffset() * 60 * 1000;
@@ -116,7 +129,7 @@
}
}
- function selectSong(songTitle: string) {
+ function selectSong(songTitle: { id: number; guess: string }) {
currentGuess = songTitle;
showDropdown = false;
handleGuess(songTitle);
@@ -126,7 +139,7 @@
if (gameWon || gameLost) return;
guesses = [...guesses, false]; // Add a skip
- currentGuess = "";
+ currentGuess = { id: -1, guess: "" };
showDropdown = false;
if (guesses.length >= maxGuesses) {
@@ -136,21 +149,27 @@
}
function handleGuess(guessedSong = currentGuess) {
- if (!guessedSong.trim() || gameWon || gameLost) return;
+ if (
+ !guessedSong.guess.trim() ||
+ gameWon ||
+ gameLost ||
+ !songData.tunicwild.title
+ )
+ return;
- const validSong = songList.find(
- (song) =>
- song.title.toLowerCase() === guessedSong.toLowerCase().trim(),
- );
+ const validSong = songList.find((song) => song.id === guessedSong.id);
if (!validSong) return;
guesses = [
...guesses,
- { title: validSong.title, game: validSong.game },
+ { id: validSong.id, title: validSong.title, game: validSong.game },
];
- if (validSong.title.toLowerCase() === songData.title.toLowerCase()) {
+ if (
+ validSong.title.toLowerCase() ===
+ songData.tunicwild.title.toLowerCase()
+ ) {
gameWon = true;
isPlaying = false;
} else if (guesses.length >= maxGuesses) {
@@ -158,24 +177,28 @@
isPlaying = false;
}
- currentGuess = "";
+ currentGuess = { id: -1, guess: "" };
showDropdown = false;
}
function shareResult() {
+ if (!songData.tunicwild.title || !songData.tunicwild.game) return;
+
const attempts = gameWon ? guesses.length : "X";
const squares = guesses
.map((guess) =>
!guess
? "π€"
: guess.title.toLowerCase() ===
- songData.title.toLowerCase() &&
- guess.game.toLowerCase() === songData.game.toLowerCase()
+ songData.tunicwild.title!.toLowerCase() &&
+ guess.game.toLowerCase() ===
+ songData.tunicwild.game!.toLowerCase()
? "π"
- : guess.game.toLowerCase() === songData.game.toLowerCase()
+ : guess.game.toLowerCase() ===
+ songData.tunicwild.game!.toLowerCase()
? "π"
: guess.title.toLowerCase() ===
- songData.title.toLowerCase()
+ songData.tunicwild.title!.toLowerCase()
? "π"
: "π",
)
@@ -254,12 +277,12 @@
{#if gameWon || gameLost}
-
"{songData.title}"
+
"{songData.tunicwild.title}"
- from {songData.game}
+ from {songData.tunicwild.game}
- Composed by {songData.composer}
+ Composed by {songData.tunicwild.composer}
@@ -313,7 +336,7 @@
{#if showGameHint && !gameWon && !gameLost}
π‘ Hint: This song is from
- {songData.game}
+ {songData.tunicwild.game}
{/if}
@@ -326,19 +349,22 @@
{#if !gameWon && !gameLost}
@@ -333,7 +375,7 @@
- {#if showGameHint && !gameWon && !gameLost}
+ {#if showGameHint}
π‘
Hint: This song is from
{songData.tunicwild.game}
@@ -344,7 +386,7 @@
@@ -408,27 +450,30 @@
Your Guesses:
- {#each guesses as guess, index}
+ {#each guesses as guessId, index}
+ {@const guessedSong = songFromID(guessId)}
+ {@const isSkip = guessId === null}
{@const isCorrect =
- guess &&
- guess.title.toLowerCase() ===
- songData.tunicwild.title?.toLowerCase()}
- {@const guessedSong = songList.find(
- (song) =>
- guess &&
- guess.title.toLowerCase() ===
- song.title.toLowerCase(),
- )}
+ guessedSong &&
+ songData.tunicwild.title &&
+ guessedSong.title.toLowerCase() ===
+ songData.tunicwild.title.toLowerCase()}
-
{guess}
- {#if guessedSong}
+
+ {isSkip
+ ? "Skipped"
+ : guessedSong?.title || "Unknown"}
+
+ {#if guessedSong && !isSkip}
{guessedSong.game}
@@ -659,6 +704,11 @@
border-color: #ef4444;
}
+ .guess-item.skipped {
+ background: rgba(107, 114, 128, 0.3);
+ border-color: #6b7280;
+ }
+
.guess-content {
display: flex;
justify-content: space-between;
From 37dcf4624b1a1dbc1165804c478408a06448bc7c Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 08:53:45 -0600
Subject: [PATCH 053/108] reorder the main divs in the page
---
src/components/Tunicwilds/Tunicwilds.svelte | 114 ++++++++++----------
1 file changed, 59 insertions(+), 55 deletions(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index 9963597..955625c 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -264,7 +264,7 @@
showSongList = false;
}
- function songFromID(id: number | null): SongData | undefined {
+ function songFromID(id: number | null | undefined): SongData | undefined {
if (!id) return undefined;
return songList.find((song) => song.id === id);
}
@@ -315,6 +315,54 @@
{:else if songData == null}
Loading today's song...
{:else}
+
+
+
Your Guesses:
+
+ {#each Array(maxGuesses) as _, index}
+ {@const guessId = guesses[index]}
+ {@const guessedSong = songFromID(guessId)}
+ {@const isSkip = guessId === null}
+ {@const isEmpty = guessId === undefined}
+ {@const isCorrect =
+ guessedSong &&
+ songData.tunicwild.title &&
+ guessedSong.title.toLowerCase() ===
+ songData.tunicwild.title.toLowerCase()}
+
+
+
+
+
+ {isEmpty
+ ? "β"
+ : isSkip
+ ? "Skipped"
+ : guessedSong?.title || "Unknown"}
+
+ {#if guessedSong && !isSkip && !isEmpty}
+
+ {guessedSong.game}
+
+ {/if}
+
+
+ {clipLengths[index]}s clip
+
+
+
+ {/each}
+
+
+
{#if gameWon || gameLost}
@@ -359,15 +407,16 @@
Clip length: {getCurrentClipLength()}s
-
- Attempt {currentGuessCount + 1} of {maxGuesses}
-
+ {#if !gameWon && !gameLost}
+
+ Attempt {currentGuessCount + 1} of {maxGuesses}
+
+ {/if}
- {#if showGameHint}
+ {#if showGameHint && !gameWon && !gameLost}
π‘ Hint: This song is from
{songData.tunicwild.game}
@@ -444,64 +493,18 @@
{/if}
-
-
- {#if guesses.length > 0}
-
-
Your Guesses:
-
- {#each guesses as guessId, index}
- {@const guessedSong = songFromID(guessId)}
- {@const isSkip = guessId === null}
- {@const isCorrect =
- guessedSong &&
- songData.tunicwild.title &&
- guessedSong.title.toLowerCase() ===
- songData.tunicwild.title.toLowerCase()}
-
-
-
-
-
- {isSkip
- ? "Skipped"
- : guessedSong?.title || "Unknown"}
-
- {#if guessedSong && !isSkip}
-
- {guessedSong.game}
-
- {/if}
-
-
- {clipLengths[index]}s clip
-
-
-
- {/each}
-
-
- {/if}
{/if}
From 2dbe364d8b4e717e4333f4f6be2d264a986d9553 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 09:16:26 -0600
Subject: [PATCH 055/108] progress bar with evil update effect method
---
src/components/Tunicwilds/Tunicwilds.svelte | 65 ++++++++++++++++++++-
1 file changed, 64 insertions(+), 1 deletion(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index cac2b0d..4aa548e 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -37,6 +37,8 @@
let showDropdown = $state(false);
let showSongList = $state(false);
let audioElement: HTMLAudioElement | null = $state(null);
+ let currentTime = $state(0);
+ let animationFrameId: number | null = null;
const maxGuesses = 6;
const clipLengths = [1, 2, 4, 8, 16, 32];
@@ -89,6 +91,52 @@
else showDropdown = false;
});
+ // Unironically just for updating the current time of the audio for the progress bar
+ $effect(() => {
+ if (!audioElement) return;
+
+ const updateProgress = () => {
+ if (audioElement && !audioElement.paused) {
+ currentTime = audioElement.currentTime;
+ animationFrameId = requestAnimationFrame(updateProgress);
+ }
+ };
+
+ const handlePlay = () => {
+ updateProgress();
+ };
+
+ const handlePause = () => {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+ currentTime = audioElement!.currentTime;
+ };
+
+ const handleLoadedMetadata = () => {
+ currentTime = audioElement!.currentTime;
+ };
+
+ audioElement.addEventListener("play", handlePlay);
+ audioElement.addEventListener("pause", handlePause);
+ audioElement.addEventListener("ended", handlePause);
+ audioElement.addEventListener("loadedmetadata", handleLoadedMetadata);
+
+ return () => {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ }
+ audioElement!.removeEventListener("play", handlePlay);
+ audioElement!.removeEventListener("pause", handlePause);
+ audioElement!.removeEventListener("ended", handlePause);
+ audioElement!.removeEventListener(
+ "loadedmetadata",
+ handleLoadedMetadata,
+ );
+ };
+ });
+
getSongData()
.then((value) => (songData = value))
.catch((err) => {
@@ -433,9 +481,15 @@
+ {#each clipLengths as length}
+
+ {/each}
@@ -591,12 +645,21 @@
}
.progress-bar {
+ position: relative;
width: 100%;
height: 0.5rem;
}
+ .progress-marker {
+ position: absolute;
+ width: 0.125rem;
+ height: 0.5rem;
+ background: var(--text-color);
+ }
+
.progress-fill {
height: 0.5rem;
+ background: var(--text-color);
}
.guess-input {
From 6974fcda53cdbfbbec08a852155e9c09d16bdad0 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 09:18:50 -0600
Subject: [PATCH 056/108] kill more padding and put game over state on top
again
---
src/components/Tunicwilds/Tunicwilds.svelte | 82 ++++++++++-----------
1 file changed, 39 insertions(+), 43 deletions(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index 4aa548e..c8f655f 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -363,6 +363,44 @@
{:else if songData == null}
Loading today's song...
{:else}
+
+ {#if gameWon || gameLost}
+
+
+
"{songData.tunicwild.title}"
+
+ from {songData.tunicwild.game}
+
+
+ Composed by {songData.tunicwild.composer}
+
+
+
+ {#if gameWon}
+
+
Nice
+
+ You guessed it in {currentGuessCount} attempt{currentGuessCount !==
+ 1
+ ? "s"
+ : ""}!
+
+
+ {:else}
+
+
π Game Over
+
Better luck next time!
+
+ {/if}
+
+
+
+
+
+ {/if}
+
Your Guesses:
@@ -411,44 +449,6 @@
-
- {#if gameWon || gameLost}
-
-
-
"{songData.tunicwild.title}"
-
- from {songData.tunicwild.game}
-
-
- Composed by {songData.tunicwild.composer}
-
-
-
- {#if gameWon}
-
-
Nice
-
- You guessed it in {currentGuessCount} attempt{currentGuessCount !==
- 1
- ? "s"
- : ""}!
-
-
- {:else}
-
-
π Game Over
-
Better luck next time!
-
- {/if}
-
-
-
-
-
- {/if}
-
@@ -574,14 +574,11 @@
}
.game-over {
- backdrop-filter: blur(10px);
- padding: 1.5rem;
text-align: center;
margin-bottom: 1.5rem;
}
.answer-display {
- padding: 1rem;
margin-bottom: 1rem;
}
@@ -640,7 +637,6 @@
}
.hint {
- padding: 0.75rem;
margin-bottom: 1rem;
}
@@ -694,7 +690,7 @@
.dropdown-item {
width: 100%;
text-align: left;
- padding: 0.75rem 1rem;
+ padding: 0.5rem 1rem;
background: none;
border: none;
cursor: pointer;
From b741f88541ef811aba6f54a329f0a577c688271d Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 09:40:14 -0600
Subject: [PATCH 057/108] force reload audio url in html
---
src/components/Tunicwilds/Tunicwilds.svelte | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index c8f655f..7f15560 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -137,6 +137,14 @@
};
});
+ // Force reload the audio when the URL changes
+ $effect(() => {
+ if (audioElement && songData?.tunicwild.audioUrl) {
+ audioElement.load();
+ currentTime = 0;
+ }
+ });
+
getSongData()
.then((value) => (songData = value))
.catch((err) => {
From 18c9660cc2fab56eb938147b829ceb4dc8766180 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 09:47:06 -0600
Subject: [PATCH 058/108] send full 32s clip when game is over
---
src/components/Tunicwilds/Tunicwilds.svelte | 24 ++++++++++++++-------
src/pages/tunicwilds/today.ts | 2 +-
2 files changed, 17 insertions(+), 9 deletions(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index 7f15560..8ecef25 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -228,7 +228,15 @@
}
function playClip() {
- if (!audioElement || gameLost || gameWon) return;
+ if (!audioElement) return;
+
+ if (gameLost || gameWon) {
+ // no timeout for when the game is over
+ audioElement.currentTime = 0;
+ audioElement.play();
+ isPlaying = true;
+ return;
+ }
const duration =
clipLengths[currentGuessCount] ||
@@ -459,16 +467,16 @@
-
-
- Clip length: {getCurrentClipLength()}s
-
- {#if !gameWon && !gameLost}
+ {#if !gameWon && !gameLost}
+
+
+ Clip length: {getCurrentClipLength()}s
+
Attempt {currentGuessCount + 1} of {maxGuesses}
- {/if}
-
+
+ {/if}
{#if showSongList}
@@ -498,12 +504,12 @@
{#each clipLengths as length}
{/each}
From 645812dec8390074fe4c02240285b85539e75401 Mon Sep 17 00:00:00 2001
From: VINXIS
Date: Sat, 21 Jun 2025 19:06:55 -0600
Subject: [PATCH 062/108] more changinghowthingslook stuff
---
src/components/Tunicwilds/Tunicwilds.svelte | 82 +++++++++++++--------
1 file changed, 52 insertions(+), 30 deletions(-)
diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte
index e86ef30..2a39fc9 100644
--- a/src/components/Tunicwilds/Tunicwilds.svelte
+++ b/src/components/Tunicwilds/Tunicwilds.svelte
@@ -388,19 +388,9 @@
{#if gameWon || gameLost}