From 5aa04d60b1602dbb6166c5459a2f1c1792e634c0 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Tue, 11 Nov 2025 16:45:33 -0800 Subject: move import script paths according to import method --- README.md | 2 + frontend/src/components/modals/DivaNetModal.tsx | 2 +- frontend/src/components/modals/MusicDiverModal.tsx | 2 +- frontend/src/pages/Import.tsx | 8 +- scripts/dancearound/README.md | 8 - .../dancearound/dancearound_play_history.user.js | 168 ------------- scripts/dancearound/eamuse/README.md | 8 + .../eamuse/dancearound_play_history.user.js | 168 +++++++++++++ scripts/dancerush/README.md | 10 - scripts/dancerush/dancerush_play_history.user.js | 153 ------------ scripts/dancerush/eamuse/README.md | 10 + .../eamuse/dancerush_play_history.user.js | 153 ++++++++++++ scripts/musicdiver/README.md | 8 - .../musicdiver/musicdiver_recent_history.user.js | 135 ----------- scripts/musicdiver/mypage/README.md | 8 + .../mypage/musicdiver_recent_history.user.js | 135 +++++++++++ scripts/nostalgia/README.md | 4 - scripts/nostalgia/flower/README.md | 4 + .../flower/nostalgia_flower_scraper.user.js | 188 +++++++++++++++ scripts/nostalgia/nostalgia_flower_scraper.user.js | 188 --------------- scripts/projectdiva-arcade/README.md | 10 - .../projectdiva-arcade/diva_net_history.user.js | 262 --------------------- scripts/projectdiva-arcade/divanet/README.md | 10 + .../divanet/diva_net_history.user.js | 262 +++++++++++++++++++++ scripts/reflecbeat/README.md | 4 - scripts/reflecbeat/flower/README.md | 4 + .../flower/reflecbeat_flower_scraper.user.js | 111 +++++++++ .../reflecbeat/reflecbeat_flower_scraper.user.js | 111 --------- scripts/taiko/.gitignore | 218 ----------------- scripts/taiko/donder-hiroba/.gitignore | 218 +++++++++++++++++ scripts/taiko/donder-hiroba/README.md | 0 scripts/taiko/donder-hiroba/requirements.txt | 2 + .../donder-hiroba/taiko_donder_hiroba_export.py | 232 ++++++++++++++++++ scripts/taiko/requirements.txt | 2 - scripts/taiko/taiko_donder_hiroba_export.py | 232 ------------------ 35 files changed, 1521 insertions(+), 1519 deletions(-) delete mode 100644 scripts/dancearound/README.md delete mode 100644 scripts/dancearound/dancearound_play_history.user.js create mode 100644 scripts/dancearound/eamuse/README.md create mode 100644 scripts/dancearound/eamuse/dancearound_play_history.user.js delete mode 100644 scripts/dancerush/README.md delete mode 100644 scripts/dancerush/dancerush_play_history.user.js create mode 100644 scripts/dancerush/eamuse/README.md create mode 100644 scripts/dancerush/eamuse/dancerush_play_history.user.js delete mode 100644 scripts/musicdiver/README.md delete mode 100644 scripts/musicdiver/musicdiver_recent_history.user.js create mode 100644 scripts/musicdiver/mypage/README.md create mode 100644 scripts/musicdiver/mypage/musicdiver_recent_history.user.js delete mode 100644 scripts/nostalgia/README.md create mode 100644 scripts/nostalgia/flower/README.md create mode 100644 scripts/nostalgia/flower/nostalgia_flower_scraper.user.js delete mode 100644 scripts/nostalgia/nostalgia_flower_scraper.user.js delete mode 100644 scripts/projectdiva-arcade/README.md delete mode 100644 scripts/projectdiva-arcade/diva_net_history.user.js create mode 100644 scripts/projectdiva-arcade/divanet/README.md create mode 100644 scripts/projectdiva-arcade/divanet/diva_net_history.user.js delete mode 100644 scripts/reflecbeat/README.md create mode 100644 scripts/reflecbeat/flower/README.md create mode 100644 scripts/reflecbeat/flower/reflecbeat_flower_scraper.user.js delete mode 100644 scripts/reflecbeat/reflecbeat_flower_scraper.user.js delete mode 100644 scripts/taiko/.gitignore create mode 100644 scripts/taiko/donder-hiroba/.gitignore create mode 100644 scripts/taiko/donder-hiroba/README.md create mode 100644 scripts/taiko/donder-hiroba/requirements.txt create mode 100644 scripts/taiko/donder-hiroba/taiko_donder_hiroba_export.py delete mode 100644 scripts/taiko/requirements.txt delete mode 100644 scripts/taiko/taiko_donder_hiroba_export.py diff --git a/README.md b/README.md index 310b324..8376ef9 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,5 @@ pnpm dev - Express - Prisma ORM - Postgres + +> You're welcome to join my [personal instance](https://mirage.pinapelz.com), but I make no guarantees about the integrity of data if something goes catastrophically wrong. diff --git a/frontend/src/components/modals/DivaNetModal.tsx b/frontend/src/components/modals/DivaNetModal.tsx index 00babc2..d3aee86 100644 --- a/frontend/src/components/modals/DivaNetModal.tsx +++ b/frontend/src/components/modals/DivaNetModal.tsx @@ -93,7 +93,7 @@ const DivaNetModal = ({

