aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-11-12 03:05:47 -0800
committerPinapelz <yukais@pinapelz.com>2025-11-12 03:05:47 -0800
commit58876529c38ee279e935c1cf3e204d2017a37a2e (patch)
treecca3f731466bad5f9571966f7df52a857baf4fd1
parentbc0d7cd3682aa1ec880fdfe18dcacbffbc2a0f96 (diff)
taiko: add TAL/EGTS import script
-rw-r--r--frontend/src/components/modals/TaikoDonderHirobaModal.tsx4
-rw-r--r--frontend/src/components/modals/TaikoEGTSModal.tsx133
-rw-r--r--frontend/src/pages/Import.tsx19
-rw-r--r--scripts/taiko/egts/README.md4
-rw-r--r--scripts/taiko/egts/taiko_egts_to_mirage.user.js243
5 files changed, 400 insertions, 3 deletions
diff --git a/frontend/src/components/modals/TaikoDonderHirobaModal.tsx b/frontend/src/components/modals/TaikoDonderHirobaModal.tsx
index 897bd75..0f5c0a1 100644
--- a/frontend/src/components/modals/TaikoDonderHirobaModal.tsx
+++ b/frontend/src/components/modals/TaikoDonderHirobaModal.tsx
@@ -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">
diff --git a/frontend/src/components/modals/TaikoEGTSModal.tsx b/frontend/src/components/modals/TaikoEGTSModal.tsx
new file mode 100644
index 0000000..45d47bf
--- /dev/null
+++ b/frontend/src/components/modals/TaikoEGTSModal.tsx
@@ -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;
diff --git a/frontend/src/pages/Import.tsx b/frontend/src/pages/Import.tsx
index b4d80a8..21bc580 100644
--- a/frontend/src/pages/Import.tsx
+++ b/frontend/src/pages/Import.tsx
@@ -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>
);
diff --git a/scripts/taiko/egts/README.md b/scripts/taiko/egts/README.md
new file mode 100644
index 0000000..a88c5e5
--- /dev/null
+++ b/scripts/taiko/egts/README.md
@@ -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
diff --git a/scripts/taiko/egts/taiko_egts_to_mirage.user.js b/scripts/taiko/egts/taiko_egts_to_mirage.user.js
new file mode 100644
index 0000000..0d30cb6
--- /dev/null
+++ b/scripts/taiko/egts/taiko_egts_to_mirage.user.js
@@ -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();
+})();
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage