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 @@ + + + + +
+ +
+

TUNICWILDS

+

Guess the indie game song

+

+ Song #{new Date().getDate()} β€’ {guesses.length}/{maxGuesses} guesses +

+
+ + + {#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]} +
+

{game}

+ +
+ {/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 @@

TUNICWILDS

-

Guess the indie game song

+

Guess the song from the game

- Song #{new Date().getDate()} β€’ {guesses.length}/{maxGuesses} guesses + {date.toLocaleDateString()} β€’ {guesses.length}/{maxGuesses} guesses

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}
-
-

"{songData.tunicwild.title}"

-

- from {songData.tunicwild.game} -

-

- Composed by {songData.tunicwild.composer} -

-
- {#if gameWon}
-

Nice

+

Nice

You guessed it in {currentGuessCount} attempt{currentGuessCount !== 1 @@ -410,11 +400,25 @@

{:else}
-

πŸ˜” Game Over

+

πŸ˜” Game Over

Better luck next time!

{/if} + +

+ {songData.tunicwild.composer} - "{songData.tunicwild + .title}" +

+

+ Game: {songData.tunicwild.game} +

+
+
{/each} @@ -596,16 +601,31 @@ .game-over { text-align: center; margin-bottom: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; } .answer-display { + padding: 0.5rem; margin-bottom: 1rem; + width: max-content; + text-decoration: none; + } + + .answer-display:hover { + text-decoration: underline; } .game-name { margin-bottom: 0.25rem; } + .win, + .lose { + font-size: 1.25rem; + } + .win { font-weight: bold; color: #22c55e; @@ -740,12 +760,14 @@ border: 1px solid; } - .guess-item.correct { + .guess-item.correct, + .answer-display.win { background: rgba(34, 197, 94, 0.3); border-color: #22c55e; } - .guess-item.incorrect { + .guess-item.incorrect, + .answer-display.lose { background: rgba(239, 68, 68, 0.3); border-color: #ef4444; } From b3b8da49805bb9be46e970074fc488e58dc7b24e Mon Sep 17 00:00:00 2001 From: VINXIS Date: Sat, 21 Jun 2025 19:07:21 -0600 Subject: [PATCH 063/108] timeout isnt necessary at all --- src/components/Tunicwilds/Tunicwilds.svelte | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 2a39fc9..85ebb9b 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -230,27 +230,9 @@ function playClip() { 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] || - clipLengths[clipLengths.length - 1]!; audioElement.currentTime = 0; audioElement.play(); isPlaying = true; - - setTimeout(() => { - if (audioElement) { - audioElement.pause(); - isPlaying = false; - } - }, duration * 1000); } function pauseClip() { From dc78f31b200e98c9e787df84475d541817fdadf9 Mon Sep 17 00:00:00 2001 From: VINXIS Date: Sat, 21 Jun 2025 19:12:51 -0600 Subject: [PATCH 064/108] actually show song list --- src/components/Tunicwilds/Tunicwilds.svelte | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 85ebb9b..e519230 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -306,7 +306,10 @@ if (!(event.target as HTMLElement).closest(".dropdown-container")) showDropdown = false; - if (!(event.target as HTMLElement).closest(".song-list-overlay")) + if ( + !(event.target as HTMLElement).closest(".song-list-overlay") && + !(event.target as HTMLElement).closest(".song-list-btn") + ) showSongList = false; } @@ -346,13 +349,15 @@ {#each Object.entries(gameGroupedSongList) as [game, songs]}
-

{game}

+

+ {game} +

@@ -570,11 +575,11 @@ .song-list-overlay { position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + padding: 2rem; + background: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; From ed070316c3dd2455cd87f48a0fd4b95c6478887e Mon Sep 17 00:00:00 2001 From: VINXIS Date: Sat, 21 Jun 2025 19:15:53 -0600 Subject: [PATCH 065/108] use css var for songlist background from themes --- src/components/Tunicwilds/Tunicwilds.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index e519230..d52c485 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -579,7 +579,11 @@ top: 50%; transform: translate(-50%, -50%); padding: 2rem; - background: rgba(0, 0, 0, 0.9); + background: color-mix( + in srgb, + var(--background-color) 90%, + transparent + ); display: flex; justify-content: center; align-items: center; From d87d6c77fcf30d79ee9fdda3eac5c5cfe3da442f Mon Sep 17 00:00:00 2001 From: VINXIS Date: Sat, 21 Jun 2025 19:22:17 -0600 Subject: [PATCH 066/108] change isplaying var in the bigass effect thing --- src/components/Tunicwilds/Tunicwilds.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index d52c485..415aa67 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -103,10 +103,12 @@ }; const handlePlay = () => { + isPlaying = true; updateProgress(); }; const handlePause = () => { + isPlaying = false; if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; From 63476aa98ff05129d9e60b9a5be51aaea8bd1b1d Mon Sep 17 00:00:00 2001 From: clayton Date: Sat, 21 Jun 2025 22:56:25 -0700 Subject: [PATCH 067/108] Use array length instead of hardcode --- src/pages/tunicwilds/today.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts index 15d36f6..d565167 100644 --- a/src/pages/tunicwilds/today.ts +++ b/src/pages/tunicwilds/today.ts @@ -65,7 +65,11 @@ export const GET: APIRoute = async (context) => { } = { session: tunicwildsSession, tunicwild: { - audioUrl: `/tunicwilds-rendered/${tunicwildInfo.audioFilenames[tunicwildsSession.result != null ? 5 : tunicwildsSession.guesses.length]}`, + audioUrl: `/tunicwilds-rendered/${tunicwildInfo.audioFilenames[ + tunicwildsSession.result != null + ? tunicwildInfo.audioFilenames.length - 1 + : tunicwildsSession.guesses.length + ]}`, }, }; From 32cfa1ce1fe26b0408331bccedbfd3dfc836a02f Mon Sep 17 00:00:00 2001 From: clayton Date: Sat, 21 Jun 2025 23:05:32 -0700 Subject: [PATCH 068/108] Use IDs from created tunicwilds to seed audio files --- src/database/seed.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/database/seed.ts b/src/database/seed.ts index 7874a30..be036d4 100644 --- a/src/database/seed.ts +++ b/src/database/seed.ts @@ -1,6 +1,11 @@ import { faker } from "@faker-js/faker"; import { ActionFactory, ActionItemFactory, MemberFactory, MotionFactory, SightFactory, SoundFactory, TunicwildFactory, WordFactory } from "./factories"; import { writeFile } from "fs/promises"; +import { join } from "path"; + +if (!process.env.TUNICWILDS_UPLOAD_DIRECTORY) { + throw new Error("TUNICWILDS_UPLOAD_DIRECTORY not set"); +} export default async function seed() { const members = await new MemberFactory().count(20).create(); @@ -45,11 +50,15 @@ export default async function seed() { memberDiscord: () => faker.helpers.arrayElement(members.map((member) => member.discord)), }); - await new TunicwildFactory().count(10).create(); + const tunicwilds = 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); + const placeholderAudioResponse = await fetch("https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3"); + const placeholderAudio = await placeholderAudioResponse.bytes(); + + for (const tunicwild of tunicwilds) { + await writeFile( + join(process.env.TUNICWILDS_UPLOAD_DIRECTORY!, tunicwild.id.toString()), + placeholderAudio, + ); + } } From e3cf21400500b08ce45a007640db5ce313e542b6 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 00:58:11 -0700 Subject: [PATCH 069/108] Use ffmpeg to get duration instead of reading from metadata --- src/bin/start-tunicwild-day.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/bin/start-tunicwild-day.ts b/src/bin/start-tunicwild-day.ts index eba64ae..8df30c6 100644 --- a/src/bin/start-tunicwild-day.ts +++ b/src/bin/start-tunicwild-day.ts @@ -1,4 +1,4 @@ -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import { randomBytes } from "node:crypto"; import { readFile, unlink, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; @@ -44,16 +44,32 @@ async function renderAudio(id: number): Promise { throw new Error(); } - const durationSeconds = Number.parseFloat(execFileSync("ffprobe", [ + const ffmpegStatsResult = spawnSync("ffmpeg", [ "-v", "quiet", - "-show_entries", "format=duration", - "-output_format", "csv=p=0", - "pipe:", + "-stats", + "-i", "pipe:", + "-f", "null", + "-", ], { encoding: "utf8", input: audioBuffer, - stdio: ["pipe", "pipe", "ignore"], - })); + stdio: ["pipe", "ignore", "pipe"], + }); + + if (ffmpegStatsResult.error != null) { + throw ffmpegStatsResult.error; + } + + const timeMatch = ffmpegStatsResult.stderr.match(/time=(\d{2,}):(\d{2}):(\d{2}\.\d{2})/); + + if (timeMatch == null) { + throw new Error(`Unexpected ffmpeg output:\n${ffmpegStatsResult}`); + } + + const durationSeconds = + Number.parseInt(timeMatch[1]!, 10) * 60 * 60 + + Number.parseInt(timeMatch[2]!, 10) * 60 + + Number.parseFloat(timeMatch[3]!); const filenames = audioLengths.map(() => `${randomBytes(16).toString("hex")}.mp3`); const maxAudioLength = audioLengths[audioLengths.length - 1]!; From 7fb4f90772118c60c91a4047421d04c968d3e20c Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 01:29:59 -0700 Subject: [PATCH 070/108] Map only first audio stream for duration check --- 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 8df30c6..ff21724 100644 --- a/src/bin/start-tunicwild-day.ts +++ b/src/bin/start-tunicwild-day.ts @@ -48,6 +48,7 @@ async function renderAudio(id: number): Promise { "-v", "quiet", "-stats", "-i", "pipe:", + "-map", "0:a:0", "-f", "null", "-", ], { From 9f7de23c5413d27ba91297c75ad2a43268690687 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 01:30:20 -0700 Subject: [PATCH 071/108] Fix match for duration --- src/bin/start-tunicwild-day.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/start-tunicwild-day.ts b/src/bin/start-tunicwild-day.ts index ff21724..98f083b 100644 --- a/src/bin/start-tunicwild-day.ts +++ b/src/bin/start-tunicwild-day.ts @@ -61,7 +61,7 @@ async function renderAudio(id: number): Promise { throw ffmpegStatsResult.error; } - const timeMatch = ffmpegStatsResult.stderr.match(/time=(\d{2,}):(\d{2}):(\d{2}\.\d{2})/); + const timeMatch = ffmpegStatsResult.stderr.match(/time=(\d{2,}):(\d{2}):(\d{2}\.\d{2})[^\r]*$/); if (timeMatch == null) { throw new Error(`Unexpected ffmpeg output:\n${ffmpegStatsResult}`); From 22e9c025715a00c560d949d4fd0dfa61ba5875f4 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 01:30:50 -0700 Subject: [PATCH 072/108] Use VBR with q0 for audio clips --- src/bin/start-tunicwild-day.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bin/start-tunicwild-day.ts b/src/bin/start-tunicwild-day.ts index 98f083b..b768a17 100644 --- a/src/bin/start-tunicwild-day.ts +++ b/src/bin/start-tunicwild-day.ts @@ -83,6 +83,8 @@ async function renderAudio(id: number): Promise { "-i", "pipe:", "-map", "0:a:0", "-map_metadata", "-1", + "-c:a", "libmp3lame", + "-q:a", "0", join(process.env.TUNICWILDS_RENDERED_DIRECTORY!, maxAudioLengthFilename), ], { input: audioBuffer, From f566176113fd4fc31cb67f7423b313973e4f4494 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 02:33:12 -0700 Subject: [PATCH 073/108] 445 effect --- src/components/Tunicwilds/Tunicwilds.svelte | 45 ++++++++++++++------- src/pages/tunicwilds/today.ts | 2 + 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 415aa67..a9a4bc8 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -22,15 +22,7 @@ ); // server data - let songData = $state() as { - session: { - guesses: (number | null)[]; - result: boolean | null; - }; - tunicwild: Partial> & { - audioUrl: string; - }; - }; + let songData = $state() as Awaited>; let currentGuess = $state({ id: -1, guess: "" }); let error = $state(""); let isPlaying = $state(false); @@ -155,6 +147,7 @@ }); async function getSongData(): Promise<{ + fourFourFiveEnabled: boolean; session: { guesses: (number | null)[]; result: boolean | null; @@ -246,15 +239,37 @@ function shareResult() { if (!songData?.tunicwild.title || !songData?.tunicwild.game) return; + const emojiSet = songData.fourFourFiveEnabled + ? { + skip: ":charles:", + correct: ":onlinecharles:", + correctGame: ":neoyellowcharles:", + correctTitle: ":jamescharles:", + incorrect: ":redcharles:", + } + : { + skip: "πŸ–€", + correct: "πŸ’š", + correctGame: "πŸ’›", + correctTitle: "πŸ’™", + incorrect: "πŸ’”", + }; + const attempts = gameWon ? currentGuessCount : "X"; const squares = guesses .map((guessId) => { - if (guessId === null) return "πŸ–€"; // Skip + if (guessId == null) { + return emojiSet.skip; + } const guessedSong = songList.find( (song) => song.id === guessId, ); - if (!guessedSong) return "πŸ’”"; + + // Only possible if the song was deleted and the page was reloaded + if (guessedSong == null) { + return emojiSet.incorrect; + } const correctSong = songData.tunicwild; @@ -265,7 +280,7 @@ guessedSong.game.toLowerCase() === correctSong.game?.toLowerCase() ) { - return "πŸ’š"; + return emojiSet.correct; } // Same game @@ -273,7 +288,7 @@ guessedSong.game.toLowerCase() === correctSong.game?.toLowerCase() ) { - return "πŸ’›"; + return emojiSet.correctGame; } // Same title (different game) @@ -281,10 +296,10 @@ guessedSong.title.toLowerCase() === correctSong.title?.toLowerCase() ) { - return "πŸ’™"; + return emojiSet.correctTitle; } - return "πŸ’”"; // Wrong + return emojiSet.incorrect; }) .join(""); diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts index d565167..69c3a53 100644 --- a/src/pages/tunicwilds/today.ts +++ b/src/pages/tunicwilds/today.ts @@ -60,9 +60,11 @@ export const GET: APIRoute = async (context) => { result: null, }; const responseBody: { + fourFourFiveEnabled: boolean; session: Required["tunicwilds"][string]; tunicwild: Partial> & { audioUrl: string }; } = { + fourFourFiveEnabled: context.locals.session.data.memberDiscord != null, session: tunicwildsSession, tunicwild: { audioUrl: `/tunicwilds-rendered/${tunicwildInfo.audioFilenames[ From 1181b6196faecd7d9be15e36308e0ac4d3606214 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 02:53:35 -0700 Subject: [PATCH 074/108] Include correctness info in guesses array --- src/pages/tunicwilds/today.ts | 55 +++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts index 69c3a53..17c1e32 100644 --- a/src/pages/tunicwilds/today.ts +++ b/src/pages/tunicwilds/today.ts @@ -14,7 +14,10 @@ if (!process.env.TUNICWILDS_DAILY_FILE) { declare global { interface SessionData { tunicwilds?: Record; } @@ -88,16 +91,23 @@ export const GET: APIRoute = async (context) => { responseBody.tunicwild.extraHint = tunicwild.extraHint; } - if (guessCount >= 5) { + if ( + guessCount >= 5 || + tunicwildsSession.guesses.some((guess) => guess?.result === "correctGame") + ) { responseBody.tunicwild.game = tunicwild.game; } + + if (tunicwildsSession.guesses.some((guess) => guess?.result === "correctTitle")) { + responseBody.tunicwild.title = tunicwild.title; + } } return jsonResponse(responseBody); }; export const POST: APIRoute = async (context) => { - const params = await context.request.json(); + const params: Partial> = await context.request.json(); if ( typeof params.timestamp !== "number" || @@ -120,14 +130,6 @@ 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) { @@ -142,7 +144,36 @@ export const POST: APIRoute = async (context) => { return jsonError("Tunicwild has already been completed"); } - tunicwildsSession.guesses.push(params.guess); + const tunicwild = await db + .select() + .from(Tunicwild) + .where(eq(Tunicwild.id, tunicwildId)) + .get(); + + if (tunicwild == null) { + throw new Error("Missing daily tunicwild in database"); + } + + const guessedTunicwild = params.guess == null ? null : await db + .select() + .from(Tunicwild) + .where(eq(Tunicwild.id, params.guess)) + .get(); + + if (params.guess != null && guessedTunicwild == null) { + return jsonError("Invalid guess"); + } + + tunicwildsSession.guesses.push(params.guess == null ? null : { + id: params.guess, + result: params.guess === tunicwildId + ? "correct" + : guessedTunicwild?.game === tunicwild.game + ? "correctGame" + : guessedTunicwild?.title === tunicwild.title + ? "correctTitle" + : "incorrect", + }); if (params.guess === tunicwildId) { tunicwildsSession.result = true; From 19e93e0215477ea5a572b09d9392c594d0bc158f Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 03:00:55 -0700 Subject: [PATCH 075/108] Refactor of guess array thing --- src/pages/tunicwilds/today.ts | 56 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts index 17c1e32..4aa62ac 100644 --- a/src/pages/tunicwilds/today.ts +++ b/src/pages/tunicwilds/today.ts @@ -144,37 +144,41 @@ export const POST: APIRoute = async (context) => { return jsonError("Tunicwild has already been completed"); } - const tunicwild = await db - .select() - .from(Tunicwild) - .where(eq(Tunicwild.id, tunicwildId)) - .get(); + if (params.guess == null) { + tunicwildsSession.guesses.push(null); + } else { + const tunicwild = await db + .select() + .from(Tunicwild) + .where(eq(Tunicwild.id, tunicwildId)) + .get(); + + if (tunicwild == null) { + throw new Error("Missing daily tunicwild in database"); + } - if (tunicwild == null) { - throw new Error("Missing daily tunicwild in database"); - } + const guessedTunicwild = await db + .select() + .from(Tunicwild) + .where(eq(Tunicwild.id, params.guess)) + .get(); - const guessedTunicwild = params.guess == null ? null : await db - .select() - .from(Tunicwild) - .where(eq(Tunicwild.id, params.guess)) - .get(); + if (guessedTunicwild == null) { + return jsonError("Invalid guess"); + } - if (params.guess != null && guessedTunicwild == null) { - return jsonError("Invalid guess"); + tunicwildsSession.guesses.push({ + id: params.guess, + result: guessedTunicwild.id === tunicwild.id + ? "correct" + : guessedTunicwild.game === tunicwild.game + ? "correctGame" + : guessedTunicwild.title === tunicwild.title + ? "correctTitle" + : "incorrect", + }); } - tunicwildsSession.guesses.push(params.guess == null ? null : { - id: params.guess, - result: params.guess === tunicwildId - ? "correct" - : guessedTunicwild?.game === tunicwild.game - ? "correctGame" - : guessedTunicwild?.title === tunicwild.title - ? "correctTitle" - : "incorrect", - }); - if (params.guess === tunicwildId) { tunicwildsSession.result = true; } else if (tunicwildsSession.guesses.length >= 6) { From 4951f9a596d1c2e23ee3b700807bcbb0fc7902f6 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 15:05:00 -0700 Subject: [PATCH 076/108] Use new guess format and add styles for other guess results --- src/components/Tunicwilds/Tunicwilds.svelte | 162 ++++++++------------ 1 file changed, 65 insertions(+), 97 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index a9a4bc8..653e0be 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -148,10 +148,7 @@ async function getSongData(): Promise<{ fourFourFiveEnabled: boolean; - session: { - guesses: (number | null)[]; - result: boolean | null; - }; + session: Required["tunicwilds"][string]; tunicwild: Partial> & { audioUrl: string; }; @@ -257,50 +254,7 @@ const attempts = gameWon ? currentGuessCount : "X"; const squares = guesses - .map((guessId) => { - if (guessId == null) { - return emojiSet.skip; - } - - const guessedSong = songList.find( - (song) => song.id === guessId, - ); - - // Only possible if the song was deleted and the page was reloaded - if (guessedSong == null) { - return emojiSet.incorrect; - } - - const correctSong = songData.tunicwild; - - // Perfect match - if ( - guessedSong.title.toLowerCase() === - correctSong.title?.toLowerCase() && - guessedSong.game.toLowerCase() === - correctSong.game?.toLowerCase() - ) { - return emojiSet.correct; - } - - // Same game - if ( - guessedSong.game.toLowerCase() === - correctSong.game?.toLowerCase() - ) { - return emojiSet.correctGame; - } - - // Same title (different game) - if ( - guessedSong.title.toLowerCase() === - correctSong.title?.toLowerCase() - ) { - return emojiSet.correctTitle; - } - - return emojiSet.incorrect; - }) + .map((guess) => emojiSet[guess?.result ?? "skip"]) .join(""); const shareText = `Tunicwilds ${date.toLocaleDateString()} ${attempts}/${maxGuesses}\n\n${squares}`; @@ -329,11 +283,6 @@ ) showSongList = false; } - - function songFromID(id: number | null | undefined): SongData | undefined { - if (!id) return undefined; - return songList.find((song) => song.id === id); - } @@ -436,45 +385,49 @@

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()} - -
-
- {#if guessedSong} - {guessedSong.composer} - "{guessedSong.title}" - ({guessedSong.game}) - {:else} -
- {isEmpty - ? "β€”" - : isSkip - ? "Skipped" - : "Unknown"} -
- {/if} - - {clipLengths[index]}s - + {@const guess = guesses[index]} + + {#if guess === undefined} +
+
+
β€”
+ + {clipLengths[index]}s + +
-
+ {:else} + {@const guessedSong = songList.find( + (song) => song.id === guess?.id, + )} + +
+
+ {#if guessedSong != null} + {guessedSong.composer} - "{guessedSong.title}" + ({guessedSong.game}) + {:else} +
+ {guess == null + ? "Skipped" + : "Song unavailable"} +
+ {/if} + + {clipLengths[index]}s + +
+
+ {/if} {/each}
@@ -768,21 +721,36 @@ border: 1px solid; } - .guess-item.correct, + .guess-item--correct, .answer-display.win { background: rgba(34, 197, 94, 0.3); - border-color: #22c55e; + border-color: rgb(34, 197, 94); } - .guess-item.incorrect, + .guess-item--correctGame { + background: rgba(231, 220, 65, 0.3); + border-color: rgb(231, 220, 65); + } + + .guess-item--correctTitle { + background: rgba(65, 131, 231, 0.3); + border-color: rgb(65, 131, 231); + } + + .guess-item--incorrect, .answer-display.lose { background: rgba(239, 68, 68, 0.3); - border-color: #ef4444; + border-color: rgb(239, 68, 68); + } + + .guess-item--skip { + background: rgba(107, 114, 128, 0.1); + border-color: rgb(209, 213, 219); } - .guess-item.empty { + .guess-item--empty { background: rgba(107, 114, 128, 0.1); - border-color: #d1d5db; + border-color: rgb(209, 213, 219); opacity: 0.5; } From 331259a7582069cea378b66de2bf2226917b8092 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 16:15:58 -0700 Subject: [PATCH 077/108] nvm combine those again --- src/components/Tunicwilds/Tunicwilds.svelte | 70 +++++++++------------ 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 653e0be..e57134f 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -386,48 +386,38 @@
{#each Array(maxGuesses) as _, index} {@const guess = guesses[index]} + {@const guessedSong = songList.find( + (song) => song.id === guess?.id, + )} - {#if guess === undefined} -
-
-
β€”
- - {clipLengths[index]}s - -
-
- {:else} - {@const guessedSong = songList.find( - (song) => song.id === guess?.id, - )} - -
-
- {#if guessedSong != null} - {guessedSong.composer} - "{guessedSong.title}" - ({guessedSong.game}) - {:else} -
- {guess == null - ? "Skipped" - : "Song unavailable"} -
- {/if} - - {clipLengths[index]}s - -
+
+
+ {#if guessedSong != null} + {guessedSong.composer} - "{guessedSong.title}" + ({guessedSong.game}) + {:else} +
+ {guess === undefined + ? "β€”" + : guess == null + ? "Skipped" + : "Song unavailable"} +
+ {/if} + + {clipLengths[index]}s +
- {/if} +
{/each}
From 477e3536876ffec0be5c3f7dcbe21ef820be750c Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 16:45:29 -0700 Subject: [PATCH 078/108] Add SMILEY --- src/components/Tunicwilds/Tunicwilds.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index e57134f..001e844 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -343,7 +343,7 @@
{#if gameWon}
-

Nice

+

πŸ˜ƒ Nice

You guessed it in {currentGuessCount} attempt{currentGuessCount !== 1 From e5c2baa2902acd190a01867d4aac4aa638de3784 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 16:45:46 -0700 Subject: [PATCH 079/108] Slight reword of win msg --- src/components/Tunicwilds/Tunicwilds.svelte | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 001e844..69e96e3 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -345,10 +345,8 @@

πŸ˜ƒ Nice

- You guessed it in {currentGuessCount} attempt{currentGuessCount !== - 1 - ? "s" - : ""}! + You guessed it in {guesses.length} + {guesses.length === 1 ? "try" : "tries"}!

{:else} From d92f2a5c26ce977e4f3135fcefa34bd54188956c Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 17:11:46 -0700 Subject: [PATCH 080/108] Remove unnecessary checks --- src/components/Tunicwilds/Tunicwilds.svelte | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 69e96e3..e304643 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -201,14 +201,13 @@ } async function skipGuess() { - if (gameWon || gameLost) return; await submitGuess(null); // Submit null for skip currentGuess = { id: -1, guess: "" }; showDropdown = false; } async function handleGuess(guessedSong = currentGuess) { - if (!guessedSong.guess.trim() || gameWon || gameLost) return; + if (!guessedSong.guess.trim()) return; const validSong = songList.find((song) => song.id === guessedSong.id); @@ -234,8 +233,6 @@ } function shareResult() { - if (!songData?.tunicwild.title || !songData?.tunicwild.game) return; - const emojiSet = songData.fourFourFiveEnabled ? { skip: ":charles:", @@ -482,7 +479,6 @@ onfocus={() => currentGuess.guess.trim() && (showDropdown = true)} placeholder="Start typing a song title..." - disabled={gameWon || gameLost} /> βŒ„
@@ -508,13 +504,7 @@ {/if} - +
{/if} {/if} From 4257f9cc7202bac5f006cf2118e245d86cf2aca8 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 22 Jun 2025 17:12:16 -0700 Subject: [PATCH 081/108] Remove unncessary state --- src/components/Tunicwilds/Tunicwilds.svelte | 40 +++++++-------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index e304643..98c34e5 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -36,17 +36,8 @@ const clipLengths = [0.5, 1, 2, 4, 8, 16]; const gameHintAfter = 3; - // Stuff derived from server data (songData) - const guesses = $derived(songData?.session.guesses || []); - const gameWon = $derived(songData?.session.result === true); - const gameLost = $derived(songData?.session.result === false); - const currentGuessCount = $derived(guesses.length); - const showGameHint = $derived( - currentGuessCount >= gameHintAfter && - !gameWon && - !gameLost && - songData?.tunicwild.game, - ); + const guesses = $derived(songData?.session.guesses ?? []); + const result = $derived(songData?.session.result ?? null); const filterProperties = ["title", "game", "composer"] as const; const filteredSongs = $derived( @@ -249,7 +240,7 @@ incorrect: "πŸ’”", }; - const attempts = gameWon ? currentGuessCount : "X"; + const attempts = result ? guesses.length : "X"; const squares = guesses .map((guess) => emojiSet[guess?.result ?? "skip"]) .join(""); @@ -263,13 +254,6 @@ } } - function getCurrentClipLength(): number | undefined { - return ( - clipLengths[currentGuessCount] || - clipLengths[clipLengths.length - 1] - ); - } - function handleClickOutside(event: MouseEvent) { if (!(event.target as HTMLElement).closest(".dropdown-container")) showDropdown = false; @@ -290,7 +274,7 @@

TUNICWILDS

Guess the song from the game

- {date.toLocaleDateString()} β€’ {currentGuessCount}/{maxGuesses} guesses + {date.toLocaleDateString()} β€’ {guesses.length}/{maxGuesses} guesses

@@ -520,45 +524,48 @@ .game-over { text-align: center; - margin-bottom: 1.5rem; + margin: 1.5rem auto; display: flex; flex-direction: column; align-items: center; + border: 1px dashed white; + width: max-content; + padding: 1rem; } .answer-display { - padding: 0.5rem; + padding: 1rem; margin-bottom: 1rem; - width: max-content; text-decoration: none; + display: flex; + flex-direction: column; + gap: 0.5rem; + border: 1px solid; + font-size: 1.25rem; } .answer-display:hover { text-decoration: underline; } - .game-name { - margin-bottom: 0.25rem; + .answer-display > strong { + font-size: 0.8em; } - .win, - .lose { + .result-message { font-size: 1.25rem; } - .win { - font-weight: bold; + .result-message--win { color: #22c55e; - margin-bottom: 0.5rem; } - .lose { - font-weight: bold; + .result-message--lose { color: #ef4444; - margin-bottom: 0.5rem; } .result p { + margin-top: 0; margin-bottom: 1rem; } @@ -681,7 +688,7 @@ } .guess-item--correct, - .answer-display.win { + .answer-display--win { background: rgba(34, 197, 94, 0.3); border-color: rgb(34, 197, 94); } @@ -697,7 +704,7 @@ } .guess-item--incorrect, - .answer-display.lose { + .answer-display--lose { background: rgba(239, 68, 68, 0.3); border-color: rgb(239, 68, 68); } From bbb9f99c8d34f2d9aca20f44f8a81dbfca41365d Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 24 Jun 2025 14:32:46 -0700 Subject: [PATCH 092/108] Show explainer text for correct game or title --- src/components/Tunicwilds/Tunicwilds.svelte | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index a27410a..8b2aebd 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -693,16 +693,39 @@ border-color: rgb(34, 197, 94); } + .guess-item--correctGame, + .guess-item--correctTitle { + position: relative; + } + + .guess-item--correctGame::after, + .guess-item--correctTitle::after { + position: absolute; + left: -45px; + top: -9px; + transform: rotate(-10deg); + } + .guess-item--correctGame { background: rgba(231, 220, 65, 0.3); border-color: rgb(231, 220, 65); } + .guess-item--correctGame::after { + content: "Game correct!"; + background-color: rgb(231, 220, 65); + } + .guess-item--correctTitle { background: rgba(65, 131, 231, 0.3); border-color: rgb(65, 131, 231); } + .guess-item--correctTitle::after { + content: "Title correct!"; + background-color: rgb(65, 131, 231); + } + .guess-item--incorrect, .answer-display--lose { background: rgba(239, 68, 68, 0.3); From 09d2e86c701bb7f0550cd8c47cb60631f7d7e955 Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 24 Jun 2025 14:33:04 -0700 Subject: [PATCH 093/108] Show all relevant hints --- src/components/Tunicwilds/Tunicwilds.svelte | 37 ++++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 8b2aebd..013a19c 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -26,9 +26,8 @@ let currentTime = $state(0); let animationFrameId: number | null = null; - const maxGuesses = 6; const clipLengths = [0.5, 1, 2, 4, 8, 16]; - const gameHintAfter = 3; + const maxGuesses = clipLengths.length; const guesses = $derived(songData?.session.guesses ?? []); const result = $derived(songData?.session.result ?? null); @@ -424,12 +423,34 @@
- - {#if guesses.length >= gameHintAfter && songData.tunicwild.game && result == null} -
- πŸ’‘ Hint: This song is from - {songData.tunicwild.game} -
+ + {#if result == null} + {#if songData.tunicwild.releaseDate != null} +
+ πŸ’‘ Hint: This song was released on + {new Date( + songData.tunicwild.releaseDate, + ).toLocaleDateString(undefined, { + timeZone: "UTC", + })} +
+ {/if} + + {#if songData.tunicwild.extraHint} +
+ πŸ’‘ Hint: + {songData.tunicwild.extraHint} +
+ {/if} + + {#if songData.tunicwild.game != null && !guesses.some((guess) => guess?.result === "correctGame")} +
+ πŸ’‘ Hint: This song is from + {songData.tunicwild.game} +
+ {/if} {/if} From 79676ee7d5322ab64917189a1dc2bbb6d902b8d5 Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 24 Jun 2025 14:33:34 -0700 Subject: [PATCH 094/108] Use case insensitive check for correctTitle --- src/pages/tunicwilds/today.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts index e951fe9..e00c2ec 100644 --- a/src/pages/tunicwilds/today.ts +++ b/src/pages/tunicwilds/today.ts @@ -174,7 +174,7 @@ export const POST: APIRoute = async (context) => { ? "correct" : guessedTunicwild.game === tunicwild.game ? "correctGame" - : guessedTunicwild.title === tunicwild.title + : guessedTunicwild.title.toLowerCase() === tunicwild.title.toLowerCase() ? "correctTitle" : "incorrect", }); From 83a5a3ab1b8983d5e967141ce4b8e2049371bef2 Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 24 Jun 2025 14:47:47 -0700 Subject: [PATCH 095/108] Wrong color --- src/components/Tunicwilds/Tunicwilds.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 013a19c..b248694 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -549,7 +549,7 @@ display: flex; flex-direction: column; align-items: center; - border: 1px dashed white; + border: 1px dashed var(--text-color); width: max-content; padding: 1rem; } From 79b38c0322fe815bbc471f236b28aa5d0d42021f Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 24 Jun 2025 14:52:44 -0700 Subject: [PATCH 096/108] Remove game and title include conditions from server No need for these anymore. --- src/pages/tunicwilds/today.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts index e00c2ec..bcfbe7c 100644 --- a/src/pages/tunicwilds/today.ts +++ b/src/pages/tunicwilds/today.ts @@ -92,16 +92,9 @@ export const GET: APIRoute = async (context) => { responseBody.tunicwild.extraHint = tunicwild.extraHint; } - if ( - guessCount >= 5 || - tunicwildsSession.guesses.some((guess) => guess?.result === "correctGame") - ) { + if (guessCount >= 5) { responseBody.tunicwild.game = tunicwild.game; } - - if (tunicwildsSession.guesses.some((guess) => guess?.result === "correctTitle")) { - responseBody.tunicwild.title = tunicwild.title; - } } return jsonResponse(responseBody); From f7c9b780eafe5b77878b1de0cab543ee1ac4a275 Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 24 Jun 2025 15:11:53 -0700 Subject: [PATCH 097/108] Better date style to fit into sentence --- src/components/Tunicwilds/Tunicwilds.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index b248694..239c865 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -432,6 +432,7 @@ >{new Date( songData.tunicwild.releaseDate, ).toLocaleDateString(undefined, { + dateStyle: "long", timeZone: "UTC", })} From 710ec0157cde432b845d2c62e70131268a77b44d Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 24 Jun 2025 15:12:15 -0700 Subject: [PATCH 098/108] Slim down hint margins --- src/components/Tunicwilds/Tunicwilds.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 239c865..b6c0801 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -626,7 +626,7 @@ } .hint { - margin-bottom: 1rem; + margin-block: 0.5em; } .progress-bar { From 2b3227c98b9d2cfea7b5c33562660e0c7ff740ea Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 24 Jun 2025 16:07:06 -0700 Subject: [PATCH 099/108] Devinxis song list overlay --- src/components/Tunicwilds/Tunicwilds.svelte | 31 +++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index b6c0801..49f6df8 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -257,14 +257,24 @@ showDropdown = false; if ( - !(event.target as HTMLElement).closest(".song-list-overlay") && - !(event.target as HTMLElement).closest(".song-list-btn") - ) + showSongList && + event.target instanceof HTMLDivElement && + event.target.classList.contains("song-list-overlay") + ) { + event.preventDefault(); showSongList = false; + } + } + + function handleKeydown(event: KeyboardEvent): void { + if (showSongList && event.key === "Escape") { + event.preventDefault(); + showSongList = false; + } } - +
@@ -530,18 +540,21 @@ .song-list-overlay { position: fixed; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - padding: 2rem; + inset: 0; background: color-mix( in srgb, - var(--background-color) 90%, + var(--background-color) 95%, transparent ); display: flex; justify-content: center; align-items: center; + z-index: 1; + } + + .song-list { + overflow-y: auto; + max-height: 100%; } .game-over { From 4172b72c9d96352abe705494d40950cc07b0e3ca Mon Sep 17 00:00:00 2001 From: VINXIS Date: Wed, 2 Jul 2025 08:24:27 -0600 Subject: [PATCH 100/108] db utils and case insensitive comparisons --- src/database/utils.ts | 14 ++++++++++++++ src/pages/api/tunicwilds/index.ts | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/database/utils.ts diff --git a/src/database/utils.ts b/src/database/utils.ts new file mode 100644 index 0000000..c1f1d12 --- /dev/null +++ b/src/database/utils.ts @@ -0,0 +1,14 @@ +import { SQL, sql } from "drizzle-orm"; +import type { SQLiteColumn } from "drizzle-orm/sqlite-core"; + +export function lower(column: SQLiteColumn | string): SQL { + return sql`lower(${column})`; +} + +export function upper(column: SQLiteColumn | string): SQL { + return sql`upper(${column})`; +} + +export function like(column: SQLiteColumn | string, value: string): SQL { + return sql`${column} LIKE ${value}`; +} \ No newline at end of file diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts index cc5bf88..72f41cd 100644 --- a/src/pages/api/tunicwilds/index.ts +++ b/src/pages/api/tunicwilds/index.ts @@ -7,6 +7,7 @@ import db, { retryIfDbBusy } from "../../../database/db"; import { Member, Tunicwild } from "../../../database/schema"; import { paginationQuery, parseNumberCursor } from "../../../server/pagination"; import { writeBlobToFile } from "../../../server/webApi"; +import { lower } from "../../../database/utils"; export const prerender = false; @@ -151,8 +152,8 @@ export const POST: APIRoute = async (context) => { .from(Tunicwild) .where( and( - eq(Tunicwild.title, title.trim()), - eq(Tunicwild.game, game.trim()) + eq(lower(Tunicwild.title), lower(title.trim())), + eq(lower(Tunicwild.game), lower(game.trim())) ) ) .get(); From 236fbeca8526222fc98299c56a16883ea202142f Mon Sep 17 00:00:00 2001 From: VINXIS Date: Wed, 2 Jul 2025 08:25:18 -0600 Subject: [PATCH 101/108] case insensitive searching in GET too --- src/pages/api/tunicwilds/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts index 72f41cd..d3e98b2 100644 --- a/src/pages/api/tunicwilds/index.ts +++ b/src/pages/api/tunicwilds/index.ts @@ -17,15 +17,15 @@ export const GET: APIRoute = async ({ url }) => { const game = params.get("game"); if (game) - conditions.push(eq(Tunicwild.game, game)); + conditions.push(eq(lower(Tunicwild.game), lower(game))); const composer = params.get("composer"); if (composer) - conditions.push(eq(Tunicwild.composer, composer)); + conditions.push(eq(lower(Tunicwild.composer), lower(composer))); const title = params.get("title"); if (title) - conditions.push(eq(Tunicwild.title, title)); + conditions.push(eq(lower(Tunicwild.title), lower(title))); const releaseDate = params.get("releaseDate"); if (releaseDate) { From 8f7dd8327006f550ef3d783e0dbf85eaa6d7c421 Mon Sep 17 00:00:00 2001 From: VINXIS Date: Wed, 2 Jul 2025 08:41:44 -0600 Subject: [PATCH 102/108] retry mechanism for timed out reqs --- src/pages/api/tunicwilds/index.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts index d3e98b2..bf2cd91 100644 --- a/src/pages/api/tunicwilds/index.ts +++ b/src/pages/api/tunicwilds/index.ts @@ -213,9 +213,33 @@ export const POST: APIRoute = async (context) => { ); 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); + // If timed out, retry a few times + if (uploadResponse.status === 408 || uploadResponse.status === 504) { + for (let i = 0; i < 3; i++) { + const retryResponse = await fetch( + `${process.env.TUNICWILDS_UPLOAD_URL}/upload/${tunicwild.id}`, + { + method: "PUT", + body: file, + headers: { + "Content-Length": file.size.toString(), + }, + } + ); + if (retryResponse.ok) + break; + + if (i === 2) { + // Rollback database entry if all retries fail + await db.delete(Tunicwild).where(eq(Tunicwild.id, tunicwild.id)); + return jsonError(`Failed to upload file after retries: ${retryResponse.statusText}`, 500); + } + } + } else { + // 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); + } } } else { return jsonError("", 500); From 9c43ba380608de09099c6db74ac12d16ec8f796a Mon Sep 17 00:00:00 2001 From: VINXIS Date: Wed, 2 Jul 2025 08:43:10 -0600 Subject: [PATCH 103/108] move tunicwilds.astro to index --- src/pages/{tunicwilds.astro => tunicwilds/index.astro} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/pages/{tunicwilds.astro => tunicwilds/index.astro} (68%) diff --git a/src/pages/tunicwilds.astro b/src/pages/tunicwilds/index.astro similarity index 68% rename from src/pages/tunicwilds.astro rename to src/pages/tunicwilds/index.astro index 9a817a6..25d3392 100644 --- a/src/pages/tunicwilds.astro +++ b/src/pages/tunicwilds/index.astro @@ -1,8 +1,8 @@ --- -import Layout from "../layouts/Layout.astro"; -import TunicwildsApp from "../components/Tunicwilds/Tunicwilds.svelte"; -import db from "../database/db"; -import { Tunicwild } from "../database/schema"; +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; From 2b49c3509a63ec2e0ede14978ec7ecebd265eb6b Mon Sep 17 00:00:00 2001 From: VINXIS Date: Wed, 2 Jul 2025 08:45:00 -0600 Subject: [PATCH 104/108] pause -> stop --- src/components/Tunicwilds/Tunicwilds.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 49f6df8..a2093f6 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -217,6 +217,8 @@ function pauseClip() { if (!audioElement) return; + + audioElement.currentTime = 0; audioElement.pause(); isPlaying = false; } @@ -429,7 +431,7 @@ onclick={isPlaying ? pauseClip : playClip} class="play-btn" > - {isPlaying ? "⏸️ Pause" : "▢️ Play"} + {isPlaying ? "⏹️ Stop" : "▢️ Play"}
From ca519f9e71cfd87bf65a351aea663c15e96bcdd6 Mon Sep 17 00:00:00 2001 From: VINXIS Date: Fri, 4 Jul 2025 15:19:23 -0600 Subject: [PATCH 105/108] send jsonerror if no tunicwilds file --- src/pages/tunicwilds/today.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts index bcfbe7c..014336e 100644 --- a/src/pages/tunicwilds/today.ts +++ b/src/pages/tunicwilds/today.ts @@ -40,7 +40,13 @@ export const GET: APIRoute = async (context) => { return jsonError("Invalid timestamp"); } - const dailyInfo: DailyInfo = JSON.parse(await readFile(process.env.TUNICWILDS_DAILY_FILE!, "utf8")); + let dailyInfo: DailyInfo; + try { + dailyInfo = JSON.parse(await readFile(process.env.TUNICWILDS_DAILY_FILE!, "utf8")); + } catch (error) { + console.error("Failed to read or parse daily tunicwilds file:", error); + return jsonError("Failed to read daily tunicwilds file"); + } const tunicwildDateString = date.toISOString().slice(0, 10); const tunicwildInfo = dailyInfo.active.find((info) => info.date === tunicwildDateString); From 4bf448effe271f5e151ba8866aced5f8ce1d9b94 Mon Sep 17 00:00:00 2001 From: VINXIS Date: Fri, 4 Jul 2025 21:56:39 -0600 Subject: [PATCH 106/108] remove extrahint and rebalance when hints show --- db/migration/019-add-tunicwilds.sql | 1 - src/components/Tunicwilds/Tunicwilds.svelte | 7 ------- src/database/factories.ts | 1 - src/database/schema.ts | 1 - src/pages/tunicwilds/today.ts | 6 +----- 5 files changed, 1 insertion(+), 15 deletions(-) diff --git a/db/migration/019-add-tunicwilds.sql b/db/migration/019-add-tunicwilds.sql index ee52dac..0779d9c 100644 --- a/db/migration/019-add-tunicwilds.sql +++ b/db/migration/019-add-tunicwilds.sql @@ -6,6 +6,5 @@ CREATE TABLE "Tunicwild" ( "game" text NOT NULL, "releaseDate" text NOT NULL, "officialLink" text NOT NULL, - "extraHint" text NOT NULL, FOREIGN KEY ("memberDiscord") REFERENCES "Member" ("discord") ); diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index a2093f6..4cfc39e 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -451,13 +451,6 @@
{/if} - {#if songData.tunicwild.extraHint} -
- πŸ’‘ Hint: - {songData.tunicwild.extraHint} -
- {/if} - {#if songData.tunicwild.game != null && !guesses.some((guess) => guess?.result === "correctGame")}
πŸ’‘ Hint: This song is from diff --git a/src/database/factories.ts b/src/database/factories.ts index 3368765..309d43c 100644 --- a/src/database/factories.ts +++ b/src/database/factories.ts @@ -90,7 +90,6 @@ export const TunicwildFactory = defineFactory(Tunicwild, { game: () => faker.commerce.productName(), releaseDate: () => faker.date.past(), officialLink: () => faker.internet.url(), - extraHint: () => faker.lorem.sentence(), }); export const WordFactory = defineFactory(Word, { diff --git a/src/database/schema.ts b/src/database/schema.ts index 3f9f29e..6d205bb 100644 --- a/src/database/schema.ts +++ b/src/database/schema.ts @@ -154,7 +154,6 @@ export const Tunicwild = sqliteTable("Tunicwild", { game: text().notNull(), releaseDate: date().notNull(), officialLink: text().notNull(), - extraHint: text().notNull(), }); export const TunicwildRelations = relations(Tunicwild, ({ one }) => ({ diff --git a/src/pages/tunicwilds/today.ts b/src/pages/tunicwilds/today.ts index 014336e..5f79e49 100644 --- a/src/pages/tunicwilds/today.ts +++ b/src/pages/tunicwilds/today.ts @@ -90,15 +90,11 @@ export const GET: APIRoute = async (context) => { } else { const guessCount = tunicwildsSession.guesses.length; - if (guessCount >= 3) { + if (guessCount >= 2) { responseBody.tunicwild.releaseDate = tunicwild.releaseDate; } if (guessCount >= 4) { - responseBody.tunicwild.extraHint = tunicwild.extraHint; - } - - if (guessCount >= 5) { responseBody.tunicwild.game = tunicwild.game; } } From 369b7e7592a55c98c799096fadea53d483914b06 Mon Sep 17 00:00:00 2001 From: VINXIS Date: Fri, 4 Jul 2025 22:04:55 -0600 Subject: [PATCH 107/108] make every song link instead of game --- src/components/Tunicwilds/Tunicwilds.svelte | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/Tunicwilds/Tunicwilds.svelte b/src/components/Tunicwilds/Tunicwilds.svelte index 4cfc39e..d673fdb 100644 --- a/src/components/Tunicwilds/Tunicwilds.svelte +++ b/src/components/Tunicwilds/Tunicwilds.svelte @@ -306,15 +306,13 @@ {#each Object.entries(gameGroupedSongList) as [game, songs]}
-

- {game} -

+

{game}

From 3531958e3c90b8b93d62de98199002b3c81b2e31 Mon Sep 17 00:00:00 2001 From: VINXIS Date: Fri, 4 Jul 2025 22:21:10 -0600 Subject: [PATCH 108/108] remove extrahint in post req --- src/pages/api/tunicwilds/index.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pages/api/tunicwilds/index.ts b/src/pages/api/tunicwilds/index.ts index bf2cd91..b9b983a 100644 --- a/src/pages/api/tunicwilds/index.ts +++ b/src/pages/api/tunicwilds/index.ts @@ -62,7 +62,6 @@ export const POST: APIRoute = async (context) => { const game = formData.get("game"); const releaseDate = formData.get("releaseDate"); const officialLink = formData.get("officialLink"); - const extraHint = formData.get("extraHint"); const file = formData.get("file") as File; if (typeof discord !== "string" || !discord) { @@ -83,9 +82,6 @@ export const POST: APIRoute = async (context) => { if (typeof officialLink !== "string" || !officialLink.trim()) { return jsonError("Official link 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"); } @@ -110,9 +106,6 @@ export const POST: APIRoute = async (context) => { if (game.length > MAX_LENGTH) { return jsonError(`Game name too long (max ${MAX_LENGTH} characters)`); } - if (extraHint.length > MAX_LENGTH) { - return jsonError(`Extra hint too long (max ${MAX_LENGTH} characters)`); - } // File validation with size limits const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB @@ -175,7 +168,6 @@ export const POST: APIRoute = async (context) => { game: game.trim(), releaseDate: releaseDateParsed, officialLink: officialLink.trim(), - extraHint: extraHint.trim(), }) .returning() .get()