mirror of
https://gitea.tendokyu.moe/pinapelz/Mirage.git
synced 2026-09-22 22:38:04 +03:00
taiko: add TAL/EGTS import script
This commit is contained in:
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
Reference in New Issue
Block a user