From 96a9c0e37323c364df969b033cfc599c8a81205e Mon Sep 17 00:00:00 2001 From: zk Date: Sat, 23 May 2026 22:22:55 +0100 Subject: [PATCH] zk/bms fixes (#1569) * fix: hahahahahaha * fix: follow redirs in bms table sync --- typescript/common/src/constants/bms-tables.ts | 17 -- .../server/src/actions/bms-table-sync.test.ts | 38 +++- .../server/src/actions/bms-table-sync.ts | 162 +++++++++++++++++- 3 files changed, 198 insertions(+), 19 deletions(-) diff --git a/typescript/common/src/constants/bms-tables.ts b/typescript/common/src/constants/bms-tables.ts index 9e8e61834..abdc10b8f 100644 --- a/typescript/common/src/constants/bms-tables.ts +++ b/typescript/common/src/constants/bms-tables.ts @@ -283,7 +283,6 @@ export const BMS_TABLES: Array = [ prefix: "乱打", description: "16th Chordstream table, sl0-sl12 difficulty", }, - { name: "PMS ● (controller)", game: "pms-controller", @@ -302,14 +301,6 @@ export const BMS_TABLES: Array = [ url: "https://pmsdifficulty.xxxxxxxx.jp/insane_PMSdifficulty.html", colour: COLOUR_SET.maroon, }, - { - name: "PMS PLv (controller)", - game: "pms-controller", - prefix: "PLv", - asciiPrefix: "pmsPLv", - description: "PMS tableFolders key PLv.", - url: "https://github.com/TachiServices/Tachi", - }, { name: "PMS ● (keyboard)", game: "pms-keyboard", @@ -328,12 +319,4 @@ export const BMS_TABLES: Array = [ url: "https://pmsdifficulty.xxxxxxxx.jp/insane_PMSdifficulty.html", colour: COLOUR_SET.maroon, }, - { - name: "PMS PLv (keyboard)", - game: "pms-keyboard", - prefix: "PLv", - asciiPrefix: "pmsPLv", - description: "PMS tableFolders key PLv.", - url: "https://github.com/TachiServices/Tachi", - }, ]; diff --git a/typescript/server/src/actions/bms-table-sync.test.ts b/typescript/server/src/actions/bms-table-sync.test.ts index 54a48d8d4..5d631b27a 100644 --- a/typescript/server/src/actions/bms-table-sync.test.ts +++ b/typescript/server/src/actions/bms-table-sync.test.ts @@ -4,7 +4,7 @@ import { seedUser } from "#test-utils/pg-fixtures"; import { CreateChartID } from "tachi-common"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ACTION_BMSTableSync } from "./bms-table-sync"; +import { ACTION_BMSTableSync, extractBmstableRedirectTarget } from "./bms-table-sync"; const { testMd5, mockLoadBMSTable, fakeTable } = vi.hoisted(() => { const md5 = "c".repeat(32); @@ -34,6 +34,42 @@ vi.mock("tachi-common", async (importOriginal) => { }; }); +describe("extractBmstableRedirectTarget", () => { + it("resolves darksabun github.io migration stubs to darksabun.club", () => { + const html = ` +`; + + expect( + extractBmstableRedirectTarget( + "https://darksabun.github.io/table/archive/insane1/", + html, + ), + ).toBe("https://darksabun.club/table/archive/insane1/"); + }); + + it("preserves the source path for bare-domain meta refresh targets", () => { + const html = ''; + + expect( + extractBmstableRedirectTarget( + "https://darksabun.github.io/table/archive/normal1/", + html, + ), + ).toBe("https://darksabun.club/table/archive/normal1/"); + }); + + it("follows absolute window.location.replace targets", () => { + const html = `window.location.replace("https://example.com/table/header.json");`; + + expect(extractBmstableRedirectTarget("https://old.example/table/", html)).toBe( + "https://example.com/table/header.json", + ); + }); +}); + describe("ACTION_BMSTableSync", () => { const songNewID = "song-bms-action-test"; const chartId = CreateChartID(); diff --git a/typescript/server/src/actions/bms-table-sync.ts b/typescript/server/src/actions/bms-table-sync.ts index 03222576a..06b00da87 100644 --- a/typescript/server/src/actions/bms-table-sync.ts +++ b/typescript/server/src/actions/bms-table-sync.ts @@ -4,7 +4,9 @@ import { MakeAction } from "#lib/actions/actions"; import { rebuildFolderChartLookup } from "#lib/folders/rebuild-folder-chart-lookup"; import { log } from "#lib/log/log"; import { DeorphanBmsIfInOrphanChartPg } from "#lib/orphan-queue/deorphan-bms-pg"; +import { Env } from "#lib/setup/config"; import DB from "#services/pg/db"; +import fetch from "#utils/fetch"; import { FormatBMSTables } from "#utils/misc"; import { FindBMSChartOnHashInGame } from "#utils/queries/charts"; import { IsUserAdmin } from "#utils/user"; @@ -23,6 +25,151 @@ import { const UPDATE_CHUNK = 500; +const BMS_TABLE_META_RE = /]*http-equiv=["']refresh["'][^>]*content=["']([^"']+)["']/iu.exec(html) ?? + /]*content=["']([^"']+)["'][^>]*http-equiv=["']refresh["']/iu.exec(html); + if (refreshMatch?.[1]) { + const urlPart = refreshMatch[1].match(/url\s*=\s*(.+)$/iu)?.[1]?.trim(); + if (urlPart) { + const target = new URL(urlPart.replace(/^['"]|['"]$/gu, ""), sourceUrl); + const source = new URL(sourceUrl); + if ((target.pathname === "/" || target.pathname === "") && source.pathname !== "/") { + target.pathname = source.pathname; + target.search = source.search; + target.hash = source.hash; + } + return target.href; + } + } + + return null; +} + +/** Follow HTTP redirects and HTML/JS migration stubs until a BMS table page is reachable. */ +async function resolveBmstableTableUrl(url: string, hopsLeft = 5): Promise { + if (Env.NODE_ENV === "test") { + return url; + } + + if (hopsLeft <= 0) { + throw new Error(`Too many redirects while resolving BMS table URL ${url}.`); + } + + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch BMS table URL ${url}: HTTP ${res.status}.`); + } + + const text = await res.text(); + const resolvedHttpUrl = res.url; + + if ( + isJsonTableHeader(res.headers.get("content-type"), text) || + responseLooksLikeBmstablePage(text) + ) { + return resolvedHttpUrl; + } + + const redirectTarget = extractBmstableRedirectTarget(url, text); + if (!redirectTarget || redirectTarget === url) { + return resolvedHttpUrl; + } + + log.info({ from: url, to: redirectTarget }, "Following BMS table redirect."); + return resolveBmstableTableUrl(redirectTarget, hopsLeft - 1); +} + +/** + * When `LoadBMSTable` fails, re-fetch the URL and log response shape hints + * (redirect stubs, missing bmstable meta tag, etc.). + */ +async function logBmstableLoadFailureDebug(tableInfo: BMSTableInfo, err: unknown): Promise { + try { + const res = await fetch(tableInfo.url); + const text = await res.text(); + const contentType = res.headers.get("content-type"); + const hasBmstableMeta = BMS_TABLE_META_RE.test(text); + const looksLikeJsRedirect = /window\.location\.(?:replace|href)/u.test(text); + const looksLikeMovedPage = + /site has moved|사이트가 이전되었습니다|Redirecting\.\.\./iu.test(text); + + const hints: Array = []; + if (res.url !== tableInfo.url) { + hints.push(`HTTP redirect resolved to ${res.url}.`); + } + if (!hasBmstableMeta && looksLikeJsRedirect) { + hints.push( + "Response looks like a JavaScript redirect page, but no redirect target could be resolved.", + ); + } + if (!hasBmstableMeta && looksLikeMovedPage) { + hints.push( + "Response looks like a site-migration landing page without a bmstable meta tag.", + ); + } + if (!hasBmstableMeta && !looksLikeJsRedirect && !looksLikeMovedPage) { + hints.push("Response is HTML but has no bmstable meta tag."); + } + + log.error( + { + err, + tableName: tableInfo.name, + tableGame: tableInfo.game, + tablePrefix: tableInfo.prefix, + tableUrl: tableInfo.url, + finalUrl: res.url, + fetchStatus: res.status, + contentType, + responseBytes: text.length, + hasBmstableMeta, + looksLikeJsRedirect, + looksLikeMovedPage, + hints, + responsePreview: text.slice(0, 500).replace(/\s+/gu, " "), + }, + `BMS table load diagnostics for ${tableInfo.name} (${tableInfo.url}).`, + ); + } catch (probeErr) { + log.error( + { err, probeErr, tableUrl: tableInfo.url }, + `BMS table load diagnostics probe failed for ${tableInfo.name}.`, + ); + } +} + type BmsChartData = ChartDocumentData["bms-7k"]; function stripTableFoldersKeySql(prefix: string) { @@ -204,7 +351,20 @@ async function ImportTableLevels( } export async function UpdateTable(tableInfo: BMSTableInfo) { - const table = await LoadBMSTable(tableInfo.url); + let table; + try { + const loadUrl = await resolveBmstableTableUrl(tableInfo.url); + if (loadUrl !== tableInfo.url) { + log.info( + { tableName: tableInfo.name, from: tableInfo.url, to: loadUrl }, + "Resolved BMS table URL after redirect.", + ); + } + table = await LoadBMSTable(loadUrl); + } catch (err) { + await logBmstableLoadFailureDebug(tableInfo, err); + throw err; + } if (table.head.symbol !== tableInfo.prefix) { throw new Error(