mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-20 01:55:26 -05:00
jsoncrush gone again
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}]}'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
8
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user