diff --git a/app/features/cv/components/sendou-upload.ts b/app/features/cv/components/sendou-upload.ts index 1b39532ac..613bd21c1 100644 --- a/app/features/cv/components/sendou-upload.ts +++ b/app/features/cv/components/sendou-upload.ts @@ -1,15 +1,18 @@ /** * "Upload to sendou.ink" link for a fully processed VoD: the detected events - * are grouped into per-match rows (src/core/vod-matches.ts) and packed, - * JSONCrush-compressed, into the `ingest` search param of sendou.ink's - * /vods/new form, which prefills a new VoD from them (the user adds the - * YouTube URL/title/date and fixes any misreads before submitting). + * are grouped into per-match rows (src/core/vod-matches.ts) and packed into + * the `ingest` search param of sendou.ink's /vods/new form (an `SP.json` + * param the search-params module compresses), which prefills a new VoD from + * them (the user adds the YouTube URL/title/date and fixes any misreads + * before submitting). * * The VoD type is auto-detected: footage containing the casted 8-player * spectator map screen is a CAST VoD; anything else leaves the form's default * type untouched. */ -import JSONCrush from "jsoncrush"; +import type { IngestVodPrefill } from "~/features/ingest/ingest-vod-schemas"; +import { vodsNewSearchParams } from "~/features/vods/vods-search-params"; +import { newVodPage } from "~/utils/urls"; import { MINIMAP_EVENT_TYPE, type MinimapData, @@ -17,8 +20,6 @@ import { import type { DetectedEvent } from "../core/detectors/types"; import { buildVodMatches } from "../core/vod-matches"; -const VODS_NEW_PATH = "/vods/new"; - /** * GET query params ride the request line, and servers/proxies commonly cap * that around 8-16 KB. A VoD long enough to blow past this needs a POST @@ -45,10 +46,10 @@ export function sendouUpload(events: readonly DetectedEvent[]): SendouUpload { (event.data as MinimapData).spectator, ); - const payload = isCast ? { type: "CAST", matches } : { matches }; - const params = new URLSearchParams(); - params.set("ingest", JSONCrush.crush(JSON.stringify(payload))); - const result = `${VODS_NEW_PATH}?${params.toString()}`; + const payload: IngestVodPrefill = isCast + ? { type: "CAST", matches } + : { matches }; + const result = vodsNewSearchParams.href(newVodPage(), { ingest: payload }); if (result.length > MAX_URL_LENGTH) { return { url: null, diff --git a/app/features/ingest/core/VodMatches.test.ts b/app/features/ingest/core/VodMatches.test.ts index a90644bf7..1fac3b66c 100644 --- a/app/features/ingest/core/VodMatches.test.ts +++ b/app/features/ingest/core/VodMatches.test.ts @@ -1,5 +1,5 @@ -import JSONCrush from "jsoncrush"; import { describe, expect, it } from "vitest"; +import { vodsNewSearchParams } from "~/features/vods/vods-search-params"; import { type IngestVodMatchInput, ingestVodPrefillSchema, @@ -61,20 +61,20 @@ describe("prefillVodMatches", () => { expect(parsed.success).toBe(false); }); - it("accepts the JSONCrushed `ingest` search param the CV VoD tab sends", () => { - // what the CV VoD tab's "Upload as VoD" button puts in the param - // (~/features/cv/components/sendou-upload.ts): a crushed - // { type?, matches } payload - const param = JSONCrush.crush( - JSON.stringify({ type: "CAST", matches: [testMatch()] }), + it("accepts the `ingest` search param the CV VoD tab sends", () => { + // what the CV VoD tab's "Upload as VoD" button puts in the URL + // (~/features/cv/components/sendou-upload.ts): a { type?, matches } + // payload in the compressed `ingest` param + const href = vodsNewSearchParams.href("/vods/new", { + ingest: { type: "CAST", matches: [testMatch()] }, + }); + + const { ingest } = vodsNewSearchParams.parse( + new URL(href, "https://sendou.ink"), ); - const parsed = ingestVodPrefillSchema.safeParse( - JSON.parse(JSONCrush.uncrush(param)), - ); - - expect(parsed.success).toBe(true); - expect(parsed.data!.type).toBe("CAST"); - expect(prefillVodMatches(parsed.data!.matches)).toHaveLength(1); + expect(ingest).not.toBeNull(); + expect(ingest!.type).toBe("CAST"); + expect(prefillVodMatches(ingest!.matches)).toHaveLength(1); }); }); diff --git a/app/features/ingest/ingest-vod-schemas.ts b/app/features/ingest/ingest-vod-schemas.ts index 524c07bba..29605cf2d 100644 --- a/app/features/ingest/ingest-vod-schemas.ts +++ b/app/features/ingest/ingest-vod-schemas.ts @@ -25,11 +25,12 @@ const ingestVodMatchSchema = z.object({ }); /** - * The CV VoD tab's "Upload as VoD" button packs this, JSONCrushed, into - * /vods/new's `ingest` search param to prefill the form: the detected match - * rows, minus the submission fields (YouTube URL, title, date) the user - * fills in the form. `type` is sent only when the scan auto-detected it - * (spectator map screens → CAST); absent means the form's default. + * The CV VoD tab's "Upload as VoD" button packs this into /vods/new's + * `ingest` search param (an `SP.json` param, compressed by the search-params + * module) to prefill the form: the detected match rows, minus the submission + * fields (YouTube URL, title, date) the user fills in the form. `type` is + * sent only when the scan auto-detected it (spectator map screens → CAST); + * absent means the form's default. */ export const ingestVodPrefillSchema = z.object({ type: z.enum(videoMatchTypes).optional(), @@ -37,3 +38,4 @@ export const ingestVodPrefillSchema = z.object({ }); export type IngestVodMatchInput = z.infer; +export type IngestVodPrefill = z.infer; diff --git a/app/features/vods/loaders/vods.new.server.ts b/app/features/vods/loaders/vods.new.server.ts index 731e4de2a..e1f43a32d 100644 --- a/app/features/vods/loaders/vods.new.server.ts +++ b/app/features/vods/loaders/vods.new.server.ts @@ -1,11 +1,10 @@ -import JSONCrush from "jsoncrush"; import type { LoaderFunctionArgs } from "react-router"; import { requireUser } from "~/features/auth/core/user.server"; import { type PrefillVodMatch, prefillVodMatches, } from "~/features/ingest/core/VodMatches"; -import { ingestVodPrefillSchema } from "~/features/ingest/ingest-vod-schemas"; +import type { IngestVodPrefill } from "~/features/ingest/ingest-vod-schemas"; import { notFoundIfNullish } from "~/utils/remix.server"; import * as VodRepository from "../VodRepository.server"; import type { videoMatchTypes } from "../vods-constants"; @@ -48,28 +47,22 @@ export interface VodPrefill { } /** - * Parses the `ingest` search param the emberz VoD parser's "Upload to - * sendou.ink" button fills (a JSONCrushed ingestVodPrefillSchema payload, see + * Maps the `ingest` search param the CV VoD tab's "Upload as VoD" button + * fills (an ingestVodPrefillSchema payload, see * ~/features/ingest/ingest-vod-schemas) into form-prefill data. Detection - * misses stay null for the user to fill; a malformed param is ignored. + * misses stay null for the user to fill; a malformed param has already + * decoded to null. */ -function vodPrefillFromIngestParam(param: string | null): VodPrefill | null { - if (!param) return null; +function vodPrefillFromIngestParam( + ingest: IngestVodPrefill | null, +): VodPrefill | null { + if (!ingest) return null; - try { - const parsed = ingestVodPrefillSchema.safeParse( - JSON.parse(JSONCrush.uncrush(param)), - ); - if (!parsed.success) return null; - - return { - type: parsed.data.type ?? null, - matches: prefillVodMatches(parsed.data.matches).map((match) => ({ - ...match, - startsAt: secondsToHoursMinutesSecondString(match.startsAt), - })), - }; - } catch { - return null; - } + return { + type: ingest.type ?? null, + matches: prefillVodMatches(ingest.matches).map((match) => ({ + ...match, + startsAt: secondsToHoursMinutesSecondString(match.startsAt), + })), + }; } diff --git a/app/features/vods/vods-search-params.test.ts b/app/features/vods/vods-search-params.test.ts index dce2fa5dd..4a0baea2c 100644 --- a/app/features/vods/vods-search-params.test.ts +++ b/app/features/vods/vods-search-params.test.ts @@ -36,10 +36,22 @@ describe("vodsSearchParams", () => { }); describe("vodsNewSearchParams", () => { + const ingestMatch = { + startsAt: 30, + mode: "SZ" as const, + modeAssumed: true, + stage: 0 as const, + weapons: [40 as const, null], + }; + it("round-trips", () => { assertRoundTrips(vodsNewSearchParams, { vod: [null, 1, 12345], - ingest: [null, "abc", '{"type":"TOURNAMENT"}'], + ingest: [ + null, + { matches: [ingestMatch] }, + { type: "CAST", matches: [ingestMatch, ingestMatch] }, + ], }); }); @@ -50,6 +62,11 @@ describe("vodsNewSearchParams", () => { ["1.5"], ["abc"], ]); + assertDecodesToDefault(vodsNewSearchParams, "ingest", [ + ["not json"], + ['{"matches":[]}'], + ['{"matches":[{"startsAt":-1}]}'], + ]); }); }); diff --git a/app/features/vods/vods-search-params.ts b/app/features/vods/vods-search-params.ts index 7755a0ed9..c2cf34c79 100644 --- a/app/features/vods/vods-search-params.ts +++ b/app/features/vods/vods-search-params.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { ingestVodPrefillSchema } from "~/features/ingest/ingest-vod-schemas"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids"; import * as SearchParams from "~/modules/search-params/search-params"; @@ -28,7 +29,10 @@ export const vodsSearchParams = SearchParams.define({ export const vodsNewSearchParams = SearchParams.define({ vod: SP.param(z.number().int().positive().nullable(), { loader: true }), - ingest: SP.param(z.string().nullable(), { loader: true }), + ingest: SP.json(ingestVodPrefillSchema.nullable(), { + loader: true, + compress: true, + }), }); export const vodsVodSearchParams = SearchParams.define({ diff --git a/package.json b/package.json index 9bf6f3b89..2a1e538da 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "i18next-http-backend": "4.0.0", "ics": "3.12.0", "isbot": "5.2.1", - "jsoncrush": "1.1.8", "kysely": "0.29.0", "lucide-react": "1.27.0", "markdown-to-jsx": "9.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79f6b9d81..12dd3db7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,9 +94,6 @@ importers: isbot: specifier: 5.2.1 version: 5.2.1 - jsoncrush: - specifier: 1.1.8 - version: 1.1.8 kysely: specifier: 0.29.0 version: 0.29.0(patch_hash=a3e94339939b1be5b70610601e96fff19ed5678aab525de24b52dfc5212c686a) @@ -3706,9 +3703,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsoncrush@1.1.8: - resolution: {integrity: sha512-lvIMGzMUA0fjuqwNcxlTNRq2bibPZ9auqT/LyGdlR5hvydJtA/BasSgkx4qclqTKVeTidrJvsS/oVjlTCPQ4Nw==} - jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -8404,8 +8398,6 @@ snapshots: json5@2.2.3: {} - jsoncrush@1.1.8: {} - jsonfile@6.2.0: dependencies: universalify: 2.0.1