diff --git a/typescript/db/src/generated/public/ImportType.ts b/typescript/db/src/generated/public/ImportType.ts index f1c5dc502..503ec3e86 100644 --- a/typescript/db/src/generated/public/ImportType.ts +++ b/typescript/db/src/generated/public/ImportType.ts @@ -3,49 +3,49 @@ /** Represents the enum public.import_type */ type ImportType = + | 'file/batch-manual' + | 'file/eamusement-iidx-csv' + | 'file/eamusement-sdvx-csv' + | 'file/mypagescraper-player-csv' + | 'file/mypagescraper-records-csv' + | 'file/pli-iidx-csv' + | 'file/solid-state-squad' | 'api/arc-iidx' | 'api/arc-sdvx' - | 'api/cg-dev-iidx' - | 'api/cg-dev-jubeat' - | 'api/cg-dev-museca' - | 'api/cg-dev-popn' - | 'api/cg-dev-sdvx' - | 'api/cg-gan-iidx' - | 'api/cg-gan-jubeat' - | 'api/cg-gan-museca' - | 'api/cg-gan-popn' - | 'api/cg-gan-sdvx' - | 'api/cg-nag-iidx' - | 'api/cg-nag-jubeat' - | 'api/cg-nag-museca' - | 'api/cg-nag-popn' - | 'api/cg-nag-sdvx' + | 'api/mer-iidx' | 'api/eag-iidx' | 'api/eag-sdvx' | 'api/flo-iidx' | 'api/flo-sdvx' - | 'api/mer-iidx' | 'api/min-sdvx' | 'api/myt-chunithm' | 'api/myt-maimaidx' | 'api/myt-ongeki' | 'api/myt-wacca' - | 'file/batch-manual' - | 'file/eamusement-iidx-csv' - | 'file/eamusement-sdvx-csv' - | 'file/import-class' - | 'file/mypagescraper-player-csv' - | 'file/mypagescraper-records-csv' - | 'file/pli-iidx-csv' - | 'file/solid-state-squad' + | 'api/cg-dev-jubeat' + | 'api/cg-dev-museca' + | 'api/cg-dev-popn' + | 'api/cg-dev-sdvx' + | 'api/cg-gan-jubeat' + | 'api/cg-gan-museca' + | 'api/cg-gan-popn' + | 'api/cg-gan-sdvx' + | 'api/cg-nag-jubeat' + | 'api/cg-nag-museca' + | 'api/cg-nag-popn' + | 'api/cg-nag-sdvx' | 'ir/barbatos' | 'ir/beatoraja' | 'ir/direct-manual' - | 'ir/fervidex' | 'ir/fervidex-static' - | 'ir/kshook-sv6c' + | 'ir/fervidex' | 'ir/kshook-sv6c-static' + | 'ir/kshook-sv6c' | 'ir/lr2hook' - | 'ir/usc'; + | 'ir/usc' + | 'file/import-class' + | 'api/cg-dev-iidx' + | 'api/cg-gan-iidx' + | 'api/cg-nag-iidx'; export default ImportType; diff --git a/typescript/seeds-scripts/rerunners/ongeki/README.md b/typescript/seeds-scripts/rerunners/ongeki/README.md index 0e6792213..54fff04d4 100644 --- a/typescript/seeds-scripts/rerunners/ongeki/README.md +++ b/typescript/seeds-scripts/rerunners/ongeki/README.md @@ -1,13 +1,16 @@ 1. Have a working Tachi Docker container. 2. Parse music data - 1. Generate `music.json` (you should know how) without omnimixes or custom charts, then put it in `seeds/scripts/rerunners/ongeki/ongeki`. - 2. In the container, in `seeds/scripts/rerunners/ongeki`, run `bun parse-music-data.ts`. + 1. Generate `music.json` (you should know how) without omnimixes or custom charts, then put it in `seeds/scripts/personal`. + 2. In the container, in `seeds/scripts/rerunners/ongeki`, run `bun parse-music-data.ts -m ../../personal/music.json`. 3. Check if `parse-music-data-output.json` doesn't list anything unexpected, then delete it. 3. Parse song durations - 1. Download `vgmstream-cli` and put it somewhere in the container. - 2. Run `bun parse-song-duration.ts -v -d -g ongeki`. + 1. Download `vgmstream-cli` and put it in `seeds/scripts/personal`. + 2. Put opts in `seeds/scripts/personal`. + 3. Run `bun parse-song-duration.ts -v /tachi/typescript/seeds-scripts/personal/vgmstream-cli -d ../../personal/vgmstream-cli -g ongeki`. 4. Scrape sdvx.in links (optional) 1. Run `bun scrape-sdvx-in.ts`. 2. Fix any missing entries manually in `seeds/collections/charts-ongeki.json` (refer to `songs-ongeki.json` for `songID`s). 5. Manually add search terms in `songs-ongeki.json`, especially romanizations (optional) -6. Run `just db-load-seeds`. +6. Run `bun sync-bonus-track-data.ts` if any bonus tracks are present. +7. Run `bun check-data.ts` and verify the output. +8. Run `just db-load-seeds`. diff --git a/typescript/seeds-scripts/rerunners/ongeki/add-tables-and-folders.ts b/typescript/seeds-scripts/rerunners/ongeki/add-tables-and-folders.ts index 2b8669b2d..ecb672148 100644 --- a/typescript/seeds-scripts/rerunners/ongeki/add-tables-and-folders.ts +++ b/typescript/seeds-scripts/rerunners/ongeki/add-tables-and-folders.ts @@ -1,9 +1,9 @@ import { Command } from "commander"; -import { type FolderDocument, GetGameConfig } from "tachi-common"; -import { type SeedFolderRow } from "tachi-common/lib/folder-slug"; +import { GetGameConfig } from "tachi-common"; +import { SEEDS_FolderDocument, SEEDS_TableDocument } from "tachi-common/types/seeds-documents-zod"; import crypto from "crypto"; -import { CreateFolderID, CreateTableID, CreateLegacyFolderID, MutateCollection } from "../../util"; +import { CreateFolderID, CreateTableID, MutateCollection } from "../../util"; const LEVELS = [ "0", @@ -32,7 +32,14 @@ const LEVELS = [ "15", "15+", ]; -const DIFFICULTIES = ["BASIC", "ADVANCED", "EXPERT", "MASTER", "LUNATIC", "Re:MASTER"]; +const DIFFICULTIES = [ + ["BASIC", "basic"], + ["ADVANCED", "advanced"], + ["EXPERT", "expert"], + ["MASTER", "master"], + ["LUNATIC", "lunatic"], + ["Re:MASTER", "lunatic-remaster"], +]; const GENRES = [ ["POPS&ANIME", "pna"], ["niconico", "niconico"], @@ -55,96 +62,88 @@ if (!versionName) { ); } -const newFolders: SeedFolderRow[] = []; -const levelFolderIDs: string[] = []; -const difficultyFolderIDs: string[] = []; +const newFolders: SEEDS_FolderDocument[] = []; +const levelFolderSlugs: string[] = []; +const difficultyFolderSlugs: string[] = []; +const genreFolderSlugs: string[] = []; -// !!!!!!!!!!!!!!!!!!!!!!!!!! -// !! Not yet updated for V3! -// !!!!!!!!!!!!!!!!!!!!!!!!!! +for (const level of LEVELS) { + const slug = `${level}-${version.toLowerCase()}`; + newFolders.push({ + game: "ongeki", + id: CreateFolderID(), + inactive: false, + legacyFolderID: `F${crypto.randomBytes(32).toString("hex")}`, + searchTerms: [], + slug, + title: `Level ${level} (${versionName})`, + versionFilter: [version], + where: `chart.data->>'isBonusTrack' = 'false' AND chart.level = '${level}'`, + }); + levelFolderSlugs.push(slug); +} -// for (const level of LEVELS) { -// const data = { -// level, -// versions: version, -// isBonusTrack: false, -// }; -// const legacyFolderID = CreateLegacyFolderID(data, "ongeki", "Single"); +for (const [fullName, safeName] of DIFFICULTIES) { + const slug = `${version.toLowerCase()}-${safeName}`; + newFolders.push({ + game: "ongeki", + id: CreateFolderID(), + inactive: false, + legacyFolderID: `F${crypto.randomBytes(32).toString("hex")}`, + searchTerms: [], + slug, + title: `${fullName} (${versionName})`, + versionFilter: [version], + where: `chart.data->>'isBonusTrack' = 'false' AND chart.difficulty = '${fullName}'`, + }); + difficultyFolderSlugs.push(slug); +} -// levelFolderIDs.push(folderID); - -// newFolders.push({ -// data, -// id: CreateFolderID(), -// legacyFolderID, -// game: "ongeki", -// inactive: false, -// playtype: "Single", -// searchTerms: [], -// title: `Level ${level} (${versionName})`, -// type: "charts", -// }); -// } - -// for (const difficulty of DIFFICULTIES) { -// const data = { difficulty, versions: version }; - -// if (difficulty === "LUNATIC") { -// data["data¬isReMaster"] = false; -// } else if (difficulty === "Re:MASTER") { -// data["data¬isReMaster"] = true; -// data.difficulty = "LUNATIC"; -// } else { -// data["data¬inGameID"] = { -// "~lt": 7000, -// }; -// } - -// const folderID = CreateLegacyFolderID(data, "ongeki", "Single"); - -// difficultyFolderIDs.push(folderID); - -// newFolders.push({ -// data, -// folderID, -// game: "ongeki", -// inactive: false, -// playtype: "Single", -// searchTerms: [], -// title: `${difficulty} (${versionName})`, -// type: "charts", -// }); -// } +for (const [fullName, safeName] of GENRES) { + const slug = `g-${safeName}-${version.toLowerCase()}`; + newFolders.push({ + game: "ongeki", + id: CreateFolderID(), + inactive: false, + legacyFolderID: `F${crypto.randomBytes(32).toString("hex")}`, + searchTerms: [], + slug, + title: `${fullName} (${versionName})`, + versionFilter: [version], + where: `(song.data->>'genre')::text = '${fullName}' AND (chart.difficulty = 'MASTER' OR chart.difficulty = 'Re:MASTER')`, + }); + genreFolderSlugs.push(slug); +} MutateCollection("tables.json", (ts) => { - ts.push( - // { - // default: false, - // description: `Levels for O.N.G.E.K.I. in ${versionName}.`, - // folders: levelFolderIDs, - // game: "ongeki", - // inactive: false, - // playtype: "Single", - // tableID: `ongeki-Single-${version}-levels`, - // title: `O.N.G.E.K.I. (${versionName})`, - // }, - // { - // default: false, - // description: `Difficulties for O.N.G.E.K.I. in ${versionName}.`, - // folders: difficultyFolderIDs, - // game: "ongeki", - // inactive: false, - // playtype: "Single", - // tableID: `ongeki-Single-${version}-difficulties`, - // title: `O.N.G.E.K.I. (${versionName}) (Difficulties)`, - // }, + (ts as SEEDS_TableDocument[]).push( + { + default: false, + description: `Levels for O.N.G.E.K.I. in ${versionName}.`, + folders: levelFolderSlugs, + game: "ongeki", + inactive: false, + id: CreateTableID(), + legacyTableID: `ongeki-Single-${version}-levels`, + title: `O.N.G.E.K.I. (${versionName})`, + }, + { + default: false, + description: `Difficulties for O.N.G.E.K.I. in ${versionName}.`, + folders: difficultyFolderSlugs, + game: "ongeki", + inactive: false, + id: CreateTableID(), + legacyTableID: `ongeki-Single-${version}-difficulties`, + title: `O.N.G.E.K.I. (${versionName}) (Difficulties)`, + }, { default: false, description: `Genres for O.N.G.E.K.I. in ${versionName}.`, - folders: GENRES.map(([_, g]) => `g-${g}-${version.toLowerCase()}`), + folders: genreFolderSlugs, game: "ongeki", - id: CreateTableID(), inactive: false, + id: CreateTableID(), legacyTableID: `ongeki-Single-${version}-genres`, title: `O.N.G.E.K.I. (${versionName}) (Genres)`, }, @@ -153,18 +152,4 @@ MutateCollection("tables.json", (ts) => { return ts; }); -for (const [fullName, safeName] of GENRES) { - newFolders.push({ - game: "ongeki", - id: CreateFolderID(), - inactive: false, - legacyFolderID: `F${crypto.randomBytes(32).toString("hex")}`, - searchTerms: [], - slug: `g-${safeName}-${version.toLowerCase()}`, - title: `${fullName} (${versionName})`, - versionFilter: [version], - where: `(song.data->>'genre')::text = '${fullName}' AND (chart.difficulty = 'MASTER' OR chart.difficulty = 'Re:MASTER')`, - }); -} - -MutateCollection("folders.json", (fs) => [...fs, ...newFolders]); +MutateCollection("folders.json", (fs) => [...(fs as SEEDS_FolderDocument[]), ...newFolders]); diff --git a/typescript/seeds-scripts/rerunners/ongeki/check-data.ts b/typescript/seeds-scripts/rerunners/ongeki/check-data.ts new file mode 100644 index 000000000..28d7c6c8d --- /dev/null +++ b/typescript/seeds-scripts/rerunners/ongeki/check-data.ts @@ -0,0 +1,69 @@ +import { ReadCollection } from "../../util"; +import { SEEDS_ChartDocument, SEEDS_SongDocument } from "tachi-common"; + +type OngekiSong = SEEDS_SongDocument<"ongeki">; +type OngekiChart = SEEDS_ChartDocument<"ongeki">; + +const hasCJK = (str: string) => + str.match(/[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]/u); + +const main = () => { + const songs: OngekiSong[] = ReadCollection("songs-ongeki.json"); + const charts: OngekiChart[] = ReadCollection("charts-ongeki.json"); + + const missingDurations: OngekiSong[] = []; + const missingTerms: OngekiSong[] = []; + const missingChartViews: OngekiSong[] = []; + const missingIDs: Map = new Map(); + + for (const song of songs) { + if (song.data.duration === null) { + missingDurations.push(song); + } + if (hasCJK(song.title) && song.searchTerms.length === 0) { + missingTerms.push(song); + } + } + + for (const chart of charts) { + const song = songs.find((s) => s.id === chart.songID); + if (song === undefined) { + throw new Error(`Unable to find song ${chart.songID}`); + } + if ( + (chart.levelNum >= 12.7 || chart.levelNum === 0) && + chart.data.chartViewURL === undefined + ) { + missingChartViews.push(song); + } + if (chart.data.inGameID === null) { + missingIDs.set(song, (missingIDs.get(song) ?? 0) + 1); + } + } + + console.log( + `Missing chart views (${missingChartViews.length} total):\n${missingChartViews + .map((s) => `\t ${s.id} ${s.title}`) + .join("\n")}`, + ); + + console.log( + `Missing search terms (${missingTerms.length} total):\n${missingTerms + .map((s) => `\t ${s.id} ${s.title}`) + .join("\n")}`, + ); + + console.log( + `Missing durations (${missingDurations.length} total):\n${missingDurations + .map((s) => `\t ${s.id} ${s.title}`) + .join("\n")}`, + ); + + console.log( + `Missing IDs (${missingIDs.size} total):\n${[...missingIDs] + .map(([song, count]) => `\t ${song.id} ${song.title} [${count}]`) + .join("\n")}`, + ); +}; + +main(); diff --git a/typescript/seeds-scripts/rerunners/ongeki/parse-music-data.ts b/typescript/seeds-scripts/rerunners/ongeki/parse-music-data.ts index 0769c10ba..8a0f56b1e 100644 --- a/typescript/seeds-scripts/rerunners/ongeki/parse-music-data.ts +++ b/typescript/seeds-scripts/rerunners/ongeki/parse-music-data.ts @@ -1,18 +1,28 @@ -// usage: cd typescript/seeds-scripts/rerunners && bun ongeki/parse-music-data.ts +// usage: cd typescript/seeds-scripts/rerunners/ongeki && bun run parse-music-data.ts import fs from "fs/promises"; -import { type ChartDocument, type Difficulties, type SongDocument } from "tachi-common"; - +import { + CreateSongID, + SEEDS_ChartDocument, + SEEDS_SongDocument, + type Difficulties, +} from "tachi-common"; +import { Command } from "commander"; import { CreateChartID, ReadCollection, WriteCollection } from "../../util"; +import crypto from "crypto"; -type OngekiChart = ChartDocument<"ongeki">; -type OngekiSong = SongDocument<"ongeki">; +type OngekiChart = SEEDS_ChartDocument<"ongeki">; +type OngekiSong = SEEDS_SongDocument<"ongeki">; type Difficulty = Difficulties["ongeki"]; +const command = new Command() + .requiredOption("-m, --musicjson ") + .parse(process.argv); +const options = command.opts(); + const CURRENT_VERSION = "refresh"; const CURRENT_OMNIMIX = "refreshOmni"; const DRY_RUN = false; -const SOURCE = "ongeki/music.json"; interface Input { dataVersion: string; @@ -43,6 +53,7 @@ interface Changes { versions: string[]; rerates: string[]; renames: string[]; + ids: string[]; } const convertLevel = (chart: InputChart) => { @@ -73,9 +84,9 @@ const updateChart = (out: OngekiChart, input: InputChart, song: InputSong, chang changes.versions.push(`${song.name} ${diff}: ${CURRENT_OMNIMIX}`); out.versions.push(CURRENT_OMNIMIX); } - - if (input.difficulty === "LUNATIC") { - out.data.isReMaster = song.isReMaster; + if (out.data.inGameID === null) { + changes.ids.push(`${song.name} ${diff}: ${song.id}`); + out.data.inGameID = song.id; } }; @@ -83,9 +94,9 @@ const main = async () => { const charts: OngekiChart[] = ReadCollection("charts-ongeki.json"); const songs: OngekiSong[] = ReadCollection("songs-ongeki.json"); - const input: Input = JSON.parse((await fs.readFile(SOURCE)).toString()); + const input: Input = JSON.parse((await fs.readFile(options.musicjson)).toString()); - console.log(`Parsing ${SOURCE} ${input.dataVersion}`); + console.log(`Parsing ${options.musicjson} ${input.dataVersion}`); const changes: Changes = { songs: [], @@ -93,6 +104,7 @@ const main = async () => { versions: [], rerates: [], renames: [], + ids: [], }; for (const inputSong of input.music) { @@ -108,13 +120,15 @@ const main = async () => { song = songs.find((s) => s.title === inputSong.name && s.artist === inputSong.artist); if (song === undefined) { song = { - id: songs[songs.length - 1]!.id + 1, + id: CreateSongID(), + legacySongID: songs[songs.length - 1]!.legacySongID + 1, altTitles: [], searchTerms: [], artist: inputSong.artist, data: { genre: inputSong.genre as any, - } as any, + duration: null, + }, title: inputSong.name, }; changes.songs.push(song.title); @@ -139,6 +153,9 @@ const main = async () => { } for (const inputChart of inputSong.charts) { + if (inputChart.difficulty === "LUNATIC" && inputSong.isReMaster) { + inputChart.difficulty = "Re:MASTER"; + } let chart = charts.find( (c) => c.songID === song!.id && c.difficulty === inputChart.difficulty, ); @@ -150,24 +167,22 @@ const main = async () => { ver = `オンゲキ ${ver}`; } const newChart: OngekiChart = { - chartID: CreateChartID(), + id: CreateChartID(), + legacyChartID: `C${crypto.randomBytes(19).toString("hex")}`, songID: song.id, - game: "ongeki", data: { displayVersion: ver as any, inGameID: inputSong.id, maxPlatScore: inputChart.platinumScoreMax, + isBonusTrack: inputSong.id >= 7000 && inputSong.id < 8000, }, difficulty: inputChart.difficulty, isPrimary: true, level: convertLevel(inputChart), levelNum: parseFloat(inputChart.internalLevel), - playtype: "Single", versions: [CURRENT_VERSION, CURRENT_OMNIMIX], }; - if (newChart.difficulty === "LUNATIC") { - newChart.data.isReMaster = inputSong.isReMaster; - } + chart = newChart; changes.charts.push(`${song.title} ${chart.difficulty} ${chart.level}`); charts.push(chart); diff --git a/typescript/seeds-scripts/rerunners/ongeki/parse-song-duration.ts b/typescript/seeds-scripts/rerunners/ongeki/parse-song-duration.ts index c85d33a3f..15cf74d60 100644 --- a/typescript/seeds-scripts/rerunners/ongeki/parse-song-duration.ts +++ b/typescript/seeds-scripts/rerunners/ongeki/parse-song-duration.ts @@ -2,11 +2,11 @@ import { Command, InvalidArgumentError } from "commander"; import { XMLParser } from "fast-xml-parser"; import { type PathLike } from "fs"; -import { execFile } from "node:child_process"; -import fs from "node:fs/promises"; -import path from "node:path"; -import { promisify } from "node:util"; -import { type ChartDocument, type SongDocument } from "tachi-common"; +import { execFile } from "child_process"; +import fs from "fs/promises"; +import path from "path"; +import { promisify } from "util"; +import { SEEDS_ChartDocument, SEEDS_SongDocument } from "tachi-common"; import { ReadCollection, WriteCollection } from "../../util"; @@ -16,17 +16,23 @@ const command = new Command() .requiredOption("-v, --vgms ") .requiredOption("-d, --data ") .requiredOption("-g, --game ") + .option("-d, --debug") .parse(process.argv); const options = command.opts(); const vgmsPath = options.vgms; const optPath = options.data; +const debug = options.debug; const game = options.game; +const debugLog = (u: unknown) => { + if (debug) console.log(u); +}; + const readOpt = async ( musicPath: string, - charts: ChartDocument<"chunithm" | "ongeki">[], - songs: SongDocument<"chunithm" | "ongeki">[], + charts: SEEDS_ChartDocument<"chunithm" | "ongeki">[], + songs: SEEDS_SongDocument<"chunithm" | "ongeki">[], ) => { let musicDir: string[]; try { @@ -38,6 +44,7 @@ const readOpt = async ( const parser = new XMLParser(); for (const songPath of musicDir) { + debugLog(songPath); const p = path.join(musicPath, songPath); if (!(await fs.stat(p)).isDirectory()) { console.log(`${p}: not a directory`); @@ -60,18 +67,17 @@ const readOpt = async ( const chart = charts.find((c) => c.data.inGameID === id); if (chart === undefined) { - if (!(game === "chunithm" && id >= 8000)) { - console.error(`Song #${id}: not present in the seeds`); - } + console.error(`Song #${id}: not present in the seeds`); continue; } - const song = songs.find((s) => s.id === chart.song.id); + const song = songs.find((s) => s.id === chart.songID); if (song === undefined) { console.error(`Song #${id}: orphan`); continue; } - if ("duration" in song.data) { + if ("duration" in song.data && song.data.duration !== null) { + debugLog(`Skipping ${song.title}`); continue; } diff --git a/typescript/seeds-scripts/rerunners/ongeki/scrape-sdvx-in.ts b/typescript/seeds-scripts/rerunners/ongeki/scrape-sdvx-in.ts index 8a19f8db4..a230138dd 100644 --- a/typescript/seeds-scripts/rerunners/ongeki/scrape-sdvx-in.ts +++ b/typescript/seeds-scripts/rerunners/ongeki/scrape-sdvx-in.ts @@ -1,12 +1,12 @@ /* eslint-disable no-await-in-loop */ import readline from "readline"; -import { type ChartDocument, type SongDocument } from "tachi-common"; +import { SEEDS_ChartDocument, SEEDS_SongDocument } from "tachi-common"; import { ReadCollection, WriteCollection } from "../../util"; -type OngekiChart = ChartDocument<"ongeki">; -type OngekiSong = SongDocument<"ongeki">; +type OngekiChart = SEEDS_ChartDocument<"ongeki">; +type OngekiSong = SEEDS_SongDocument<"ongeki">; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const prompt = (query: string) => new Promise((resolve) => rl.question(query, resolve)); @@ -108,8 +108,19 @@ const scrape = async (url: string, allCharts: OngekiChart[], allSongs: OngekiSon } const song = songs[0]!; + + if (song.id === "S19d35e0fee643d12a81") { + // Perfect shining my behated + continue; + } + const properDiff = convertDiffProper(diff!); - const chart = allCharts.find((c) => c.songID === song.id && c.difficulty === properDiff); + const chart = allCharts.find( + (c) => + c.songID === song.id && + (c.difficulty === properDiff || + (properDiff === "LUNATIC" && c.difficulty === "Re:MASTER")), + ); if (chart) { chart.data.chartViewURL = link; @@ -131,13 +142,13 @@ const scrapeAll = async (charts: OngekiChart[], songs: OngekiSong[]) => { const listMissing = async (charts: OngekiChart[], songs: OngekiSong[], minLevel: number) => { for (const chart of charts) { - if (chart.data.inGameID >= 7000 && chart.data.inGameID < 8000) { + if (chart.data.isBonusTrack) { // Bonus track continue; } if (chart.levelNum >= minLevel && !chart.data.chartViewURL) { - const song = songs.find((s) => s.id === chart.song.id); - // @ts-expect-error The linter disagrees with the compiler + const song = songs.find((s) => s.id === chart.songID); + // @ts-expect-error what?? chart.data.chartViewURL = await prompt( `Missing: ${song?.artist} ${song?.title} ${chart.difficulty} ${chart.level}\n`, ); diff --git a/typescript/seeds-scripts/rerunners/ongeki/sync-bonus-track-data.ts b/typescript/seeds-scripts/rerunners/ongeki/sync-bonus-track-data.ts new file mode 100644 index 000000000..875e65373 --- /dev/null +++ b/typescript/seeds-scripts/rerunners/ongeki/sync-bonus-track-data.ts @@ -0,0 +1,109 @@ +import { ReadCollection, WriteCollection } from "../../util"; +import { SEEDS_ChartDocument, SEEDS_SongDocument } from "tachi-common"; + +type OngekiSong = SEEDS_SongDocument<"ongeki">; +type OngekiChart = SEEDS_ChartDocument<"ongeki">; + +const characters = { + "星咲 あかり": "Hoshizaki Akari", + "藤沢 柚子": "Fujisawa Yuzu", + "三角 葵": "Misumi Aoi", + "高瀬 梨緒": "Takase Rio", + "結城 莉玖": "Yūki Riku", + "藍原 椿": "Aihara Tsubaki", + "早乙女 彩華": "Saotome Ayaka", + "桜井 春菜": "Sakurai Haruna", + "柏木 咲姫": "Kashiwagi Saki", + "井之原 小星": "Inohara Koboshi", + "逢坂 茜": "Ōsaka Akane", + "九條 楓": "Kujō Kaede", + "珠洲島 有栖": "Suzushima Arisu", + "日向 千夏": "Hinata Chinatsu", + "柏木 美亜": "Kashiwagi Mia", + "東雲 つむぎ": "Shinonome Tsumugi", + "皇城 セツナ": "Sumeragi Setsuna", + "式宮 舞菜": "Shikimiya Mana", + "式宮 碧音": "Shikimiya Aone", +}; + +// Songs that have search terms and don't need a romanization +const searchTermBlacklist = ["Iudicium “Apocalypsis Mix”"]; + +const isBonusTrack = (song: OngekiSong, charts: OngekiChart[]) => { + const chart = charts.find((ch) => ch.songID === song.id); + return chart?.data.isBonusTrack; +}; + +const splitTitle = (song: OngekiSong) => { + const match = song.title.match(/(.+) +-(.+)ソロver\.-/u); + if (match === null || match.length !== 3) { + throw new Error(`Invalid bonus track title: ${song.title}`); + } + const [, title, rawCharacter] = match; + return { + title: title!.trim(), + rawCharacter: rawCharacter!, + }; +}; + +const findOriginalSong = (originalTitle: string, songs: OngekiSong[]) => { + const results = songs.filter( + // The song Dramatic uses a different question mark in the OG title and bonus titles + (s) => s.title.replace("?", "?") === originalTitle.replace("?", "?"), + ); + if (results.length !== 1) { + throw new Error(`Invalid bonus track: ${originalTitle}`); + } + return results[0]!; +}; + +const main = () => { + const songs: OngekiSong[] = ReadCollection("songs-ongeki.json"); + const charts: OngekiChart[] = ReadCollection("charts-ongeki.json"); + + for (const bonusSong of songs) { + if (!isBonusTrack(bonusSong, charts)) { + continue; + } + + const { title, rawCharacter } = splitTitle(bonusSong); + const originalSong = findOriginalSong(title, songs); + + bonusSong.data.flavorGenre = originalSong.data.flavorGenre; + + if (!(rawCharacter in characters)) { + throw new Error(`Unknown character: ${rawCharacter}`); + } + + const character = rawCharacter as keyof typeof characters; + + const fixedTitle = title.replace(/?/gu, "?").replace(/…/gu, "...").replace(/!/gu, "!"); + + if (originalSong.searchTerms.length > 0 && !searchTermBlacklist.includes(title)) { + bonusSong.searchTerms = [ + `${originalSong.searchTerms[0]} -${characters[character]} Solo ver.-`, + ...originalSong.searchTerms.slice(1), + ]; + } else { + bonusSong.searchTerms = [ + `${fixedTitle} -${characters[character]} Solo ver.-`, + ...originalSong.searchTerms, + ]; + } + + for (const chart of charts.filter((ch) => ch.songID === bonusSong.id)) { + const originalChart = charts.find( + (ch) => ch.songID === originalSong.id && ch.difficulty === chart.difficulty, + ); + if (originalChart === undefined) { + throw new Error(`Cannot find ${originalSong.title} ${chart.difficulty}`); + } + chart.data.chartViewURL = originalChart.data.chartViewURL; + } + } + + WriteCollection(`songs-ongeki.json`, songs); + WriteCollection(`charts-ongeki.json`, charts); +}; + +main();