aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--chuni/tachi/README.md22
-rw-r--r--chuni/tachi/tachi_to_tachi_pb.js96
-rw-r--r--chuni/tachi/tachi_to_tachi_session.js82
4 files changed, 201 insertions, 0 deletions
diff --git a/README.md b/README.md
index 3aa4dce..4756212 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@ Use with caution as there may be some cases missing.
- [SDVX e-amusement CSV (with limited Session/Date data)](./sdvx/eamuse_csv)
- [CHUNITHM AquaDX (Online Hosted) User Data Export JSON](./chuni/aquadx)
+- [CHUNITHM Tachi to Tachi (Good for MYT data)](./chuni/tachi)
- [maimai DX AquaDX (Online Hosted) User Data Export JSON](./mai2/aquadx)
- [O.N.G.E.K.I AquaDX (Online Hosted) API recently played (no export functionality in AquaNet yet)](./ongeki/aquadx)
- [WACCA AquaDX (Online Hosted) API recently played (no export functionality in AquaNet yet)](./wacca/aquadx)
diff --git a/chuni/tachi/README.md b/chuni/tachi/README.md
new file mode 100644
index 0000000..a145e4f
--- /dev/null
+++ b/chuni/tachi/README.md
@@ -0,0 +1,22 @@
+# CHUNITHM Tachi to Tachi
+
+This is a bookmarklet that will allow you to download data shown in the Web UI of Tachi into a `Batch-Manual` compatible form again.
+
+This can be handy if you want to download/save data from the MYT network if you are already in a Tachi instance that has integrations with it.
+
+# Scripts
+
+## `tachi_to_tachi_pb.js`
+Takes data from your profile page which only shows your PB for each song
+Page: `https://my-tachi-instance.com/u/USERNAME/games/chunithm/Single/scores`
+
+## `tachi_to_tachi_session.js`
+For a singular session. Choose the session to export and go to the `All Scores` tab
+Page: `https://my-tachi-instance.com/u/USERNAME/games/chunithm/Single/sessions/SESSION_ID`
+
+# Usage
+
+1. Copy the contents of the script you want to use
+2. Create a new bookmark and paste the full contents of the script into the URL section
+3. Navigate to the relevant score page as shown in the previous section
+4. Show as many scores as possible on a singular view, if you exceed the largest limit then repeat the steps above for the next page(s)
diff --git a/chuni/tachi/tachi_to_tachi_pb.js b/chuni/tachi/tachi_to_tachi_pb.js
new file mode 100644
index 0000000..b0dccb1
--- /dev/null
+++ b/chuni/tachi/tachi_to_tachi_pb.js
@@ -0,0 +1,96 @@
+javascript:void(function () {
+ function parseChunithmTable() {
+ const rows = document.querySelectorAll("table.table tbody tr");
+ const difficultyMap = {
+ E: "Expert",
+ A: "Advanced",
+ B: "Basic",
+ M: "Master"
+ };
+ const results = {
+ meta: {
+ game: "chunithm",
+ playtype: "Single",
+ service: "Tachi to Tachi PB Export",
+ },
+ scores: []
+ };
+
+ for (let i = 0; i < rows.length; i += 3) {
+ const row = rows[i];
+ if (!row) continue;
+
+ const cells = row.querySelectorAll("td");
+ if (cells.length < 11) continue;
+
+ let difficulty = cells[1].innerText.trim().replace(/\n/, " ").split(" ")[0];
+ if (difficulty.length === 1) {
+ difficulty = difficultyMap[difficulty] || difficulty;
+ }
+
+ const songAnchor = cells[3].querySelector("a");
+ const title = songAnchor?.childNodes[0]?.textContent.trim() || "";
+ const artist = songAnchor?.querySelector("small")?.textContent.trim() || "";
+
+ const scoreRank = cells[5].querySelector("strong")?.innerText.trim() || "";
+ const scoreValue = parseInt(
+ cells[5].innerText.replace(scoreRank, "").trim().replace(/,/g, "")
+ );
+
+ const judgementText = cells[6].innerText.trim();
+ const parts = judgementText.split("-").map((x) => parseInt(x.trim()));
+ const [jcrit, justice, attack, miss] = parts;
+
+ const fastSlowMatch = judgementText.match(/\(F:(\d+)\s+S:(\d+)\)/);
+ const fast = fastSlowMatch ? parseInt(fastSlowMatch[1]) : undefined;
+ const slow = fastSlowMatch ? parseInt(fastSlowMatch[2]) : undefined;
+
+ const lamp = cells[7].innerText.trim();
+ const rating = parseFloat(cells[8].innerText.trim());
+
+ const timestampText = cells[10].innerText.trim().split("\n");
+ const timestampString = timestampText[1]?.trim() || "";
+ const timeAchieved = timestampString ? new Date(timestampString).getTime() : 0;
+
+ const score = {
+ score: scoreValue,
+ lamp,
+ matchType: "songTitle",
+ difficulty,
+ identifier: title,
+ artist,
+ judgements: {
+ jcrit,
+ justice,
+ attack,
+ miss,
+ },
+ timeAchieved
+ };
+
+ if (fast !== undefined && slow !== undefined) {
+ score.judgements.fast = fast;
+ score.judgements.slow = slow;
+ }
+
+ results.scores.push(score);
+ }
+
+ return results;
+ }
+
+ function downloadJSON(data) {
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "chunithm_table.json";
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ const data = parseChunithmTable();
+ downloadJSON(data);
+})();
diff --git a/chuni/tachi/tachi_to_tachi_session.js b/chuni/tachi/tachi_to_tachi_session.js
new file mode 100644
index 0000000..05fe300
--- /dev/null
+++ b/chuni/tachi/tachi_to_tachi_session.js
@@ -0,0 +1,82 @@
+javascript: void (function () {
+ function parseChunithmTable() {
+ const rows = document.querySelectorAll("table.table tbody tr");
+ const results = {
+ meta: {
+ game: "chunithm",
+ playtype: "Single",
+ service: "Tachi to Tachi Export",
+ },
+ scores: [],
+ };
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+ const cells = row.querySelectorAll("td");
+ if (
+ cells.length < 9 ||
+ row.classList.contains("expandable-pseudo-row") ||
+ row.classList.contains("fake-row")
+ )
+ continue;
+ let difficultyCell = cells[0];
+ let difficultyText = difficultyCell.innerText.trim();
+ let difficultyMatch = difficultyText.match(
+ /(MASTER|EXPERT|ADVANCED|BASIC|M|E|A|B)/i,
+ );
+ let difficulty = difficultyMatch
+ ? difficultyMatch[0].toUpperCase()
+ : "UNKNOWN";
+ const difficultyMap = {
+ M: "Master",
+ E: "Expert",
+ A: "Advanced",
+ B: "Basic",
+ };
+ if (difficulty.length === 1)
+ difficulty = difficultyMap[difficulty] || difficulty;
+ const songAnchor = cells[2].querySelector("a");
+ const title = songAnchor?.childNodes[0]?.textContent.trim() || "";
+ const artist =
+ songAnchor?.querySelector("small")?.textContent.trim() || "";
+ const scoreRank =
+ cells[3].querySelector("strong")?.innerText.trim() || "";
+ const scoreValue = parseInt(
+ cells[3].innerText.replace(scoreRank, "").trim().replace(/,/g, ""),
+ );
+ const judgementText = cells[4].innerText.trim();
+ const parts = judgementText.split("-").map((x) => parseInt(x.trim()));
+ const [jcrit, justice, attack, miss] = parts;
+ const lamp = cells[5].innerText.trim();
+ const rating = parseFloat(cells[6].innerText.trim());
+ const timestampCellLines = cells[7].innerText.trim().split("\n");
+ const dateString =
+ timestampCellLines.find((line) => /\w+ \d+, \d+/.test(line)) || "";
+ const timeAchieved = dateString ? new Date(dateString).getTime() : 0;
+ results.scores.push({
+ score: scoreValue,
+ lamp,
+ matchType: "songTitle",
+ difficulty,
+ identifier: title,
+ artist,
+ rating,
+ judgements: { jcrit, justice, attack, miss },
+ timeAchieved,
+ });
+ }
+ return results;
+ }
+ function downloadJSON(data) {
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "chunithm_table.json";
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+ const data = parseChunithmTable();
+ downloadJSON(data);
+})();
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage