diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-04-06 23:00:20 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-04-06 23:00:20 -0700 |
| commit | d837a235d820891964f370101a699600df50b028 (patch) | |
| tree | a04aadaf69a1963887ef10efe25abf494f870879 /chuni | |
| parent | 172613c9c0b37e33a71b6875fcc6d5680880f7a2 (diff) | |
chunithm_tachi: bookmarklets for exporting playdata from tachi to batch-manual (MYT)
Diffstat (limited to 'chuni')
| -rw-r--r-- | chuni/tachi/README.md | 22 | ||||
| -rw-r--r-- | chuni/tachi/tachi_to_tachi_pb.js | 96 | ||||
| -rw-r--r-- | chuni/tachi/tachi_to_tachi_session.js | 82 |
3 files changed, 200 insertions, 0 deletions
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); +})(); |
