fix: repair SUS timing and note classification for 18 charts
Publish GitHub Pages output / publish (push) Successful in 47s
Publish GitHub Pages output / publish (push) Successful in 47s
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import { createDecipheriv, createHash } from 'node:crypto';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { prepareHolodoriDbSource } from './holodoridb-source.mjs';
|
||||
import { susScoreWeight } from './sus-score-weight.mjs';
|
||||
|
||||
const OCTO_API_KEY = 'B46OtKlGGHoz6sxbOWDe3VUvBsagXxr5av38IQIKUKo=';
|
||||
const OCTO_CLIENT_KEY = 'CwFhQ+S5m4nERWVaq5oFZIP0cZLc0j7O/zllG0UYVNo=';
|
||||
@@ -94,7 +96,13 @@ function decryptResource(bytes, name) {
|
||||
}
|
||||
|
||||
const requested = process.argv.slice(2);
|
||||
if (requested.length === 0) throw new Error('SUS 파일명 또는 곡 ID를 지정하세요. 예: m0344 m0348 m0527');
|
||||
if (requested.length === 0) throw new Error('SUS 파일명, 곡 ID 또는 --all을 지정하세요.');
|
||||
const collectAll = requested.length === 1 && requested[0] === '--all';
|
||||
if (!collectAll && requested.some((value) => !/^m\d{4}$|^chart_m\d{4}_(easy|normal|hard|expert)\.sus$/.test(value))) {
|
||||
throw new Error('잘못된 SUS 수집 대상입니다.');
|
||||
}
|
||||
// Pin every reviewed locale before downloading. Never use stale refs after a failed fetch.
|
||||
const sources = Object.fromEntries(['eng', 'kor', 'jpn'].map((locale) => [locale, prepareHolodoriDbSource(locale)]));
|
||||
const requestedNames = new Set(requested.flatMap((value) =>
|
||||
/^m\d{4}$/.test(value) ? ['easy', 'normal', 'hard', 'expert'].map((difficulty) => `chart_${value}_${difficulty}.sus`) : [value]
|
||||
));
|
||||
@@ -102,20 +110,48 @@ const requestedNames = new Set(requested.flatMap((value) =>
|
||||
const catalogResponse = await fetch(OCTO_LIST_URL, { headers: { 'X-OCTO-KEY': OCTO_API_KEY, 'X-APP-OCTO-KEY': OCTO_CLIENT_KEY } });
|
||||
if (!catalogResponse.ok) throw new Error(`Octo 카탈로그 요청 실패: ${catalogResponse.status}`);
|
||||
const catalog = decryptCatalog(Buffer.from(await catalogResponse.arrayBuffer()));
|
||||
const selected = catalog.entries.filter((entry) => requestedNames.has(entry.name));
|
||||
if (selected.length !== requestedNames.size) {
|
||||
const selected = catalog.entries.filter((entry) => collectAll ? /^chart_m\d{4}_(easy|normal|hard|expert)\.sus$/.test(entry.name) : requestedNames.has(entry.name));
|
||||
if (!collectAll && selected.length !== requestedNames.size) {
|
||||
const found = new Set(selected.map((entry) => entry.name));
|
||||
throw new Error(`카탈로그에 없는 SUS: ${[...requestedNames].filter((name) => !found.has(name)).join(', ')}`);
|
||||
}
|
||||
|
||||
const outputDirectory = path.resolve('data/raw/sus');
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
for (const entry of selected.sort((left, right) => left.name.localeCompare(right.name))) {
|
||||
const response = await fetch(catalog.urlFormat.replace('{o}', entry.objectName));
|
||||
if (!response.ok) throw new Error(`${entry.name} 다운로드 실패: ${response.status}`);
|
||||
const plaintext = decryptResource(Buffer.from(await response.arrayBuffer()), entry.name);
|
||||
if (!plaintext.toString('utf8', 0, 9).startsWith('#CREATE_')) throw new Error(`${entry.name} 복호화 결과가 SUS가 아닙니다.`);
|
||||
await writeFile(path.join(outputDirectory, entry.name), plaintext);
|
||||
console.log(`${entry.name} ${createHash('sha256').update(plaintext).digest('hex')}`);
|
||||
if (!selected.length || new Set(selected.map((entry) => entry.name)).size !== selected.length) throw new Error('비어 있거나 중복된 SUS 카탈로그입니다.');
|
||||
const results = [];
|
||||
let cursor = 0;
|
||||
console.log(`Octo revision ${catalog.revisionId}: ${selected.length}개 SUS 수집 시작`);
|
||||
async function worker() {
|
||||
while (cursor < selected.length) {
|
||||
const entry = selected[cursor++];
|
||||
const url = catalog.urlFormat.replace('{o}', entry.objectName);
|
||||
let error;
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(120000) });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const plaintext = decryptResource(Buffer.from(await response.arrayBuffer()), entry.name);
|
||||
const text = plaintext.toString('utf8');
|
||||
if (!text.startsWith('#CREATE_')) throw new Error('복호화 결과가 SUS가 아닙니다.');
|
||||
const songId = text.match(/^#MUSIC_ID\s+(\S+)/m)?.[1];
|
||||
if (songId !== entry.name.split('_')[1]) throw new Error('SUS 곡 ID가 파일명과 다릅니다.');
|
||||
let weight = null;
|
||||
let validationError = null;
|
||||
try { weight = susScoreWeight(text); } catch (validation) { validationError = validation.message; }
|
||||
await writeFile(path.join(outputDirectory, entry.name), plaintext);
|
||||
results.push({ name: entry.name, url, bytes: plaintext.length, sha256: createHash('sha256').update(plaintext).digest('hex'), weight, validationError });
|
||||
error = null;
|
||||
break;
|
||||
} catch (failure) { error = failure; }
|
||||
}
|
||||
if (error) results.push({ name: entry.name, url, error: error.message });
|
||||
if (results.length % 50 === 0 || results.length === selected.length) console.log(`${results.length}/${selected.length}개 처리`);
|
||||
}
|
||||
}
|
||||
console.log(`Octo revision ${catalog.revisionId}: ${selected.length}개 SUS 저장 완료`);
|
||||
await Promise.all(Array.from({ length: 6 }, worker));
|
||||
results.sort((a, b) => a.name.localeCompare(b.name));
|
||||
await writeFile(path.join(outputDirectory, collectAll ? 'collection-manifest.json' : 'selected-collection-manifest.json'), JSON.stringify({ collectedAt: new Date().toISOString(), revisionId: catalog.revisionId, sources, files: results }, null, 2));
|
||||
const failed = results.filter((result) => result.error);
|
||||
if (failed.length) throw new Error(`SUS ${failed.length}개 수집 실패: ${failed.map((result) => result.name).join(', ')}`);
|
||||
console.log(`Octo revision ${catalog.revisionId}: ${results.length}개 SUS 저장 완료, 점수 구조 검증 경고 ${results.filter((result) => result.validationError).length}개`);
|
||||
|
||||
Reference in New Issue
Block a user