diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-10-12 03:38:28 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-10-12 03:39:01 -0700 |
| commit | 43a43fd0d8b251fe29a09b59010cd16ef2567e0d (patch) | |
| tree | e74e16f21c2138c2d27a8c7e677fb4f85f706815 | |
| parent | 108938f881f03355107358977d1550737486ca04 (diff) | |
FLOWER reflect beat and nostalgia userscripts
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | backend/prisma/seed.ts | 10 | ||||
| -rw-r--r-- | scripts/nostalgia/nostalgia_flower_scraper.js | 186 | ||||
| -rw-r--r-- | scripts/reflecbeat/reflecbeat_flower_scraper.js | 109 |
4 files changed, 306 insertions, 1 deletions
@@ -16,7 +16,7 @@ # Development Install dependencies: ```bash -pnpm install:all1 +pnpm install:all ``` Create a `.env` file in `backend` based on `backend/.env.template`. Fill in the fields as required. diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 01459c5..624e8b7 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -26,6 +26,16 @@ async function main() { formattedName: "MUSIC DIVER", description: "Taito's quadrant based drumming game", }, + { + internalName: "nostalgia", + formattedName: "NOSTALGIA", + description: "A piano based touch music game from KONAMI", + }, + { + internalName: "reflecbeat", + formattedName: "REFLEC BEAT", + description: "A touchscreen rhythm game from KONAMI", + } ], }); console.log("Initial seed data inserted"); diff --git a/scripts/nostalgia/nostalgia_flower_scraper.js b/scripts/nostalgia/nostalgia_flower_scraper.js new file mode 100644 index 0000000..d588432 --- /dev/null +++ b/scripts/nostalgia/nostalgia_flower_scraper.js @@ -0,0 +1,186 @@ +// ==UserScript== +// @name Mirage Nostalgia Score Scraper for FLOWER +// @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 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())); + judgements.fast = parts[0] || 0; + judgements.slow = parts[1] || 0; + } + }); + return judgements; + } + + // 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 = parseJudgements(detailsRow); + + scores.push({ + title: title, + artist: artist, + difficulty: difficulty, + level: level, + score: score, + lamp: lamp, + timestamp: timeAchieved, + judgements: judgements + }); + + 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/reflecbeat/reflecbeat_flower_scraper.js b/scripts/reflecbeat/reflecbeat_flower_scraper.js new file mode 100644 index 0000000..42050d6 --- /dev/null +++ b/scripts/reflecbeat/reflecbeat_flower_scraper.js @@ -0,0 +1,109 @@ +// ==UserScript== +// @name REFLEC BEAT SCORE EXPORT +// @namespace https://example.com/ +// @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.replace(/,/g, '').match(/\d+/); + const scoreNum = scoreMatch ? parseInt(scoreMatch[0]) : null; + + // Extract timestamp from <small> 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, + lamp: lampElem?.innerText.trim() || '', + lifeLeft: parseInt(lifeLeft) || null, + timestamp, // Unix ms + judgements: { justReflec, just, great, good, miss } + }); + }); + + return scores; + } + +})(); |
