aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-10-08 01:11:53 -0700
committerPinapelz <yukais@pinapelz.com>2025-10-08 01:11:53 -0700
commitf2dfc50001e146fbb2d9a32a6825e415a27b6d18 (patch)
tree49aa6379b0bd432cf82af4c1a8d6383c89b4a884
parent95d1fdd32712721ce065780d4be44ea8f6b6db59 (diff)
implement diva.net score scraper
-rw-r--r--scripts/projectdiva-arcade/diva_net_history.js262
1 files changed, 262 insertions, 0 deletions
diff --git a/scripts/projectdiva-arcade/diva_net_history.js b/scripts/projectdiva-arcade/diva_net_history.js
new file mode 100644
index 0000000..f6b2b2c
--- /dev/null
+++ b/scripts/projectdiva-arcade/diva_net_history.js
@@ -0,0 +1,262 @@
+// ==UserScript==
+// @name 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));
+})();
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage