mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-06 13:58:05 +03:00
fix: update ongeki seed scripts to v3 (#1739)
* wip * fmt * the rest * fix the table script * fix remaster handling in scrape-sdvx-in * perfect shining
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 <absolute-path-to-vgmstream-cli> -d <path-to-opts> -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`.
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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<OngekiSong, number> = 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();
|
||||
@@ -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 <path-to-music-json>")
|
||||
.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);
|
||||
|
||||
@@ -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 <path-to-vgmstream-cli>")
|
||||
.requiredOption("-d, --data <path-with-AXXX>")
|
||||
.requiredOption("-g, --game <ongeki|chunithm>")
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user