diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-11-12 03:05:47 -0800 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-11-12 03:05:47 -0800 |
| commit | 58876529c38ee279e935c1cf3e204d2017a37a2e (patch) | |
| tree | cca3f731466bad5f9571966f7df52a857baf4fd1 | |
| parent | bc0d7cd3682aa1ec880fdfe18dcacbffbc2a0f96 (diff) | |
taiko: add TAL/EGTS import script
| -rw-r--r-- | frontend/src/components/modals/TaikoDonderHirobaModal.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/components/modals/TaikoEGTSModal.tsx | 133 | ||||
| -rw-r--r-- | frontend/src/pages/Import.tsx | 19 | ||||
| -rw-r--r-- | scripts/taiko/egts/README.md | 4 | ||||
| -rw-r--r-- | scripts/taiko/egts/taiko_egts_to_mirage.user.js | 243 |
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(); +})(); |
