jsoncrush gone again

This commit is contained in:
Kalle
2026-08-05 09:40:30 +03:00
parent 8eb52707d9
commit 7195e7c588
8 changed files with 72 additions and 64 deletions

View File

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

View File

@@ -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);
});
});

View File

@@ -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<typeof ingestVodMatchSchema>;
export type IngestVodPrefill = z.infer<typeof ingestVodPrefillSchema>;

View File

@@ -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),
})),
};
}

View File

@@ -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}]}'],
]);
});
});

View File

@@ -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({

View File

@@ -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",

8
pnpm-lock.yaml generated
View File

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