taiko: add TAL/EGTS import script

This commit is contained in:
Pinapelz
2025-11-12 03:05:47 -08:00
parent bc0d7cd368
commit 58876529c3
5 changed files with 400 additions and 3 deletions
@@ -1,6 +1,6 @@
import type { SupportedGame } from "../../types/game";
interface DonerHirobaModalProps {
interface DonderHirobaModalProps {
isOpen: boolean;
onClose: () => void;
game: SupportedGame | undefined;
@@ -12,7 +12,7 @@ const TaikoDonderHirobaModal = ({
onClose,
game,
renderAsCard,
}: DonerHirobaModalProps) => {
}: DonderHirobaModalProps) => {
if (renderAsCard) {
return (
<div className="bg-slate-800 rounded-lg border border-slate-700 p-6 hover:border-violet-500 transition-colors">
@@ -0,0 +1,133 @@
import type { SupportedGame } from "../../types/game";
interface TaikoEGTSModalProps {
isOpen: boolean;
onClose: () => void;
game: SupportedGame | undefined;
renderAsCard?: () => void;
}
const TaikoEGTSModal = ({
isOpen,
onClose,
game,
renderAsCard,
}: TaikoEGTSModalProps) => {
if (renderAsCard) {
return (
<div className="bg-slate-800 rounded-lg border border-slate-700 p-6 hover:border-[#533166] transition-colors">
<div className="w-12 h-12 bg-[#533166]/20 rounded-lg flex items-center justify-center mb-4">
<svg
className="w-6 h-6 text-[#8a5a9c]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</div>
<h4 className="text-white font-semibold mb-2">EGTS/TaikoLocalServer Import</h4>
<p className="text-slate-400 text-sm mb-4">
Import Play History from EGTS Legacy WebUI or a local TaikoLocalServer instance
</p>
<button
onClick={renderAsCard}
className="w-full bg-[#533166] hover:bg-[#4a2c5a] text-white py-2 px-3 sm:px-4 rounded-md text-sm sm:text-base font-medium transition-colors"
>
Export EGTS Play History
</button>
</div>
);
}
if (!isOpen) return null;
const handleClose = () => {
onClose();
};
if (game === undefined) {
return "Sorry, due to some extreme error the game you're looking for doesn't seem to exist...";
}
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
onClick={handleClose}
/>
{/* Modal */}
<div className="flex min-h-full items-center justify-center p-4">
<div className="relative bg-slate-900 rounded-lg border border-slate-700 w-full max-w-xl p-6 shadow-xl">
{/* Header */}
<div className="mb-6">
<h3 className="text-xl font-bold text-white mb-2">
Import {game.formattedName} EGTS Data
</h3>
<p className="text-slate-400 text-sm">
Follow the instructions below to import your data
</p>
</div>
{/* Warning */}
<div className="mb-6 rounded-md bg-[#533166]/10 border border-[#533166]/20 p-3">
<p className="text-sm text-[#8a5a9c]">
Before exporting ensure that the display language of Songs is set to Japanese, or is consistent with your other imports so that your data is consistent.
</p>
</div>
{/* Instructions */}
<div className="mb-4 rounded-md bg-slate-800 border border-slate-700 p-4">
<h4 className="text-sm font-semibold text-slate-300 mb-2">
Instructions:
</h4>
<ol className="text-sm text-slate-400 space-y-1 list-decimal list-inside">
<li>Log into your the WebUI. Its assumed you already know the link if you're using this import method</li>
<li>
Install the appropriate userscript to your browser (use an extension such
as Tampermonkey).
</li>
{/* Additional Info */}
<div className="my-2 rounded-md bg-[#533166]/10 border border-[#533166]/20 p-3">
<p className="text-sm text-[#8a5a9c]">
<a
href="https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/taiko/egts/taiko_egts_to_mirage.user.js"
className="underline"
>
EGTS/TAL Export Userscript
</a>
</p>
</div>
<li>
On the WebUI, navigate to the "Play History" page and refresh.
</li>
<li>
A button will appear on the page that you can click to start the scraping process.
</li>
<li>Upload the resulting JSON file into Mirage using the Batch-Manual Upload functionality</li>
<li>Verify that all data has been imported correctly</li>
</ol>
</div>
{/* Actions */}
<div className="flex justify-center">
<button
onClick={handleClose}
className="px-6 py-2 bg-[#533166] hover:bg-[#4a2c5a] text-white rounded-md font-medium transition-colors"
>
Got it
</button>
</div>
</div>
</div>
</div>
);
};
export default TaikoEGTSModal;
+18 -1
View File
@@ -8,13 +8,14 @@ import { uploadScore } from "../utils/scoreUpload";
import { NavBar } from "../components/NavBar";
import { EamusementUserscriptCard } from "../components/modals/EamusementUserscriptModal";
import { FlowerUserscriptCard } from "../components/modals/FlowerUserscriptModal";
import TaikoDonderHirobaModal from "../components/modals/TaikoDonderHirobaModal";
const JsonUploadModal = lazy(() => import("../components/modals/JsonUploadModal"));
const EamusementUserscriptModal = lazy(() => import("../components/modals/EamusementUserscriptModal"));
const DivaNetModal = lazy(() => import("../components/modals/DivaNetModal"));
const MusicDiverModal = lazy(() => import("../components/modals/MusicDiverModal"));
const FlowerUserscriptModal = lazy(() => import("../components/modals/FlowerUserscriptModal"));
const TaikoDonderHirobaModal = lazy(() => import("../components/modals/TaikoDonderHirobaModal"));
const TaikoEGTSModal = lazy(() => import("../components/modals/TaikoEGTSModal"));
type ModalType = 'json' | 'dancerush' | 'dancearound' | 'divanet' | 'musicdiver' | 'nostalgia' | 'reflecbeat' | 'taiko';
@@ -207,6 +208,12 @@ const Import = () => {
game={supportedGames.find((g) => g.internalName === selectedGame)}
renderAsCard={() => setOpenModal('taiko')}
/>
<TaikoEGTSModal
isOpen={false}
onClose={() => {}}
game={supportedGames.find((g) => g.internalName === selectedGame)}
renderAsCard={() => setOpenModal('taiko')}
/>
</>
);
default:
@@ -405,6 +412,16 @@ const Import = () => {
}
/>
)}
{openModal === 'taiko' && (
<TaikoEGTSModal
isOpen={true}
onClose={() => setOpenModal(null)}
game={
supportedGames.find((g) => g.internalName === selectedGame) ||
undefined
}
/>
)}
</Suspense>
</div>
);
+4
View File
@@ -0,0 +1,4 @@
# EGTS/Taiko Local Server to Mirage
This script was primarily tested with EGTS. However it should also work with a TAL instance since the EGTS Legacy WebUI is roughly the same.
Please ensure that the song display language is consistent with the rest of your scores before importing
@@ -0,0 +1,243 @@
// ==UserScript==
// @name Taiko EGTS to Mirage
// @namespace http://tampermonkey.net/
// @version 2025-01-12
// @author You
// @match https://legacy.egts.ca/Users/*
// @match http://localhost
// @icon https://www.google.com/s2/favicons?sz=64&domain=egts.ca
// @grant none
// ==/UserScript==
(function() {
'use strict';
const DIFFICULTY_MAP = {
"difficulty_Easy.webp": "EASY",
"difficulty_Normal.webp": "NORMAL",
"difficulty_Hard.webp": "HARD",
"difficulty_Oni.webp": "ONI",
"difficulty_UraOni.webp": "URA_ONI"
};
const CROWN_MAP = {
"crown_None.webp": "NOT CLEAR",
"crown_Gold.webp": "FULL COMBO",
"crown_Clear.webp": "CLEAR",
"crown_Dondaful.webp": "DONDERFUL COMBO",
};
const LAMP_MAP = {
"rank_White.webp": "IKI 1",
"rank_Bronze.webp": "IKI 2",
"rank_Silver.webp": "IKI 3",
"rank_Gold.webp": "MIYABI 1",
"rank_Sakura.webp": "MIYABI 2",
"rank_Purple.webp": "MIYABI 3",
"rank_Dondaful.webp": "KIWAMI",
};
function waitForAppAndInject() {
// Wait until Blazor app actually loads DOM elements
const appRoot = document.querySelector(".mud-table, #app, .scoreCard");
if (appRoot) {
if (!document.getElementById("egts-export-btn")) createButton();
} else {
setTimeout(waitForAppAndInject, 1000);
}
}
function createButton() {
const btn = document.createElement("button");
btn.id = "egts-export-btn";
btn.textContent = "Export Taiko EGTS → Mirage";
Object.assign(btn.style, {
position: "fixed",
bottom: "10px",
right: "10px",
zIndex: 99999,
padding: "10px 14px",
background: "#0078d4",
color: "white",
border: "none",
borderRadius: "8px",
cursor: "pointer",
boxShadow: "0 2px 8px rgba(0,0,0,0.3)"
});
btn.onclick = exportData;
document.body.appendChild(btn);
console.log("[EGTS Exporter] Button added to page.");
}
function parseTimestamp(dateString) {
const date = new Date(dateString + ' GMT');
return date.getTime();
}
function exportData() {
const langConfirm = confirm("Before exporting, please confirm:\n\nIs your song language setting configured correctly in EGTS?\n\nClick OK to proceed with export, or Cancel to check your language settings first.");
if (!langConfirm) {
return;
}
const results = [];
const tables = document.querySelectorAll(".mud-table.mud-table-outlined.tagged-toolbar");
console.log(`Found ${tables.length} tables`);
tables.forEach((table, tableIndex) => {
const toolbar = table.querySelector('.mud-toolbar .mud-typography-body1');
let timestamp = 0;
if (toolbar) {
const timestampText = toolbar.textContent?.trim();
if (timestampText) {
timestamp = parseTimestamp(timestampText);
console.log(`Table ${tableIndex}: Timestamp "${timestampText}" -> ${timestamp}`);
}
}
const rows = table.querySelectorAll("tr.mud-table-row");
console.log(`Table ${tableIndex}: Found ${rows.length} rows`);
rows.forEach((row, rowIndex) => {
if (row.closest('thead')) return;
const cells = row.querySelectorAll('td.mud-table-cell');
if (cells.length < 5) return; // Skip if not enough columns
console.log(`Table ${tableIndex}, Row ${rowIndex}:`, cells.length, 'cells');
const songCell = cells[0];
const titleElement = songCell.querySelector('p.mud-typography-body2');
const artistElement = songCell.querySelector('span.mud-typography-caption');
const title = titleElement?.textContent?.trim() || "";
const artist = artistElement?.textContent?.trim() || "";
console.log(`Table ${tableIndex}, Row ${rowIndex}: Title="${title}", Artist="${artist}"`);
let difficulty = null;
const difficultyImage = songCell.querySelector('svg image[href*="difficulty_"]');
if (difficultyImage) {
const href = difficultyImage.getAttribute('href');
const difficultyFile = href.split('/').pop();
difficulty = DIFFICULTY_MAP[difficultyFile] || null;
}
let level = null;
if (cells[3]) {
const levelElement = cells[3].querySelector('span.mud-typography-caption');
if (levelElement) {
const levelText = levelElement.textContent?.trim();
level = levelText ? parseInt(levelText) : null;
}
}
let score = null;
if (cells[4]) {
const scoreText = cells[4].textContent?.replace(/[^\d]/g, "");
score = scoreText ? parseInt(scoreText) : null;
}
let crown = null;
if (cells[5]) {
const crownImage = cells[5].querySelector('img[src*="crown_"]');
if (crownImage) {
const src = crownImage.src;
const crownFile = src.split('/').pop();
crown = CROWN_MAP[crownFile] || null;
}
}
let lamp = null;
if (cells[6]) {
const rankImage = cells[6].querySelector('img[src*="rank_"]');
if (rankImage) {
const src = rankImage.src;
const rankFile = src.split('/').pop();
lamp = LAMP_MAP[rankFile] || null;
}
}
let good = null, ok = null, bad = null;
if (cells[7]) {
const goodText = cells[7].textContent?.trim();
good = goodText ? parseInt(goodText) : null;
}
if (cells[8]) {
const okText = cells[8].textContent?.trim();
ok = okText ? parseInt(okText) : null;
}
if (cells[9]) {
const badText = cells[9].textContent?.trim();
bad = badText ? parseInt(badText) : null;
}
let drumroll = null, maxCombo = null;
if (cells[10]) {
const drumrollText = cells[10].textContent?.trim();
drumroll = drumrollText ? parseInt(drumrollText) : null;
}
if (cells[11]) {
const maxComboText = cells[11].textContent?.trim();
maxCombo = maxComboText ? parseInt(maxComboText) : null;
}
console.log(`Table ${tableIndex}, Row ${rowIndex}: Difficulty="${difficulty}", Level=${level}, Score=${score}, Crown="${crown}", Lamp="${lamp}"`);
console.log(`Table ${tableIndex}, Row ${rowIndex}: Good=${good}, OK=${ok}, Bad=${bad}, Drumroll=${drumroll}, MaxCombo=${maxCombo}`);
const result = {
title,
timestamp,
artist,
difficulty,
level,
crown_rank: crown,
score_rank: lamp,
score,
judgements: {
good: good,
ok: ok,
bad: bad
},
optional: {
pound: drumroll,
combo: maxCombo
}
};
if (title) {
results.push(result);
console.log(`Added score for: ${title}`);
}
});
});
console.log(`Extracted ${results.length} scores`);
if (results.length === 0) {
alert("No scores found. Please make sure you're on a page with score data.");
return;
}
const exportObj = {
meta: {
game: "taiko",
playtype: "Single",
service: "EGTS Play History Export"
},
scores: results
};
const blob = new Blob([JSON.stringify(exportObj, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "mirage_egts_export.json";
a.click();
URL.revokeObjectURL(url);
alert(`Successfully exported ${results.length} scores!`);
}
waitForAppAndInject();
})();