diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-10-08 01:11:53 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-10-08 01:11:53 -0700 |
| commit | f2dfc50001e146fbb2d9a32a6825e415a27b6d18 (patch) | |
| tree | 49aa6379b0bd432cf82af4c1a8d6383c89b4a884 | |
| parent | 95d1fdd32712721ce065780d4be44ea8f6b6db59 (diff) | |
implement diva.net score scraper
| -rw-r--r-- | scripts/projectdiva-arcade/diva_net_history.js | 262 |
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)); +})(); |