DIVA.NET Recently Played Score Export Userscript (Last 20 Played) diff --git a/frontend/src/components/modals/MusicDiverModal.tsx b/frontend/src/components/modals/MusicDiverModal.tsx index 0113bf5..e995978 100644 --- a/frontend/src/components/modals/MusicDiverModal.tsx +++ b/frontend/src/components/modals/MusicDiverModal.tsx @@ -102,7 +102,7 @@ const MusicDiverMyPageModal = ({

MyPage Recently Played Score Export Userscript diff --git a/frontend/src/pages/Import.tsx b/frontend/src/pages/Import.tsx index 5b48c5d..4e3707c 100644 --- a/frontend/src/pages/Import.tsx +++ b/frontend/src/pages/Import.tsx @@ -319,7 +319,7 @@ const Import = () => { importPage="https://p.eagate.573.jp/payment/p/ex_select_course.html" scripts={[{ name: "e-amusement Recently Played Score Export Userscript (Last 20 Played)", - url: "https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/dancerush/dancerush_play_history.user.js" + url: "https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/dancerush/eamuse/dancerush_play_history.user.js" }]} /> )} @@ -332,7 +332,7 @@ const Import = () => { importPage="https://p.eagate.573.jp/game/around/1st/top/index.html#play_hist" scripts={[{ name: "e-amusement Recently Played Score Export Userscript (Last 20 Played)", - url: "https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/dancearound/dancearound_play_history.user.js" + url: "https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/dancearound/eamuse/dancearound_play_history.user.js" }]} /> )} @@ -365,7 +365,7 @@ const Import = () => { importPage="https://projectflower.eu/game/nostalgia/54827307" scripts={[{ name: "Flower Play History (Exports only the page you are on)", - url: "https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/nostalgia/nostalgia_flower_scraper.user.js" + url: "https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/nostalgia/flower/nostalgia_flower_scraper.user.js" }]} /> )} @@ -378,7 +378,7 @@ const Import = () => { importPage="https://projectflower.eu/game/rb/profile/21363050" scripts={[{ name: "Flower Play History (Exports only the page you are on)", - url: "https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/reflecbeat/reflecbeat_flower_scraper.user.js" + url: "https://github.com/pinapelz/Mirage/raw/refs/heads/main/scripts/reflecbeat/flower/reflecbeat_flower_scraper.user.js" }]} /> )} diff --git a/scripts/dancearound/README.md b/scripts/dancearound/README.md deleted file mode 100644 index 404f605..0000000 --- a/scripts/dancearound/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# DANCE aROUND - -Score Page: https://p.eagate.573.jp/game/around/1st/playdata/index.html#play_hist - -**Dance aROUND only stores a record of your last 30 plays**. Anything older than that will not have date data and can only be retrieved via looking at your best scores. - -*Scripts Available:* -- [Recently Played History](./dancearound_play_history.user.js) \ No newline at end of file diff --git a/scripts/dancearound/dancearound_play_history.user.js b/scripts/dancearound/dancearound_play_history.user.js deleted file mode 100644 index eccc1a9..0000000 --- a/scripts/dancearound/dancearound_play_history.user.js +++ /dev/null @@ -1,168 +0,0 @@ -// ==UserScript== -// @name DANCEAROUND (e-amusement) Recently Played Mirage Scraper -// @namespace http://tampermonkey.net/ -// @version 1.0 -// @description DANCEAROUND e-amusement site to Mirage import JSON -// @match https://p.eagate.573.jp/game/around/1st/playdata/index.html* -// @grant none -// @run-at document-idle -// ==/UserScript== - -(function () { - function waitFor(selector, timeout = 10000) { - return new Promise((resolve, reject) => { - const interval = 300; - let waited = 0; - const check = () => { - const el = document.querySelector(selector); - if (el) return resolve(el); - waited += interval; - if (waited >= timeout) return reject(`Timeout: ${selector}`); - setTimeout(check, interval); - }; - check(); - }); - } - - function getDifficulty(fumen, songData) { - let difficulty, lamp; - - switch (fumen) { - case "ADVANCED": - difficulty = songData?.ADVANCED?.level || 0; - lamp = "ADVANCED"; - break; - case "BASIC": - difficulty = songData?.BASIC?.level || 0; - lamp = "BASIC"; - break; - case "MASTER": - difficulty = songData?.MASTER?.level || 0; - lamp = "MASTER"; - break; - default: - difficulty = 0; - lamp = fumen; - break; - } - - return { difficulty, lamp }; - } - function getLampText(status) { - switch (status) { - case 0: - return "C"; - case 1: - return "B"; - case 2: - return "A"; - case 3: - return "AA"; - case 4: - return "AAA"; - case 5: - return "AAA+"; - } - } - - function getClearStatusText(status){ - switch(status){ - case 1: - return "FAILURE"; - case 2: - return "PASSED"; - case 3: - return "FULL COMBO"; - case 4: - return "EXC"; - } - } - - async function fetchAndDownload() { - const url = "https://p.eagate.573.jp/game/around/1st/json/pdata_getdata.html"; - const payload = new URLSearchParams({ - service_kind: "play_hist", - pdata_kind: "play_hist", - }); - - try { - const response = await fetch(url, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "X-Requested-With": "XMLHttpRequest", - }, - body: payload.toString(), - }); - - if (!response.ok) throw new Error(`HTTP ${response.status}`); - - const data = await response.json(); - const play_hist = data.data.easite_get_playerdata.music_hist.music; - const song_db = data.data.easite_get_playerdata.mdb; - let mirage = { - meta: { - game: "dancearound", - playtype: "Single", - service: "e-amusement PLAY HISTORY", - }, - }; - const remappedList = play_hist.map((entry) => { - const level = getDifficulty(entry.fumen_type, song_db[entry.music_id].fumens) - return { - title: song_db[entry.music_id].title_name, - artist: song_db[entry.music_id].artist_name, - level: level.difficulty, - score: entry.score, - lamp: getLampText(entry.rank), - clear_status: getClearStatusText(entry.clear_status), - difficulty: level.lamp, - timestamp: entry.play_date, - judgements: { - "perfect": entry.perfect, - "great": entry.great, - "good": entry.good, - "bad": entry.bad - }, - optional: { - maxCombo: entry.combo, - } - - }; - }); - mirage.scores = remappedList; - console.log("Final mirage object:", mirage); - - const blob = new Blob([JSON.stringify(mirage, null, 2)], { - type: "application/json", - }); - - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "dancearound_scores_mirage_import.json"; - a.click(); - console - } catch (err) { - console.error("Fetch/download error:", err); - alert("Failed to fetch or process JSON. See console for details."); - } - } - - waitFor("#id_ctpl_body") - .then((container) => { - const btn = document.createElement("button"); - btn.textContent = "πŸ“₯ DOWNLOAD PLAY HISTORY SCORE JSON"; - btn.style.cssText = ` - margin: 10px; padding: 8px 12px; - font-size: 14px; cursor: pointer; - background: #2563eb; color: white; - border: none; border-radius: 6px; - z-index: 9999; - `; - btn.onclick = fetchAndDownload; - - container.prepend(btn); - }) - .catch((err) => console.warn("Could not inject button:", err)); -})(); diff --git a/scripts/dancearound/eamuse/README.md b/scripts/dancearound/eamuse/README.md new file mode 100644 index 0000000..404f605 --- /dev/null +++ b/scripts/dancearound/eamuse/README.md @@ -0,0 +1,8 @@ +# DANCE aROUND + +Score Page: https://p.eagate.573.jp/game/around/1st/playdata/index.html#play_hist + +**Dance aROUND only stores a record of your last 30 plays**. Anything older than that will not have date data and can only be retrieved via looking at your best scores. + +*Scripts Available:* +- [Recently Played History](./dancearound_play_history.user.js) \ No newline at end of file diff --git a/scripts/dancearound/eamuse/dancearound_play_history.user.js b/scripts/dancearound/eamuse/dancearound_play_history.user.js new file mode 100644 index 0000000..eccc1a9 --- /dev/null +++ b/scripts/dancearound/eamuse/dancearound_play_history.user.js @@ -0,0 +1,168 @@ +// ==UserScript== +// @name DANCEAROUND (e-amusement) Recently Played Mirage Scraper +// @namespace http://tampermonkey.net/ +// @version 1.0 +// @description DANCEAROUND e-amusement site to Mirage import JSON +// @match https://p.eagate.573.jp/game/around/1st/playdata/index.html* +// @grant none +// @run-at document-idle +// ==/UserScript== + +(function () { + function waitFor(selector, timeout = 10000) { + return new Promise((resolve, reject) => { + const interval = 300; + let waited = 0; + const check = () => { + const el = document.querySelector(selector); + if (el) return resolve(el); + waited += interval; + if (waited >= timeout) return reject(`Timeout: ${selector}`); + setTimeout(check, interval); + }; + check(); + }); + } + + function getDifficulty(fumen, songData) { + let difficulty, lamp; + + switch (fumen) { + case "ADVANCED": + difficulty = songData?.ADVANCED?.level || 0; + lamp = "ADVANCED"; + break; + case "BASIC": + difficulty = songData?.BASIC?.level || 0; + lamp = "BASIC"; + break; + case "MASTER": + difficulty = songData?.MASTER?.level || 0; + lamp = "MASTER"; + break; + default: + difficulty = 0; + lamp = fumen; + break; + } + + return { difficulty, lamp }; + } + function getLampText(status) { + switch (status) { + case 0: + return "C"; + case 1: + return "B"; + case 2: + return "A"; + case 3: + return "AA"; + case 4: + return "AAA"; + case 5: + return "AAA+"; + } + } + + function getClearStatusText(status){ + switch(status){ + case 1: + return "FAILURE"; + case 2: + return "PASSED"; + case 3: + return "FULL COMBO"; + case 4: + return "EXC"; + } + } + + async function fetchAndDownload() { + const url = "https://p.eagate.573.jp/game/around/1st/json/pdata_getdata.html"; + const payload = new URLSearchParams({ + service_kind: "play_hist", + pdata_kind: "play_hist", + }); + + try { + const response = await fetch(url, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Requested-With": "XMLHttpRequest", + }, + body: payload.toString(), + }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const data = await response.json(); + const play_hist = data.data.easite_get_playerdata.music_hist.music; + const song_db = data.data.easite_get_playerdata.mdb; + let mirage = { + meta: { + game: "dancearound", + playtype: "Single", + service: "e-amusement PLAY HISTORY", + }, + }; + const remappedList = play_hist.map((entry) => { + const level = getDifficulty(entry.fumen_type, song_db[entry.music_id].fumens) + return { + title: song_db[entry.music_id].title_name, + artist: song_db[entry.music_id].artist_name, + level: level.difficulty, + score: entry.score, + lamp: getLampText(entry.rank), + clear_status: getClearStatusText(entry.clear_status), + difficulty: level.lamp, + timestamp: entry.play_date, + judgements: { + "perfect": entry.perfect, + "great": entry.great, + "good": entry.good, + "bad": entry.bad + }, + optional: { + maxCombo: entry.combo, + } + + }; + }); + mirage.scores = remappedList; + console.log("Final mirage object:", mirage); + + const blob = new Blob([JSON.stringify(mirage, null, 2)], { + type: "application/json", + }); + + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "dancearound_scores_mirage_import.json"; + a.click(); + console + } catch (err) { + console.error("Fetch/download error:", err); + alert("Failed to fetch or process JSON. See console for details."); + } + } + + waitFor("#id_ctpl_body") + .then((container) => { + const btn = document.createElement("button"); + btn.textContent = "πŸ“₯ DOWNLOAD PLAY HISTORY SCORE JSON"; + btn.style.cssText = ` + margin: 10px; padding: 8px 12px; + font-size: 14px; cursor: pointer; + background: #2563eb; color: white; + border: none; border-radius: 6px; + z-index: 9999; + `; + btn.onclick = fetchAndDownload; + + container.prepend(btn); + }) + .catch((err) => console.warn("Could not inject button:", err)); +})(); diff --git a/scripts/dancerush/README.md b/scripts/dancerush/README.md deleted file mode 100644 index d5efc1f..0000000 --- a/scripts/dancerush/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# DANCERUSH -Versions: -- DANCERUSH STARDOM - -Score Page: https://p.eagate.573.jp/game/dan/1st/playdata/entrance.html#play_hist - -**DANCERUSH only stores a record of your last 30 plays**. Anything older than that will not have date data and can only be retrieved via looking at your best scores. - -*Scripts Available:* -- [Recently Played History](./dancerush_play_history.user.js) \ No newline at end of file diff --git a/scripts/dancerush/dancerush_play_history.user.js b/scripts/dancerush/dancerush_play_history.user.js deleted file mode 100644 index 1f0121b..0000000 --- a/scripts/dancerush/dancerush_play_history.user.js +++ /dev/null @@ -1,153 +0,0 @@ -// ==UserScript== -// @name DANCERUSH (e-amusement) Recently Played Mirage Scraper -// @namespace http://tampermonkey.net/ -// @version 1.0 -// @description DANCERUSH e-amusement site to Mirage import JSON -// @match https://p.eagate.573.jp/game/dan/1st/playdata/entrance.html* -// @grant none -// @run-at document-idle -// ==/UserScript== - -(function () { - function waitFor(selector, timeout = 10000) { - return new Promise((resolve, reject) => { - const interval = 300; - let waited = 0; - const check = () => { - const el = document.querySelector(selector); - if (el) return resolve(el); - waited += interval; - if (waited >= timeout) return reject(`Timeout: ${selector}`); - setTimeout(check, interval); - }; - check(); - }); - } - - function getDifficulty(fumen, mdb) { - let difficulty, lamp; - - switch (fumen) { - case "1a": - difficulty = mdb.fumen_1a.difnum; - lamp = "NORMAL"; - break; - case "1b": - difficulty = mdb.fumen_1b.difnum; - lamp = "EASY"; - break; - case "2a": - difficulty = mdb.fumen_2a.difnum; - lamp = "NORMAL"; - break; - case "2b": - default: - difficulty = mdb.fumen_2b.difnum; - lamp = "EASY"; - break; - } - - return { difficulty, lamp }; - } - - function getCorrectPlayerJudgements(player_code, score_data){ - if(!score_data.p2){ - return score_data.p1; - } - if(player_code === score_data.p1.member_code){ - return score_data.p1; - } - else{ - return score_data.p2; - } - } - - - async function fetchAndDownload() { - const url = "https://p.eagate.573.jp/game/dan/1st/json/pdata_getdata.html"; - const payload = new URLSearchParams({ - service_kind: "play_hist", - pdata_kind: "play_hist", - }); - - try { - const response = await fetch(url, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "X-Requested-With": "XMLHttpRequest", - }, - body: payload.toString(), - }); - - if (!response.ok) throw new Error(`HTTP ${response.status}`); - - const data = await response.json(); - const play_hist = data.data.easite_get_playerdata.music_hist.music; - const song_db = data.data.easite_get_playerdata.mdb; - let mirage = { - meta: { - game: "DANCERUSH STARDOM", - playtype: "Single", - service: "e-amusement PLAY HISTORY", - }, - }; - const remappedList = play_hist.map((entry) => { - const p_judgements = getCorrectPlayerJudgements(data.data.easite_get_playerdata.userid.code, entry) - const diff = getDifficulty(entry.music_type, song_db[entry.music_id].difficulty) - const numPlayers = (entry.p1 && entry.p2) ? 2 : 1; - return { - title: song_db[entry.music_id].info.title_name, - artist: song_db[entry.music_id].info.artist_name, - diff_lamp: diff.lamp, - num_players: numPlayers, - score: entry.score, - lamp: entry.rank, - difficulty: diff.difficulty, - timestamp: entry.lastplay_date, - judgements: { - "perfect": p_judgements.perfect, - "great": p_judgements.great, - "good": p_judgements.good, - "bad": p_judgements.bad - }, - optional: { - maxCombo: entry.combo, - } - - }; - }); - mirage.scores = remappedList; - - const blob = new Blob([JSON.stringify(mirage, null, 2)], { - type: "application/json", - }); - - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "dancerush_scores_mirage_import.json"; - a.click(); - } catch (err) { - console.error("Fetch/download error:", err); - alert("Failed to fetch or process JSON. See console for details."); - } - } - - waitFor("#id_ctpl_body") - .then((container) => { - const btn = document.createElement("button"); - btn.textContent = "πŸ“₯ DOWNLOAD PLAY HISTORY SCORE JSON"; - btn.style.cssText = ` - margin: 10px; padding: 8px 12px; - font-size: 14px; cursor: pointer; - background: #2563eb; color: white; - border: none; border-radius: 6px; - z-index: 9999; - `; - btn.onclick = fetchAndDownload; - - container.prepend(btn); - }) - .catch((err) => console.warn("Could not inject button:", err)); -})(); diff --git a/scripts/dancerush/eamuse/README.md b/scripts/dancerush/eamuse/README.md new file mode 100644 index 0000000..d5efc1f --- /dev/null +++ b/scripts/dancerush/eamuse/README.md @@ -0,0 +1,10 @@ +# DANCERUSH +Versions: +- DANCERUSH STARDOM + +Score Page: https://p.eagate.573.jp/game/dan/1st/playdata/entrance.html#play_hist + +**DANCERUSH only stores a record of your last 30 plays**. Anything older than that will not have date data and can only be retrieved via looking at your best scores. + +*Scripts Available:* +- [Recently Played History](./dancerush_play_history.user.js) \ No newline at end of file diff --git a/scripts/dancerush/eamuse/dancerush_play_history.user.js b/scripts/dancerush/eamuse/dancerush_play_history.user.js new file mode 100644 index 0000000..1f0121b --- /dev/null +++ b/scripts/dancerush/eamuse/dancerush_play_history.user.js @@ -0,0 +1,153 @@ +// ==UserScript== +// @name DANCERUSH (e-amusement) Recently Played Mirage Scraper +// @namespace http://tampermonkey.net/ +// @version 1.0 +// @description DANCERUSH e-amusement site to Mirage import JSON +// @match https://p.eagate.573.jp/game/dan/1st/playdata/entrance.html* +// @grant none +// @run-at document-idle +// ==/UserScript== + +(function () { + function waitFor(selector, timeout = 10000) { + return new Promise((resolve, reject) => { + const interval = 300; + let waited = 0; + const check = () => { + const el = document.querySelector(selector); + if (el) return resolve(el); + waited += interval; + if (waited >= timeout) return reject(`Timeout: ${selector}`); + setTimeout(check, interval); + }; + check(); + }); + } + + function getDifficulty(fumen, mdb) { + let difficulty, lamp; + + switch (fumen) { + case "1a": + difficulty = mdb.fumen_1a.difnum; + lamp = "NORMAL"; + break; + case "1b": + difficulty = mdb.fumen_1b.difnum; + lamp = "EASY"; + break; + case "2a": + difficulty = mdb.fumen_2a.difnum; + lamp = "NORMAL"; + break; + case "2b": + default: + difficulty = mdb.fumen_2b.difnum; + lamp = "EASY"; + break; + } + + return { difficulty, lamp }; + } + + function getCorrectPlayerJudgements(player_code, score_data){ + if(!score_data.p2){ + return score_data.p1; + } + if(player_code === score_data.p1.member_code){ + return score_data.p1; + } + else{ + return score_data.p2; + } + } + + + async function fetchAndDownload() { + const url = "https://p.eagate.573.jp/game/dan/1st/json/pdata_getdata.html"; + const payload = new URLSearchParams({ + service_kind: "play_hist", + pdata_kind: "play_hist", + }); + + try { + const response = await fetch(url, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Requested-With": "XMLHttpRequest", + }, + body: payload.toString(), + }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const data = await response.json(); + const play_hist = data.data.easite_get_playerdata.music_hist.music; + const song_db = data.data.easite_get_playerdata.mdb; + let mirage = { + meta: { + game: "DANCERUSH STARDOM", + playtype: "Single", + service: "e-amusement PLAY HISTORY", + }, + }; + const remappedList = play_hist.map((entry) => { + const p_judgements = getCorrectPlayerJudgements(data.data.easite_get_playerdata.userid.code, entry) + const diff = getDifficulty(entry.music_type, song_db[entry.music_id].difficulty) + const numPlayers = (entry.p1 && entry.p2) ? 2 : 1; + return { + title: song_db[entry.music_id].info.title_name, + artist: song_db[entry.music_id].info.artist_name, + diff_lamp: diff.lamp, + num_players: numPlayers, + score: entry.score, + lamp: entry.rank, + difficulty: diff.difficulty, + timestamp: entry.lastplay_date, + judgements: { + "perfect": p_judgements.perfect, + "great": p_judgements.great, + "good": p_judgements.good, + "bad": p_judgements.bad + }, + optional: { + maxCombo: entry.combo, + } + + }; + }); + mirage.scores = remappedList; + + const blob = new Blob([JSON.stringify(mirage, null, 2)], { + type: "application/json", + }); + + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "dancerush_scores_mirage_import.json"; + a.click(); + } catch (err) { + console.error("Fetch/download error:", err); + alert("Failed to fetch or process JSON. See console for details."); + } + } + + waitFor("#id_ctpl_body") + .then((container) => { + const btn = document.createElement("button"); + btn.textContent = "πŸ“₯ DOWNLOAD PLAY HISTORY SCORE JSON"; + btn.style.cssText = ` + margin: 10px; padding: 8px 12px; + font-size: 14px; cursor: pointer; + background: #2563eb; color: white; + border: none; border-radius: 6px; + z-index: 9999; + `; + btn.onclick = fetchAndDownload; + + container.prepend(btn); + }) + .catch((err) => console.warn("Could not inject button:", err)); +})(); diff --git a/scripts/musicdiver/README.md b/scripts/musicdiver/README.md deleted file mode 100644 index c1316d0..0000000 --- a/scripts/musicdiver/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# MUSIC DIVER - -Score Page: https://mypage.musicdiver.jp/record?view=history - -- Without DIVER PASS, you are limited to only the last 5 plays - -*Scripts Available:* -- [Recently Played History](./musicdiver_recent_history.user.js) diff --git a/scripts/musicdiver/musicdiver_recent_history.user.js b/scripts/musicdiver/musicdiver_recent_history.user.js deleted file mode 100644 index 36af873..0000000 --- a/scripts/musicdiver/musicdiver_recent_history.user.js +++ /dev/null @@ -1,135 +0,0 @@ -// ==UserScript== -// @name Music Diver (MyPage) Recently Played Mirage Scraper -// @namespace https://mypage.musicdiver.jp/ -// @version 1.1 -// @description MUSIC DIVER My Page Recent History to Mirage import JSON -// @match https://mypage.musicdiver.jp/record?view=history* -// @grant none -// ==/UserScript== - -(function () { - "use strict"; - - let mirage = {}; - let remappedData = []; - - async function fetchRecordHistory() { - const url = "https://mypage.musicdiver.jp/api/record_history?lang=en"; - try { - const response = await fetch(url, { - headers: { - Accept: "application/json, text/javascript, */*; q=0.01", - "X-Requested-With": "XMLHttpRequest", - }, - credentials: "include", - }); - - const data = await response.json(); - if (data.responseCode !== 200) { - console.error("Music Diver API error:", data.responseMessage); - return; - } - - const diffMap = { - 0: "EASY", - 1: "NORMAL", - 2: "HARD", - 3: "EXTREME", - }; - - remappedData = data.response.map((rec) => { - const date = new Date(rec.created_at.replace(" ", "T") + "+09:00"); - const unixTime = date.getTime(); - - // Determine lamp based on flag precedence - let lamp = "FAILED"; - if (rec.clear_flag) lamp = "CLEAR"; - else if (rec.epic_flag) lamp = "EPIC"; - else if (rec.all_perfect_flag) lamp = "ALL PERFECT"; - else if (rec.full_combo_flag) lamp = "FULL COMBO"; - - return { - timestamp: unixTime, - title: rec.music_title, - artist: rec.artist_name, - difficulty: diffMap[rec.difficulty_id] || "UNKNOWN", - level: rec.level, - score: rec.score, - rank: rec.rank, - lamp, - judgements: { - critical: rec.critical_num, - perfect: rec.perfect_num, - great: rec.great_num, - good: rec.good_num, - bad: rec.bad_num, - miss: rec.miss_num, - }, - }; - }); - - mirage = { - meta: { - game: "musicdiver", - playtype: "Single", - service: "MUSIC DIVER My Page Recent History", - }, - scores: remappedData, - }; - - console.log("🎡 Music Diver Records:", remappedData); - console.log("Mirage export object:", mirage); - - showDownloadButton(); - } catch (err) { - console.error("Error fetching Music Diver data:", err); - } - } - - function showDownloadButton() { - // Avoid duplicates - if (document.getElementById("md-download-json")) return; - - const btn = document.createElement("button"); - btn.id = "md-download-json"; - btn.textContent = "⬇️ Download Mirage Score JSON"; - Object.assign(btn.style, { - position: "fixed", - bottom: "20px", - right: "20px", - zIndex: "9999", - padding: "10px 16px", - background: "#1e90ff", - color: "#fff", - border: "none", - borderRadius: "8px", - cursor: "pointer", - fontSize: "14px", - boxShadow: "0 2px 8px rgba(0,0,0,0.25)", - transition: "background 0.2s", - }); - - btn.onmouseenter = () => (btn.style.background = "#0070f0"); - btn.onmouseleave = () => (btn.style.background = "#1e90ff"); - - btn.addEventListener("click", () => { - if (!remappedData.length) { - alert("No data available yet. Try refreshing!"); - return; - } - const blob = new Blob([JSON.stringify(mirage, null, 2)], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "musicdiver_records.json"; - a.click(); - URL.revokeObjectURL(url); - }); - - document.body.appendChild(btn); - } - - window.addEventListener("load", fetchRecordHistory); -})(); diff --git a/scripts/musicdiver/mypage/README.md b/scripts/musicdiver/mypage/README.md new file mode 100644 index 0000000..c1316d0 --- /dev/null +++ b/scripts/musicdiver/mypage/README.md @@ -0,0 +1,8 @@ +# MUSIC DIVER + +Score Page: https://mypage.musicdiver.jp/record?view=history + +- Without DIVER PASS, you are limited to only the last 5 plays + +*Scripts Available:* +- [Recently Played History](./musicdiver_recent_history.user.js) diff --git a/scripts/musicdiver/mypage/musicdiver_recent_history.user.js b/scripts/musicdiver/mypage/musicdiver_recent_history.user.js new file mode 100644 index 0000000..36af873 --- /dev/null +++ b/scripts/musicdiver/mypage/musicdiver_recent_history.user.js @@ -0,0 +1,135 @@ +// ==UserScript== +// @name Music Diver (MyPage) Recently Played Mirage Scraper +// @namespace https://mypage.musicdiver.jp/ +// @version 1.1 +// @description MUSIC DIVER My Page Recent History to Mirage import JSON +// @match https://mypage.musicdiver.jp/record?view=history* +// @grant none +// ==/UserScript== + +(function () { + "use strict"; + + let mirage = {}; + let remappedData = []; + + async function fetchRecordHistory() { + const url = "https://mypage.musicdiver.jp/api/record_history?lang=en"; + try { + const response = await fetch(url, { + headers: { + Accept: "application/json, text/javascript, */*; q=0.01", + "X-Requested-With": "XMLHttpRequest", + }, + credentials: "include", + }); + + const data = await response.json(); + if (data.responseCode !== 200) { + console.error("Music Diver API error:", data.responseMessage); + return; + } + + const diffMap = { + 0: "EASY", + 1: "NORMAL", + 2: "HARD", + 3: "EXTREME", + }; + + remappedData = data.response.map((rec) => { + const date = new Date(rec.created_at.replace(" ", "T") + "+09:00"); + const unixTime = date.getTime(); + + // Determine lamp based on flag precedence + let lamp = "FAILED"; + if (rec.clear_flag) lamp = "CLEAR"; + else if (rec.epic_flag) lamp = "EPIC"; + else if (rec.all_perfect_flag) lamp = "ALL PERFECT"; + else if (rec.full_combo_flag) lamp = "FULL COMBO"; + + return { + timestamp: unixTime, + title: rec.music_title, + artist: rec.artist_name, + difficulty: diffMap[rec.difficulty_id] || "UNKNOWN", + level: rec.level, + score: rec.score, + rank: rec.rank, + lamp, + judgements: { + critical: rec.critical_num, + perfect: rec.perfect_num, + great: rec.great_num, + good: rec.good_num, + bad: rec.bad_num, + miss: rec.miss_num, + }, + }; + }); + + mirage = { + meta: { + game: "musicdiver", + playtype: "Single", + service: "MUSIC DIVER My Page Recent History", + }, + scores: remappedData, + }; + + console.log("🎡 Music Diver Records:", remappedData); + console.log("Mirage export object:", mirage); + + showDownloadButton(); + } catch (err) { + console.error("Error fetching Music Diver data:", err); + } + } + + function showDownloadButton() { + // Avoid duplicates + if (document.getElementById("md-download-json")) return; + + const btn = document.createElement("button"); + btn.id = "md-download-json"; + btn.textContent = "⬇️ Download Mirage Score JSON"; + Object.assign(btn.style, { + position: "fixed", + bottom: "20px", + right: "20px", + zIndex: "9999", + padding: "10px 16px", + background: "#1e90ff", + color: "#fff", + border: "none", + borderRadius: "8px", + cursor: "pointer", + fontSize: "14px", + boxShadow: "0 2px 8px rgba(0,0,0,0.25)", + transition: "background 0.2s", + }); + + btn.onmouseenter = () => (btn.style.background = "#0070f0"); + btn.onmouseleave = () => (btn.style.background = "#1e90ff"); + + btn.addEventListener("click", () => { + if (!remappedData.length) { + alert("No data available yet. Try refreshing!"); + return; + } + const blob = new Blob([JSON.stringify(mirage, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "musicdiver_records.json"; + a.click(); + URL.revokeObjectURL(url); + }); + + document.body.appendChild(btn); + } + + window.addEventListener("load", fetchRecordHistory); +})(); diff --git a/scripts/nostalgia/README.md b/scripts/nostalgia/README.md deleted file mode 100644 index 496ca10..0000000 --- a/scripts/nostalgia/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Nostalgia - -*Scripts Available:* -- [Flower Play History, exports only the page you are on](./nostalgia_flower_scraper.user.js) diff --git a/scripts/nostalgia/flower/README.md b/scripts/nostalgia/flower/README.md new file mode 100644 index 0000000..496ca10 --- /dev/null +++ b/scripts/nostalgia/flower/README.md @@ -0,0 +1,4 @@ +# Nostalgia + +*Scripts Available:* +- [Flower Play History, exports only the page you are on](./nostalgia_flower_scraper.user.js) diff --git a/scripts/nostalgia/flower/nostalgia_flower_scraper.user.js b/scripts/nostalgia/flower/nostalgia_flower_scraper.user.js new file mode 100644 index 0000000..0cc7699 --- /dev/null +++ b/scripts/nostalgia/flower/nostalgia_flower_scraper.user.js @@ -0,0 +1,188 @@ +// ==UserScript== +// @name Nostalgia (Flower) Play History Scraper +// @version 1.0 +// @description Scrapes scores from Flower's Nostalgia page and converts to Mirage +// @author Meta-link +// @match https://projectflower.eu/game/nostalgia/* +// @grant none +// ==/UserScript== + +(function() { + 'use strict'; + + let mirage = {}; + const scores = []; + + // Create progress bar container + const progressContainer = document.createElement("div"); + progressContainer.style.position = "fixed"; + progressContainer.style.top = "50px"; + progressContainer.style.left = "50%"; + progressContainer.style.transform = "translateX(-50%)"; + progressContainer.style.width = "50%"; + progressContainer.style.background = "#eee"; + progressContainer.style.border = "1px solid #ccc"; + progressContainer.style.borderRadius = "4px"; + progressContainer.style.zIndex = 9999; + progressContainer.style.display = "none"; + + const progressBar = document.createElement("div"); + progressBar.style.width = "0%"; + progressBar.style.height = "20px"; + progressBar.style.background = "#28a745"; + progressBar.style.borderRadius = "4px"; + progressContainer.appendChild(progressBar); + document.body.appendChild(progressContainer); + + function updateProgress(current, total) { + progressContainer.style.display = "block"; + const percent = Math.round((current / total) * 100); + progressBar.style.width = percent + "%"; + progressBar.textContent = `${current} / ${total} songs`; + } + + // Parse judgements from details row + function parseJudgements(detailsRow) { + const judgements = {}; + const optional = {}; + const cols = detailsRow.querySelectorAll("div.col-sm-2"); + cols.forEach(col => { + const labelElem = col.querySelector("strong"); + if (!labelElem) return; + + const label = labelElem.textContent.replace(/\s/g, ""); + const valueText = labelElem.nextSibling?.textContent?.trim() || col.textContent.replace(labelElem.textContent, "").trim(); + + if (label === "β—†Just") judgements.marvelous = Number(valueText) || 0; + else if (label === "Just") judgements.just = Number(valueText) || 0; + else if (label === "Good") judgements.good = Number(valueText) || 0; + else if (label === "Near") judgements.near = Number(valueText) || 0; + else if (label === "Miss") judgements.miss = Number(valueText) || 0; + else if (label === "Fast/Slow" || label === "Fast/Slow") { + const parts = valueText.split("/").map(x => Number(x.trim())); + optional.fast = parts[0] || 0; + optional.slow = parts[1] || 0; + } + }); + return { judgements, optional }; + } + + // Fetch artist from song page + async function getArtistFromLink(link) { + try { + const res = await fetch(link); + const text = await res.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(text, "text/html"); + const ths = doc.querySelectorAll("th"); + for (let th of ths) { + if (th.textContent.trim() === "Song Artist") { + const td = th.nextElementSibling; + if (td) return td.textContent.trim(); + } + } + } catch (e) { + console.error("Error fetching artist from", link, e); + } + return ""; + } + + // Parse the Nostalgia score table + async function parseNostalgia() { + const scoreLines = document.querySelectorAll(".table > tbody:nth-child(3) > tr"); + const songs = Array.from(scoreLines).filter(row => row.classList.contains("accordion-toggle")); + const totalSongs = songs.length; + + for (let i = 0; i < songs.length; i++) { + const row = songs[i]; + + // Title and song link + const songLinkElem = row.querySelector("td:nth-child(2) > a"); + const title = songLinkElem.querySelector("strong").textContent.trim(); + const songLink = songLinkElem.href; + + // Fetch artist from song page + const artist = await getArtistFromLink(songLink); + + // Chart info: difficulty + level + const chartCell = row.querySelector("td:nth-child(3)").textContent.trim(); + let difficulty = "Unknown"; + let level = 0; + const match = chartCell.match(/([A-Za-z]+)\s*(\d+)/); + if (match) { + difficulty = match[1]; + level = Number(match[2]); + } + + // Score + const score = parseInt(row.querySelector("td:nth-child(4) small").textContent.replaceAll(',', '')); + + // Lamp + const lamp = row.querySelector("td:nth-child(5) strong").textContent.trim(); + + // Time achieved + const timeTxt = row.querySelector("td:nth-child(6) small").textContent.trim(); + const timeAchieved = new Date(timeTxt).getTime(); + + // Judgements + const detailsRow = scoreLines[i * 2 + 1].querySelector("div[style*='padding: 5px']"); + const { judgements, optional } = parseJudgements(detailsRow); + + scores.push({ + title: title, + artist: artist, + difficulty: difficulty, + level: level, + score: score, + lamp: lamp, + timestamp: timeAchieved, + judgements: judgements, + optional: optional + }); + + updateProgress(i + 1, totalSongs); + } + + progressContainer.style.display = "none"; + exportScores(); + } + + // Export scores as JSON + function exportScores() { + mirage = { + meta: { + game: "nostalgia", + playtype: "Single", + service: "NOSTALGIA Flower Play History", + }, + scores: scores, + }; + + const blob = new Blob([JSON.stringify(mirage, null, 2)], {type: "application/json"}); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "nostalgia_scores.json"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + alert(`Exported ${scores.length} scores!`); + } + + // Add export button + const button = document.createElement("button"); + button.textContent = "Export Nostalgia Scores MIRAGE"; + button.style.position = "fixed"; + button.style.top = "10px"; + button.style.right = "10px"; + button.style.zIndex = 9999; + button.style.padding = "8px 12px"; + button.style.background = "#007bff"; + button.style.color = "white"; + button.style.border = "none"; + button.style.borderRadius = "4px"; + button.style.cursor = "pointer"; + button.onclick = parseNostalgia; + document.body.appendChild(button); + +})(); diff --git a/scripts/nostalgia/nostalgia_flower_scraper.user.js b/scripts/nostalgia/nostalgia_flower_scraper.user.js deleted file mode 100644 index 0cc7699..0000000 --- a/scripts/nostalgia/nostalgia_flower_scraper.user.js +++ /dev/null @@ -1,188 +0,0 @@ -// ==UserScript== -// @name Nostalgia (Flower) Play History Scraper -// @version 1.0 -// @description Scrapes scores from Flower's Nostalgia page and converts to Mirage -// @author Meta-link -// @match https://projectflower.eu/game/nostalgia/* -// @grant none -// ==/UserScript== - -(function() { - 'use strict'; - - let mirage = {}; - const scores = []; - - // Create progress bar container - const progressContainer = document.createElement("div"); - progressContainer.style.position = "fixed"; - progressContainer.style.top = "50px"; - progressContainer.style.left = "50%"; - progressContainer.style.transform = "translateX(-50%)"; - progressContainer.style.width = "50%"; - progressContainer.style.background = "#eee"; - progressContainer.style.border = "1px solid #ccc"; - progressContainer.style.borderRadius = "4px"; - progressContainer.style.zIndex = 9999; - progressContainer.style.display = "none"; - - const progressBar = document.createElement("div"); - progressBar.style.width = "0%"; - progressBar.style.height = "20px"; - progressBar.style.background = "#28a745"; - progressBar.style.borderRadius = "4px"; - progressContainer.appendChild(progressBar); - document.body.appendChild(progressContainer); - - function updateProgress(current, total) { - progressContainer.style.display = "block"; - const percent = Math.round((current / total) * 100); - progressBar.style.width = percent + "%"; - progressBar.textContent = `${current} / ${total} songs`; - } - - // Parse judgements from details row - function parseJudgements(detailsRow) { - const judgements = {}; - const optional = {}; - const cols = detailsRow.querySelectorAll("div.col-sm-2"); - cols.forEach(col => { - const labelElem = col.querySelector("strong"); - if (!labelElem) return; - - const label = labelElem.textContent.replace(/\s/g, ""); - const valueText = labelElem.nextSibling?.textContent?.trim() || col.textContent.replace(labelElem.textContent, "").trim(); - - if (label === "β—†Just") judgements.marvelous = Number(valueText) || 0; - else if (label === "Just") judgements.just = Number(valueText) || 0; - else if (label === "Good") judgements.good = Number(valueText) || 0; - else if (label === "Near") judgements.near = Number(valueText) || 0; - else if (label === "Miss") judgements.miss = Number(valueText) || 0; - else if (label === "Fast/Slow" || label === "Fast/Slow") { - const parts = valueText.split("/").map(x => Number(x.trim())); - optional.fast = parts[0] || 0; - optional.slow = parts[1] || 0; - } - }); - return { judgements, optional }; - } - - // Fetch artist from song page - async function getArtistFromLink(link) { - try { - const res = await fetch(link); - const text = await res.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(text, "text/html"); - const ths = doc.querySelectorAll("th"); - for (let th of ths) { - if (th.textContent.trim() === "Song Artist") { - const td = th.nextElementSibling; - if (td) return td.textContent.trim(); - } - } - } catch (e) { - console.error("Error fetching artist from", link, e); - } - return ""; - } - - // Parse the Nostalgia score table - async function parseNostalgia() { - const scoreLines = document.querySelectorAll(".table > tbody:nth-child(3) > tr"); - const songs = Array.from(scoreLines).filter(row => row.classList.contains("accordion-toggle")); - const totalSongs = songs.length; - - for (let i = 0; i < songs.length; i++) { - const row = songs[i]; - - // Title and song link - const songLinkElem = row.querySelector("td:nth-child(2) > a"); - const title = songLinkElem.querySelector("strong").textContent.trim(); - const songLink = songLinkElem.href; - - // Fetch artist from song page - const artist = await getArtistFromLink(songLink); - - // Chart info: difficulty + level - const chartCell = row.querySelector("td:nth-child(3)").textContent.trim(); - let difficulty = "Unknown"; - let level = 0; - const match = chartCell.match(/([A-Za-z]+)\s*(\d+)/); - if (match) { - difficulty = match[1]; - level = Number(match[2]); - } - - // Score - const score = parseInt(row.querySelector("td:nth-child(4) small").textContent.replaceAll(',', '')); - - // Lamp - const lamp = row.querySelector("td:nth-child(5) strong").textContent.trim(); - - // Time achieved - const timeTxt = row.querySelector("td:nth-child(6) small").textContent.trim(); - const timeAchieved = new Date(timeTxt).getTime(); - - // Judgements - const detailsRow = scoreLines[i * 2 + 1].querySelector("div[style*='padding: 5px']"); - const { judgements, optional } = parseJudgements(detailsRow); - - scores.push({ - title: title, - artist: artist, - difficulty: difficulty, - level: level, - score: score, - lamp: lamp, - timestamp: timeAchieved, - judgements: judgements, - optional: optional - }); - - updateProgress(i + 1, totalSongs); - } - - progressContainer.style.display = "none"; - exportScores(); - } - - // Export scores as JSON - function exportScores() { - mirage = { - meta: { - game: "nostalgia", - playtype: "Single", - service: "NOSTALGIA Flower Play History", - }, - scores: scores, - }; - - const blob = new Blob([JSON.stringify(mirage, null, 2)], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "nostalgia_scores.json"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - alert(`Exported ${scores.length} scores!`); - } - - // Add export button - const button = document.createElement("button"); - button.textContent = "Export Nostalgia Scores MIRAGE"; - button.style.position = "fixed"; - button.style.top = "10px"; - button.style.right = "10px"; - button.style.zIndex = 9999; - button.style.padding = "8px 12px"; - button.style.background = "#007bff"; - button.style.color = "white"; - button.style.border = "none"; - button.style.borderRadius = "4px"; - button.style.cursor = "pointer"; - button.onclick = parseNostalgia; - document.body.appendChild(button); - -})(); diff --git a/scripts/projectdiva-arcade/README.md b/scripts/projectdiva-arcade/README.md deleted file mode 100644 index 7ce9ee5..0000000 --- a/scripts/projectdiva-arcade/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Project DIVA Arcade -- Future Tone - -Score Page: https://p.eagate.573.jp/game/around/1st/playdata/index.html#play_hist - -> [!WARNING] -> DIVA.NET deletes detailed record views after a certain amount of time, make sure you import recently played scores ASAP! - -*Scripts Available:* -- [Recently Played History, Recent 20 Played](./diva_net_history.user.js) \ No newline at end of file diff --git a/scripts/projectdiva-arcade/diva_net_history.user.js b/scripts/projectdiva-arcade/diva_net_history.user.js deleted file mode 100644 index 1c1e205..0000000 --- a/scripts/projectdiva-arcade/diva_net_history.user.js +++ /dev/null @@ -1,262 +0,0 @@ -// ==UserScript== -// @name PDAFT (DIVA.NET) Mirage Scraper -// @namespace http://tampermonkey.net/ -// @version 1.2 -// @description Scrape DIVA.NET play history (pages 1–20) into Mirage JSON -// @match https://project-diva-ac.net/divanet/personal/playHistory/* -// @grant none -// @run-at document-idle -// ==/UserScript== - -(function () { - // --- Utility: wait for selector --- - function waitFor(selector, timeout = 10000) { - return new Promise((resolve, reject) => { - const interval = 300; - let waited = 0; - const check = () => { - const el = document.querySelector(selector); - if (el) return resolve(el); - waited += interval; - if (waited >= timeout) return reject(`Timeout: ${selector}`); - setTimeout(check, interval); - }; - check(); - }); - } - - // --- Fetch artist name from info page --- - async function getArtistFromInfoPage(url) { - try { - const res = await fetch(url, { credentials: "include" }); - const html = await res.text(); - const doc = new DOMParser().parseFromString(html, "text/html"); - const rows = Array.from(doc.querySelectorAll("table tr")); - for (const row of rows) { - const label = row.querySelector("td:first-child font"); - const value = row.querySelector("td:last-child font"); - if (label && value && label.textContent.includes("δ½œζ›²θ€…")) { - return value.textContent.trim(); - } - } - return null; - } catch { - return null; - } - } - - // --- Parse a single page --- - async function parsePlayHistoryPage(html) { - const doc = new DOMParser().parseFromString(html, "text/html"); - const center = doc.querySelector(".center_middle"); - if (!center) return null; - - const findFont = (keyword) => - Array.from(center.querySelectorAll("font")).find((f) => - f.textContent && f.textContent.includes(keyword) - ); - - const getTextAfterLabel = (keyword) => { - const font = findFont(keyword); - if (!font) return null; - - let node = font.nextSibling; - const parts = []; - let scanned = 0; - - while (node && scanned < 20) { - scanned++; - if (node.nodeType === Node.TEXT_NODE) { - const txt = node.textContent.replace(/\s+/g, " ").trim(); - if (txt) parts.push(txt); - node = node.nextSibling; - continue; - } - if (node.nodeType === Node.ELEMENT_NODE) { - if (["BR", "HR"].includes(node.tagName)) { - node = node.nextSibling; - continue; - } - if (node.tagName === "FONT" && /\[.*\]/.test(node.textContent)) break; - - const txt = node.textContent.replace(/\s+/g, " ").trim(); - if (txt) parts.push(txt); - node = node.nextSibling; - continue; - } - node = node.nextSibling; - } - return parts.join(" ").trim() || null; - }; - - const entry = {}; - - // --- Timestamp --- - const datetimeRaw = getTextAfterLabel("ζ—₯ζ™‚") || ""; - const dtMatch = datetimeRaw.match(/(\d{2})\/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2})/); - if (dtMatch) { - const [_, yy, mm, dd, hh, min] = dtMatch.map(Number); - const fullYear = 2000 + yy; - const timestamp = Date.UTC(fullYear, mm - 1, dd, hh - 9, min, 0); - entry.timestamp = timestamp; - } else entry.timestamp = null; - - // --- Title & Artist --- - const a = center.querySelector("a[href*='/divanet/pv/info/']"); - if (a) { - entry.title = a.textContent.trim(); - const songInfoUrl = new URL(a.getAttribute("href"), location.origin).href; - entry.artist = await getArtistFromInfoPage(songInfoUrl); - } else { - entry.title = getTextAfterLabel("曲名"); - entry.artist = null; - } - - // --- Difficulty --- - const diffRaw = getTextAfterLabel("ι›£ζ˜“εΊ¦") || ""; - const diffMatch = diffRaw.match(/([A-Zぁ-γ‚“γ‚‘-ン一-ιΎ―]+)\s*β˜…?\s*([\d.]+)/i); - if (diffMatch) { - entry.diff_lamp = diffMatch[1].trim().toUpperCase(); - entry.difficulty = parseFloat(diffMatch[2]); - } else { - entry.diff_lamp = diffRaw.trim(); - entry.difficulty = null; - } - - // --- Clear rank --- - entry.lamp = (getTextAfterLabel("CLEAR RANK") || "").trim(); - - // --- Achievement --- - const achRaw = getTextAfterLabel("ι”ζˆηŽ‡") || ""; - const achNum = (achRaw.match(/[\d.]+/) || [null])[0]; - entry.achievement = achNum ? parseFloat(achNum) : null; - - // --- Score --- - const scoreRaw = getTextAfterLabel("SCORE") || ""; - const scoreDigits = (scoreRaw.match(/(\d+)/) || [null])[0]; - entry.score = scoreDigits ? parseInt(scoreDigits, 10) : null; - - // --- Judgements --- - entry.judgements = {}; - const table = center.querySelector("table"); - if (table) { - const rows = Array.from(table.querySelectorAll("tr")); - for (let i = 0; i < rows.length; i++) { - const text = rows[i].textContent.replace(/\s+/g, " ").trim(); - const val = parseInt((text.match(/(\d+)/) || [0])[0], 10); - if (/COOL/i.test(text)) entry.judgements.cool = val; - else if (/FINE/i.test(text)) entry.judgements.fine = val; - else if (/SAFE/i.test(text)) entry.judgements.safe = val; - else if (/SAD/i.test(text)) entry.judgements.sad = val; - else if (/WORST|WRONG/i.test(text)) entry.judgements.worst = val; - else if (/COMBO/i.test(text)) - entry.maxCombo = parseInt((text.match(/(\d+)/g) || []).pop() || "0", 10); - } - } - - entry.judgements = { - cool: entry.judgements.cool ?? 0, - fine: entry.judgements.fine ?? 0, - safe: entry.judgements.safe ?? 0, - sad: entry.judgements.sad ?? 0, - worst: entry.judgements.worst ?? 0, - }; - entry.maxCombo = entry.maxCombo ?? 0; - - return entry; - } - - // --- Fetch all pages (1–20) --- - async function fetchAllPages(progressBar) { - const scores = []; - for (let i = 1; i <= 20; i++) { - const url = `https://project-diva-ac.net/divanet/personal/playHistoryDetail/${i}/0`; - try { - progressBar.style.width = `${(i / 20) * 100}%`; - console.log(`Fetching page ${i}...`); - const response = await fetch(url, { credentials: "include" }); - if (!response.ok) continue; - const html = await response.text(); - const parsed = await parsePlayHistoryPage(html); - if (parsed && parsed.title) scores.push(parsed); - } catch (e) { - console.warn(`Failed to fetch page ${i}:`, e); - } - } - progressBar.style.width = "100%"; - return scores; - } - - // --- Fetch & Download all as JSON --- - async function fetchAndDownload() { - try { - const progressContainer = document.createElement("div"); - progressContainer.style.cssText = ` - width: 200px; - height: 20px; - background: #eee; - border-radius: 10px; - overflow: hidden; - margin: 10px; - `; - - const progressBar = document.createElement("div"); - progressBar.style.cssText = ` - width: 0%; - height: 100%; - background: #2563eb; - transition: width 0.3s ease; - `; - - progressContainer.appendChild(progressBar); - document.querySelector(".center").prepend(progressContainer); - - const scores = await fetchAllPages(progressBar); - console.log(`Fetched ${scores.length} entries.`); - - const mirage = { - meta: { - game: "diva", - playtype: "Single", - service: "DIVA.NET PLAY HISTORY", - }, - scores: scores, - }; - - const blob = new Blob([JSON.stringify(mirage, null, 2)], { - type: "application/json", - }); - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "divanet_scores_mirage_import.json"; - a.click(); - - setTimeout(() => progressContainer.remove(), 1000); - } catch (err) { - console.error("Error during fetch/download:", err); - alert("Error while scraping pages β€” see console for details."); - } - } - - // --- Inject button --- - waitFor(".center") - .then((container) => { - const btn = document.createElement("button"); - btn.textContent = "πŸ“₯ DOWNLOAD PLAY HISTORY SCORE JSON"; - btn.style.cssText = ` - margin: 10px; - padding: 8px 12px; - font-size: 14px; - cursor: pointer; - background: #2563eb; - color: white; - border: none; - border-radius: 6px; - z-index: 9999; - position: relative; - `; - btn.onclick = fetchAndDownload; - container.prepend(btn); - }) - .catch((err) => console.warn("Could not inject button:", err)); -})(); diff --git a/scripts/projectdiva-arcade/divanet/README.md b/scripts/projectdiva-arcade/divanet/README.md new file mode 100644 index 0000000..7ce9ee5 --- /dev/null +++ b/scripts/projectdiva-arcade/divanet/README.md @@ -0,0 +1,10 @@ +# Project DIVA Arcade +- Future Tone + +Score Page: https://p.eagate.573.jp/game/around/1st/playdata/index.html#play_hist + +> [!WARNING] +> DIVA.NET deletes detailed record views after a certain amount of time, make sure you import recently played scores ASAP! + +*Scripts Available:* +- [Recently Played History, Recent 20 Played](./diva_net_history.user.js) \ No newline at end of file diff --git a/scripts/projectdiva-arcade/divanet/diva_net_history.user.js b/scripts/projectdiva-arcade/divanet/diva_net_history.user.js new file mode 100644 index 0000000..1c1e205 --- /dev/null +++ b/scripts/projectdiva-arcade/divanet/diva_net_history.user.js @@ -0,0 +1,262 @@ +// ==UserScript== +// @name PDAFT (DIVA.NET) Mirage Scraper +// @namespace http://tampermonkey.net/ +// @version 1.2 +// @description Scrape DIVA.NET play history (pages 1–20) into Mirage JSON +// @match https://project-diva-ac.net/divanet/personal/playHistory/* +// @grant none +// @run-at document-idle +// ==/UserScript== + +(function () { + // --- Utility: wait for selector --- + function waitFor(selector, timeout = 10000) { + return new Promise((resolve, reject) => { + const interval = 300; + let waited = 0; + const check = () => { + const el = document.querySelector(selector); + if (el) return resolve(el); + waited += interval; + if (waited >= timeout) return reject(`Timeout: ${selector}`); + setTimeout(check, interval); + }; + check(); + }); + } + + // --- Fetch artist name from info page --- + async function getArtistFromInfoPage(url) { + try { + const res = await fetch(url, { credentials: "include" }); + const html = await res.text(); + const doc = new DOMParser().parseFromString(html, "text/html"); + const rows = Array.from(doc.querySelectorAll("table tr")); + for (const row of rows) { + const label = row.querySelector("td:first-child font"); + const value = row.querySelector("td:last-child font"); + if (label && value && label.textContent.includes("δ½œζ›²θ€…")) { + return value.textContent.trim(); + } + } + return null; + } catch { + return null; + } + } + + // --- Parse a single page --- + async function parsePlayHistoryPage(html) { + const doc = new DOMParser().parseFromString(html, "text/html"); + const center = doc.querySelector(".center_middle"); + if (!center) return null; + + const findFont = (keyword) => + Array.from(center.querySelectorAll("font")).find((f) => + f.textContent && f.textContent.includes(keyword) + ); + + const getTextAfterLabel = (keyword) => { + const font = findFont(keyword); + if (!font) return null; + + let node = font.nextSibling; + const parts = []; + let scanned = 0; + + while (node && scanned < 20) { + scanned++; + if (node.nodeType === Node.TEXT_NODE) { + const txt = node.textContent.replace(/\s+/g, " ").trim(); + if (txt) parts.push(txt); + node = node.nextSibling; + continue; + } + if (node.nodeType === Node.ELEMENT_NODE) { + if (["BR", "HR"].includes(node.tagName)) { + node = node.nextSibling; + continue; + } + if (node.tagName === "FONT" && /\[.*\]/.test(node.textContent)) break; + + const txt = node.textContent.replace(/\s+/g, " ").trim(); + if (txt) parts.push(txt); + node = node.nextSibling; + continue; + } + node = node.nextSibling; + } + return parts.join(" ").trim() || null; + }; + + const entry = {}; + + // --- Timestamp --- + const datetimeRaw = getTextAfterLabel("ζ—₯ζ™‚") || ""; + const dtMatch = datetimeRaw.match(/(\d{2})\/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2})/); + if (dtMatch) { + const [_, yy, mm, dd, hh, min] = dtMatch.map(Number); + const fullYear = 2000 + yy; + const timestamp = Date.UTC(fullYear, mm - 1, dd, hh - 9, min, 0); + entry.timestamp = timestamp; + } else entry.timestamp = null; + + // --- Title & Artist --- + const a = center.querySelector("a[href*='/divanet/pv/info/']"); + if (a) { + entry.title = a.textContent.trim(); + const songInfoUrl = new URL(a.getAttribute("href"), location.origin).href; + entry.artist = await getArtistFromInfoPage(songInfoUrl); + } else { + entry.title = getTextAfterLabel("曲名"); + entry.artist = null; + } + + // --- Difficulty --- + const diffRaw = getTextAfterLabel("ι›£ζ˜“εΊ¦") || ""; + const diffMatch = diffRaw.match(/([A-Zぁ-γ‚“γ‚‘-ン一-ιΎ―]+)\s*β˜…?\s*([\d.]+)/i); + if (diffMatch) { + entry.diff_lamp = diffMatch[1].trim().toUpperCase(); + entry.difficulty = parseFloat(diffMatch[2]); + } else { + entry.diff_lamp = diffRaw.trim(); + entry.difficulty = null; + } + + // --- Clear rank --- + entry.lamp = (getTextAfterLabel("CLEAR RANK") || "").trim(); + + // --- Achievement --- + const achRaw = getTextAfterLabel("ι”ζˆηŽ‡") || ""; + const achNum = (achRaw.match(/[\d.]+/) || [null])[0]; + entry.achievement = achNum ? parseFloat(achNum) : null; + + // --- Score --- + const scoreRaw = getTextAfterLabel("SCORE") || ""; + const scoreDigits = (scoreRaw.match(/(\d+)/) || [null])[0]; + entry.score = scoreDigits ? parseInt(scoreDigits, 10) : null; + + // --- Judgements --- + entry.judgements = {}; + const table = center.querySelector("table"); + if (table) { + const rows = Array.from(table.querySelectorAll("tr")); + for (let i = 0; i < rows.length; i++) { + const text = rows[i].textContent.replace(/\s+/g, " ").trim(); + const val = parseInt((text.match(/(\d+)/) || [0])[0], 10); + if (/COOL/i.test(text)) entry.judgements.cool = val; + else if (/FINE/i.test(text)) entry.judgements.fine = val; + else if (/SAFE/i.test(text)) entry.judgements.safe = val; + else if (/SAD/i.test(text)) entry.judgements.sad = val; + else if (/WORST|WRONG/i.test(text)) entry.judgements.worst = val; + else if (/COMBO/i.test(text)) + entry.maxCombo = parseInt((text.match(/(\d+)/g) || []).pop() || "0", 10); + } + } + + entry.judgements = { + cool: entry.judgements.cool ?? 0, + fine: entry.judgements.fine ?? 0, + safe: entry.judgements.safe ?? 0, + sad: entry.judgements.sad ?? 0, + worst: entry.judgements.worst ?? 0, + }; + entry.maxCombo = entry.maxCombo ?? 0; + + return entry; + } + + // --- Fetch all pages (1–20) --- + async function fetchAllPages(progressBar) { + const scores = []; + for (let i = 1; i <= 20; i++) { + const url = `https://project-diva-ac.net/divanet/personal/playHistoryDetail/${i}/0`; + try { + progressBar.style.width = `${(i / 20) * 100}%`; + console.log(`Fetching page ${i}...`); + const response = await fetch(url, { credentials: "include" }); + if (!response.ok) continue; + const html = await response.text(); + const parsed = await parsePlayHistoryPage(html); + if (parsed && parsed.title) scores.push(parsed); + } catch (e) { + console.warn(`Failed to fetch page ${i}:`, e); + } + } + progressBar.style.width = "100%"; + return scores; + } + + // --- Fetch & Download all as JSON --- + async function fetchAndDownload() { + try { + const progressContainer = document.createElement("div"); + progressContainer.style.cssText = ` + width: 200px; + height: 20px; + background: #eee; + border-radius: 10px; + overflow: hidden; + margin: 10px; + `; + + const progressBar = document.createElement("div"); + progressBar.style.cssText = ` + width: 0%; + height: 100%; + background: #2563eb; + transition: width 0.3s ease; + `; + + progressContainer.appendChild(progressBar); + document.querySelector(".center").prepend(progressContainer); + + const scores = await fetchAllPages(progressBar); + console.log(`Fetched ${scores.length} entries.`); + + const mirage = { + meta: { + game: "diva", + playtype: "Single", + service: "DIVA.NET PLAY HISTORY", + }, + scores: scores, + }; + + const blob = new Blob([JSON.stringify(mirage, null, 2)], { + type: "application/json", + }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "divanet_scores_mirage_import.json"; + a.click(); + + setTimeout(() => progressContainer.remove(), 1000); + } catch (err) { + console.error("Error during fetch/download:", err); + alert("Error while scraping pages β€” see console for details."); + } + } + + // --- Inject button --- + waitFor(".center") + .then((container) => { + const btn = document.createElement("button"); + btn.textContent = "πŸ“₯ DOWNLOAD PLAY HISTORY SCORE JSON"; + btn.style.cssText = ` + margin: 10px; + padding: 8px 12px; + font-size: 14px; + cursor: pointer; + background: #2563eb; + color: white; + border: none; + border-radius: 6px; + z-index: 9999; + position: relative; + `; + btn.onclick = fetchAndDownload; + container.prepend(btn); + }) + .catch((err) => console.warn("Could not inject button:", err)); +})(); diff --git a/scripts/reflecbeat/README.md b/scripts/reflecbeat/README.md deleted file mode 100644 index 85c1a3d..0000000 --- a/scripts/reflecbeat/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# REFLEC BEAT - -*Scripts Available:* -- [Flower Play History, exports only the page you are on](./reflecbeat_flower_scraper.user.js) \ No newline at end of file diff --git a/scripts/reflecbeat/flower/README.md b/scripts/reflecbeat/flower/README.md new file mode 100644 index 0000000..85c1a3d --- /dev/null +++ b/scripts/reflecbeat/flower/README.md @@ -0,0 +1,4 @@ +# REFLEC BEAT + +*Scripts Available:* +- [Flower Play History, exports only the page you are on](./reflecbeat_flower_scraper.user.js) \ No newline at end of file diff --git a/scripts/reflecbeat/flower/reflecbeat_flower_scraper.user.js b/scripts/reflecbeat/flower/reflecbeat_flower_scraper.user.js new file mode 100644 index 0000000..dd756df --- /dev/null +++ b/scripts/reflecbeat/flower/reflecbeat_flower_scraper.user.js @@ -0,0 +1,111 @@ +// ==UserScript== +// @name REFLEC BEAT (Flower) Play History Scraper +// @namespace http://tampermonkey.net/ +// @version 1.2 +// @description Export REFLEC BEAT scores including full judgements and timestamps as JSON +// @match https://projectflower.eu/game/rb/profile/* +// @grant none +// ==/UserScript== + +(function() { + 'use strict'; + + // Add export button + const btn = document.createElement('button'); + btn.textContent = 'Mirage Export Scores JSON'; + btn.style.position = 'fixed'; + btn.style.top = '10px'; + btn.style.right = '10px'; + btn.style.zIndex = 9999; + btn.style.padding = '8px 12px'; + btn.style.background = '#4CAF50'; + btn.style.color = 'white'; + btn.style.border = 'none'; + btn.style.borderRadius = '4px'; + btn.style.cursor = 'pointer'; + document.body.appendChild(btn); + + btn.addEventListener('click', () => { + const json = { + meta: { + game: "reflecbeat", + playtype: "Single", + service: "REFLEC BEAT Flower Play History" + }, + scores: parseScoreLog() + }; + + // Download JSON + const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'reflecbeat_scores.json'; + a.click(); + URL.revokeObjectURL(url); + }); + + function parseScoreLog() { + const rows = document.querySelectorAll('table.table tbody tr.accordion-toggle'); + const scores = []; + + rows.forEach(row => { + const titleElem = row.querySelector('td.pnmTitle a'); + const artistElem = row.querySelector('td.pnmTitle small'); + const difficultyElem = row.querySelector('td.gradeHARD, td.gradeMEDIUM, td.gradeEASY'); + const levelElem = difficultyElem?.querySelector('strong'); + const lampElem = row.querySelector('td.rb-rank strong'); // Rank / Lamp + const scoreText = row.querySelector('td.rb-rank span')?.innerText || ''; + const scoreMatch = scoreText.match(/(\d+(?:,\d+)*)\s*\((\d+(?:\.\d+)?)%\)/); + const scoreNum = scoreMatch ? parseInt(scoreMatch[1].replace(/,/g, '')) : null; + const scorePercent = scoreMatch ? parseFloat(scoreMatch[2]) : null; + + // Extract timestamp from in last column + const timestampElem = row.querySelector('td.hidden-from-mobile small'); + let timestamp = null; + if (timestampElem) { + const dateStr = timestampElem.innerText.trim(); // e.g., "2025-06-27 5:23 PM" + timestamp = new Date(dateStr).getTime(); // Unix ms + } + + // Get accordion ID + const accordionId = row.getAttribute('data-target')?.replace('#', ''); + const accordion = document.getElementById(accordionId); + + // Initialize metadata + let lifeLeft = '', justReflec = 0, just = 0, great = 0, good = 0, miss = 0; + + if (accordion) { + const divs = accordion.querySelectorAll('div.col-sm-3, div.col-sm-2, div.col-sm-4'); + divs.forEach(div => { + const label = div.querySelector('strong')?.innerText.trim(); + const value = div.childNodes[div.childNodes.length-1].nodeValue?.trim() || div.querySelector('a')?.innerText.trim() || ''; + switch(label) { + case 'Life Left': lifeLeft = value; break; + case 'Just Reflec': justReflec = parseInt(value) || 0; break; + case 'Just': just = parseInt(value) || 0; break; + case 'Great': great = parseInt(value) || 0; break; + case 'Good': good = parseInt(value) || 0; break; + case 'Miss': miss = parseInt(value) || 0; break; + } + }); + } + + scores.push({ + title: titleElem?.innerText.trim() || '', + artist: artistElem?.innerText.trim() || '', + difficulty: difficultyElem?.innerText.replace(levelElem?.innerText, '').trim() || '', + level: levelElem ? parseInt(levelElem.innerText.trim()) : null, + score: scoreNum, + scorePercent, + lamp: lampElem?.innerText.trim() || '', + lifeLeft: parseInt(lifeLeft) || null, + timestamp, // Unix ms + judgements: { justReflec, just, great, good, miss } + }); + }); + + return scores; + } + +})(); diff --git a/scripts/reflecbeat/reflecbeat_flower_scraper.user.js b/scripts/reflecbeat/reflecbeat_flower_scraper.user.js deleted file mode 100644 index dd756df..0000000 --- a/scripts/reflecbeat/reflecbeat_flower_scraper.user.js +++ /dev/null @@ -1,111 +0,0 @@ -// ==UserScript== -// @name REFLEC BEAT (Flower) Play History Scraper -// @namespace http://tampermonkey.net/ -// @version 1.2 -// @description Export REFLEC BEAT scores including full judgements and timestamps as JSON -// @match https://projectflower.eu/game/rb/profile/* -// @grant none -// ==/UserScript== - -(function() { - 'use strict'; - - // Add export button - const btn = document.createElement('button'); - btn.textContent = 'Mirage Export Scores JSON'; - btn.style.position = 'fixed'; - btn.style.top = '10px'; - btn.style.right = '10px'; - btn.style.zIndex = 9999; - btn.style.padding = '8px 12px'; - btn.style.background = '#4CAF50'; - btn.style.color = 'white'; - btn.style.border = 'none'; - btn.style.borderRadius = '4px'; - btn.style.cursor = 'pointer'; - document.body.appendChild(btn); - - btn.addEventListener('click', () => { - const json = { - meta: { - game: "reflecbeat", - playtype: "Single", - service: "REFLEC BEAT Flower Play History" - }, - scores: parseScoreLog() - }; - - // Download JSON - const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'reflecbeat_scores.json'; - a.click(); - URL.revokeObjectURL(url); - }); - - function parseScoreLog() { - const rows = document.querySelectorAll('table.table tbody tr.accordion-toggle'); - const scores = []; - - rows.forEach(row => { - const titleElem = row.querySelector('td.pnmTitle a'); - const artistElem = row.querySelector('td.pnmTitle small'); - const difficultyElem = row.querySelector('td.gradeHARD, td.gradeMEDIUM, td.gradeEASY'); - const levelElem = difficultyElem?.querySelector('strong'); - const lampElem = row.querySelector('td.rb-rank strong'); // Rank / Lamp - const scoreText = row.querySelector('td.rb-rank span')?.innerText || ''; - const scoreMatch = scoreText.match(/(\d+(?:,\d+)*)\s*\((\d+(?:\.\d+)?)%\)/); - const scoreNum = scoreMatch ? parseInt(scoreMatch[1].replace(/,/g, '')) : null; - const scorePercent = scoreMatch ? parseFloat(scoreMatch[2]) : null; - - // Extract timestamp from in last column - const timestampElem = row.querySelector('td.hidden-from-mobile small'); - let timestamp = null; - if (timestampElem) { - const dateStr = timestampElem.innerText.trim(); // e.g., "2025-06-27 5:23 PM" - timestamp = new Date(dateStr).getTime(); // Unix ms - } - - // Get accordion ID - const accordionId = row.getAttribute('data-target')?.replace('#', ''); - const accordion = document.getElementById(accordionId); - - // Initialize metadata - let lifeLeft = '', justReflec = 0, just = 0, great = 0, good = 0, miss = 0; - - if (accordion) { - const divs = accordion.querySelectorAll('div.col-sm-3, div.col-sm-2, div.col-sm-4'); - divs.forEach(div => { - const label = div.querySelector('strong')?.innerText.trim(); - const value = div.childNodes[div.childNodes.length-1].nodeValue?.trim() || div.querySelector('a')?.innerText.trim() || ''; - switch(label) { - case 'Life Left': lifeLeft = value; break; - case 'Just Reflec': justReflec = parseInt(value) || 0; break; - case 'Just': just = parseInt(value) || 0; break; - case 'Great': great = parseInt(value) || 0; break; - case 'Good': good = parseInt(value) || 0; break; - case 'Miss': miss = parseInt(value) || 0; break; - } - }); - } - - scores.push({ - title: titleElem?.innerText.trim() || '', - artist: artistElem?.innerText.trim() || '', - difficulty: difficultyElem?.innerText.replace(levelElem?.innerText, '').trim() || '', - level: levelElem ? parseInt(levelElem.innerText.trim()) : null, - score: scoreNum, - scorePercent, - lamp: lampElem?.innerText.trim() || '', - lifeLeft: parseInt(lifeLeft) || null, - timestamp, // Unix ms - judgements: { justReflec, just, great, good, miss } - }); - }); - - return scores; - } - -})(); diff --git a/scripts/taiko/.gitignore b/scripts/taiko/.gitignore deleted file mode 100644 index e9aae41..0000000 --- a/scripts/taiko/.gitignore +++ /dev/null @@ -1,218 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -# Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -# poetry.lock -# poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -# pdm.lock -# pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -# pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# Redis -*.rdb -*.aof -*.pid - -# RabbitMQ -mnesia/ -rabbitmq/ -rabbitmq-data/ - -# ActiveMQ -activemq-data/ - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -# .idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ - -# Streamlit -.streamlit/secrets.toml -taiko_charts.json -mirage_donder_hiroba_export.json diff --git a/scripts/taiko/donder-hiroba/.gitignore b/scripts/taiko/donder-hiroba/.gitignore new file mode 100644 index 0000000..e9aae41 --- /dev/null +++ b/scripts/taiko/donder-hiroba/.gitignore @@ -0,0 +1,218 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml +taiko_charts.json +mirage_donder_hiroba_export.json diff --git a/scripts/taiko/donder-hiroba/README.md b/scripts/taiko/donder-hiroba/README.md new file mode 100644 index 0000000..e69de29 diff --git a/scripts/taiko/donder-hiroba/requirements.txt b/scripts/taiko/donder-hiroba/requirements.txt new file mode 100644 index 0000000..adc36d4 --- /dev/null +++ b/scripts/taiko/donder-hiroba/requirements.txt @@ -0,0 +1,2 @@ +beautifulsoup4==4.14.2 +Requests==2.32.5 diff --git a/scripts/taiko/donder-hiroba/taiko_donder_hiroba_export.py b/scripts/taiko/donder-hiroba/taiko_donder_hiroba_export.py new file mode 100644 index 0000000..dd32f3b --- /dev/null +++ b/scripts/taiko/donder-hiroba/taiko_donder_hiroba_export.py @@ -0,0 +1,232 @@ +import requests +from bs4 import BeautifulSoup +import json +import time +import argparse +import os + + +SONG_CATEGORIES = ["pops", "kids", "anime", "vocaloid", "game", "variety", "classic", "namco"] +SONG_LIST_BASE_URL = "https://taiko.namco-ch.net/taiko/en/songlist/" +headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" +} +PLAY_HISTORY_URL = "https://donderhiroba.jp/history_recent_score.php" +DIFFICULTIES = ["support", "easy", "normal", "hard", "oni", "ura_oni"] + +DIFFICULTY_MAP = { + "icon_course02_1_640.png": "EASY", + "icon_course02_2_640.png": "NORMAL", + "icon_course02_3_640.png": "HARD", + "icon_course02_4_640.png": "ONI", + "icon_course02_4_640.png": "URA_ONI" +} + +CROWN_MAP = { + "crown_02_640.png": "FULL COMBO", + "crown_03_640.png": "CLEAR", + "crown_04_640.png": "DONDERFUL COMBO", +} + +LAMP_MAP = { + "best_score_rank_2_640.png": "IKI 1", + "best_score_rank_3_640.png": "IKI 2", + "best_score_rank_4_640.png": "IKI 3", + "best_score_rank_5_640.png": "MIYABI 1", + "best_score_rank_6_640.png": "MIYABI 2", + "best_score_rank_7_640.png": "MIYABI 3", + "best_score_rank_8_640.png": "KIWAMI", +} + +def load_chart_cache(): + with open("taiko_charts.json") as f: + return dict(json.load(f)) + +def build_taiko_chart_metadata(): + """ + Unfortnatly Donder Hiroba doesn't store any data about the level, need to fetch this elsewhere + """ + chart_data = {} + for category in SONG_CATEGORIES: + url = f"{SONG_LIST_BASE_URL}/{category}.php" + print(f"[DATA] Getting {category} category charts") + resp = requests.get(url, headers=headers) + soup = BeautifulSoup(resp.text, 'html.parser') + table = soup.find("tbody") + if table is None: + raise Exception("Unable to fetch chart data for ", category) + rows = table.find_all("tr") + for row in rows: + cols = row.find_all("td") + if len(cols) < 6: + continue + + curr_song = {} + song_metadata = row.find_all("th") + if not song_metadata: + continue + + title_th = song_metadata[0] + artist_tag = title_th.find("p") + song_artist = artist_tag.get_text(strip=True) if artist_tag else "" + + for tag in title_th.find_all(["p", "span"]): + tag.decompose() + song_title = title_th.get_text(strip=True) + + for i in range(len(DIFFICULTIES)): + if DIFFICULTIES[i] == "support": + continue + diff = str(cols[i].get_text()) + curr_song[DIFFICULTIES[i]] = None if diff == "-" else diff + + curr_song["artist"] = song_artist + chart_data[song_title] = curr_song + + with open("taiko_charts.json", "w") as f: + print("Writing charts to cache. Delete this file when new charts come out!") + json.dump(chart_data, f) + return chart_data + +def get_play_hist(token: str, chart_data): + """ + Fetch and parse Donder Hiroba play history page. + Extracts scores, difficulty, ranks, and performance breakdowns. + Handles pagination by going through all pages until duplicate results are found. + """ + all_results = [] + page = 1 + previous_page_titles = set() + + while True: + page_url = f"{PLAY_HISTORY_URL}?page={page}" if page > 1 else PLAY_HISTORY_URL + print(f"[INFO] Fetching page {page}...") + play_hist_page = requests.get(page_url, cookies={"_token_v2": token}, headers=headers) + soup = BeautifulSoup(play_hist_page.text, "html.parser") + scores = soup.find_all(class_="scoreUser") + + if not scores: + print(f"[INFO] No scores found on page {page}. Ending pagination.") + break + + current_page_titles = set() + page_results = [] + + for s in scores: + title_tag = s.find("h2") + title = title_tag.text.strip() if title_tag else None + + total_score_tag = s.find("div", class_="scoreScore") + total_score = total_score_tag.text.strip().replace("η‚Ή", "") if total_score_tag else None + + # Skip unknown songs + if not title or chart_data.get(title) is None: + print(f"[WARN] {title} is unknown in chart_data. Skipping.") + continue + + current_page_titles.add(title) + difficulty = crown = lamp = None + score_element = s.find("div", class_="playDataArea", attrs={"style": True}) + img_tags = score_element.find_all("img") if score_element else [] + + for img in img_tags: + src = img["src"].split("/")[-1] + if src in DIFFICULTY_MAP: + difficulty = DIFFICULTY_MAP[src] + elif src in CROWN_MAP: + crown = CROWN_MAP[src] + elif src in LAMP_MAP: + lamp = LAMP_MAP[src] + + judgements = {} + combo = pound = None + + score_data_area = s.find("div", class_="scoreDataArea") + if score_data_area: + score_elements = score_data_area.find_all("div", class_="playDataArea", recursive=True) + for el in score_elements: + img = el.find("img", class_="score_name") + val_tag = el.find("div", class_="playDataScore") + if not img or not val_tag: + continue + + src = img["src"].split("/")[-1] + value = val_tag.get_text(strip=True).replace("ε›ž", "") + if not value.isdigit(): + continue + value = int(value) + + if "score_name_good" in src: + judgements["good"] = value + elif "score_name_ok" in src: + judgements["ok"] = value + elif "score_name_ng" in src: + judgements["bad"] = value + elif "score_name_combo" in src: + combo = value + elif "score_name_pound" in src: + pound = value + + result_entry = { + "title": title, + "timestamp": 0, + "artist": chart_data[title]["artist"], + "difficulty": difficulty, + "level": int(chart_data[title].get(difficulty.lower(), 0)) if difficulty else None, + "crown_rank": crown, + "score_rank": lamp, + "score": int(total_score) if total_score and total_score.isdigit() else total_score, + "judgements": judgements, + "optional": { + "combo": combo, + "pound": pound + } + } + page_results.append(result_entry) + if page > 1 and current_page_titles.issubset(previous_page_titles): + print(f"[INFO] Page {page} contains duplicate results. Stopping pagination.") + break + + all_results.extend(page_results) + print(f"[INFO] Page {page} processed: {len(page_results)} scores found") + + previous_page_titles.update(current_page_titles) + page += 1 + + print(f"[INFO] Total scores collected: {len(all_results)} across {page - 1} pages") + + return { + "meta": { + "game": "taiko", + "playtype": "Single", + "service": "Donder Hiroba Export" + }, + "scores": all_results, + } + + +if __name__ == "__main__": + print("[ALERT!] Please first refresh your scores on Donder Hiroba so that it has the latest info. Visit: https://donderhiroba.jp/score_list.php and click on the top right\n\n") + print("!Your token will change after doing this!") + parser = argparse.ArgumentParser( + prog="taiko_donder_hiroba_export.py", + description="Exports Taiko no Tatsujin scores from Donder Hiroba into a Mirage compatible JSON", + ) + parser.add_argument("-t", "--token", help="Donder Hiroba _token_v2. See README for instructions on how to get this!", required=True) + args = parser.parse_args() + chart_data = {} + if os.path.exists("taiko_charts.json"): + file_time = os.path.getmtime("taiko_charts.json") + current_time = time.time() + if current_time - file_time > 7 * 24 * 60 * 60: + print("Chart cache is older than 1 week, regenerating...") + chart_data = build_taiko_chart_metadata() + else: + print("Using cached chart data") + chart_data = load_chart_cache() + else: + print("No chart cache found, generating...") + chart_data = build_taiko_chart_metadata() + score_data = get_play_hist(args.token, chart_data) + with open("mirage_donder_hiroba_export.json", "w") as f: + json.dump(score_data, f) diff --git a/scripts/taiko/requirements.txt b/scripts/taiko/requirements.txt deleted file mode 100644 index adc36d4..0000000 --- a/scripts/taiko/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -beautifulsoup4==4.14.2 -Requests==2.32.5 diff --git a/scripts/taiko/taiko_donder_hiroba_export.py b/scripts/taiko/taiko_donder_hiroba_export.py deleted file mode 100644 index dd32f3b..0000000 --- a/scripts/taiko/taiko_donder_hiroba_export.py +++ /dev/null @@ -1,232 +0,0 @@ -import requests -from bs4 import BeautifulSoup -import json -import time -import argparse -import os - - -SONG_CATEGORIES = ["pops", "kids", "anime", "vocaloid", "game", "variety", "classic", "namco"] -SONG_LIST_BASE_URL = "https://taiko.namco-ch.net/taiko/en/songlist/" -headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" -} -PLAY_HISTORY_URL = "https://donderhiroba.jp/history_recent_score.php" -DIFFICULTIES = ["support", "easy", "normal", "hard", "oni", "ura_oni"] - -DIFFICULTY_MAP = { - "icon_course02_1_640.png": "EASY", - "icon_course02_2_640.png": "NORMAL", - "icon_course02_3_640.png": "HARD", - "icon_course02_4_640.png": "ONI", - "icon_course02_4_640.png": "URA_ONI" -} - -CROWN_MAP = { - "crown_02_640.png": "FULL COMBO", - "crown_03_640.png": "CLEAR", - "crown_04_640.png": "DONDERFUL COMBO", -} - -LAMP_MAP = { - "best_score_rank_2_640.png": "IKI 1", - "best_score_rank_3_640.png": "IKI 2", - "best_score_rank_4_640.png": "IKI 3", - "best_score_rank_5_640.png": "MIYABI 1", - "best_score_rank_6_640.png": "MIYABI 2", - "best_score_rank_7_640.png": "MIYABI 3", - "best_score_rank_8_640.png": "KIWAMI", -} - -def load_chart_cache(): - with open("taiko_charts.json") as f: - return dict(json.load(f)) - -def build_taiko_chart_metadata(): - """ - Unfortnatly Donder Hiroba doesn't store any data about the level, need to fetch this elsewhere - """ - chart_data = {} - for category in SONG_CATEGORIES: - url = f"{SONG_LIST_BASE_URL}/{category}.php" - print(f"[DATA] Getting {category} category charts") - resp = requests.get(url, headers=headers) - soup = BeautifulSoup(resp.text, 'html.parser') - table = soup.find("tbody") - if table is None: - raise Exception("Unable to fetch chart data for ", category) - rows = table.find_all("tr") - for row in rows: - cols = row.find_all("td") - if len(cols) < 6: - continue - - curr_song = {} - song_metadata = row.find_all("th") - if not song_metadata: - continue - - title_th = song_metadata[0] - artist_tag = title_th.find("p") - song_artist = artist_tag.get_text(strip=True) if artist_tag else "" - - for tag in title_th.find_all(["p", "span"]): - tag.decompose() - song_title = title_th.get_text(strip=True) - - for i in range(len(DIFFICULTIES)): - if DIFFICULTIES[i] == "support": - continue - diff = str(cols[i].get_text()) - curr_song[DIFFICULTIES[i]] = None if diff == "-" else diff - - curr_song["artist"] = song_artist - chart_data[song_title] = curr_song - - with open("taiko_charts.json", "w") as f: - print("Writing charts to cache. Delete this file when new charts come out!") - json.dump(chart_data, f) - return chart_data - -def get_play_hist(token: str, chart_data): - """ - Fetch and parse Donder Hiroba play history page. - Extracts scores, difficulty, ranks, and performance breakdowns. - Handles pagination by going through all pages until duplicate results are found. - """ - all_results = [] - page = 1 - previous_page_titles = set() - - while True: - page_url = f"{PLAY_HISTORY_URL}?page={page}" if page > 1 else PLAY_HISTORY_URL - print(f"[INFO] Fetching page {page}...") - play_hist_page = requests.get(page_url, cookies={"_token_v2": token}, headers=headers) - soup = BeautifulSoup(play_hist_page.text, "html.parser") - scores = soup.find_all(class_="scoreUser") - - if not scores: - print(f"[INFO] No scores found on page {page}. Ending pagination.") - break - - current_page_titles = set() - page_results = [] - - for s in scores: - title_tag = s.find("h2") - title = title_tag.text.strip() if title_tag else None - - total_score_tag = s.find("div", class_="scoreScore") - total_score = total_score_tag.text.strip().replace("η‚Ή", "") if total_score_tag else None - - # Skip unknown songs - if not title or chart_data.get(title) is None: - print(f"[WARN] {title} is unknown in chart_data. Skipping.") - continue - - current_page_titles.add(title) - difficulty = crown = lamp = None - score_element = s.find("div", class_="playDataArea", attrs={"style": True}) - img_tags = score_element.find_all("img") if score_element else [] - - for img in img_tags: - src = img["src"].split("/")[-1] - if src in DIFFICULTY_MAP: - difficulty = DIFFICULTY_MAP[src] - elif src in CROWN_MAP: - crown = CROWN_MAP[src] - elif src in LAMP_MAP: - lamp = LAMP_MAP[src] - - judgements = {} - combo = pound = None - - score_data_area = s.find("div", class_="scoreDataArea") - if score_data_area: - score_elements = score_data_area.find_all("div", class_="playDataArea", recursive=True) - for el in score_elements: - img = el.find("img", class_="score_name") - val_tag = el.find("div", class_="playDataScore") - if not img or not val_tag: - continue - - src = img["src"].split("/")[-1] - value = val_tag.get_text(strip=True).replace("ε›ž", "") - if not value.isdigit(): - continue - value = int(value) - - if "score_name_good" in src: - judgements["good"] = value - elif "score_name_ok" in src: - judgements["ok"] = value - elif "score_name_ng" in src: - judgements["bad"] = value - elif "score_name_combo" in src: - combo = value - elif "score_name_pound" in src: - pound = value - - result_entry = { - "title": title, - "timestamp": 0, - "artist": chart_data[title]["artist"], - "difficulty": difficulty, - "level": int(chart_data[title].get(difficulty.lower(), 0)) if difficulty else None, - "crown_rank": crown, - "score_rank": lamp, - "score": int(total_score) if total_score and total_score.isdigit() else total_score, - "judgements": judgements, - "optional": { - "combo": combo, - "pound": pound - } - } - page_results.append(result_entry) - if page > 1 and current_page_titles.issubset(previous_page_titles): - print(f"[INFO] Page {page} contains duplicate results. Stopping pagination.") - break - - all_results.extend(page_results) - print(f"[INFO] Page {page} processed: {len(page_results)} scores found") - - previous_page_titles.update(current_page_titles) - page += 1 - - print(f"[INFO] Total scores collected: {len(all_results)} across {page - 1} pages") - - return { - "meta": { - "game": "taiko", - "playtype": "Single", - "service": "Donder Hiroba Export" - }, - "scores": all_results, - } - - -if __name__ == "__main__": - print("[ALERT!] Please first refresh your scores on Donder Hiroba so that it has the latest info. Visit: https://donderhiroba.jp/score_list.php and click on the top right\n\n") - print("!Your token will change after doing this!") - parser = argparse.ArgumentParser( - prog="taiko_donder_hiroba_export.py", - description="Exports Taiko no Tatsujin scores from Donder Hiroba into a Mirage compatible JSON", - ) - parser.add_argument("-t", "--token", help="Donder Hiroba _token_v2. See README for instructions on how to get this!", required=True) - args = parser.parse_args() - chart_data = {} - if os.path.exists("taiko_charts.json"): - file_time = os.path.getmtime("taiko_charts.json") - current_time = time.time() - if current_time - file_time > 7 * 24 * 60 * 60: - print("Chart cache is older than 1 week, regenerating...") - chart_data = build_taiko_chart_metadata() - else: - print("Using cached chart data") - chart_data = load_chart_cache() - else: - print("No chart cache found, generating...") - chart_data = build_taiko_chart_metadata() - score_data = get_play_hist(args.token, chart_data) - with open("mirage_donder_hiroba_export.json", "w") as f: - json.dump(score_data, f) -- cgit v1.2.3