aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-10-12 03:38:28 -0700
committerPinapelz <yukais@pinapelz.com>2025-10-12 03:39:01 -0700
commit43a43fd0d8b251fe29a09b59010cd16ef2567e0d (patch)
treee74e16f21c2138c2d27a8c7e677fb4f85f706815
parent108938f881f03355107358977d1550737486ca04 (diff)
FLOWER reflect beat and nostalgia userscripts
-rw-r--r--README.md2
-rw-r--r--backend/prisma/seed.ts10
-rw-r--r--scripts/nostalgia/nostalgia_flower_scraper.js186
-rw-r--r--scripts/reflecbeat/reflecbeat_flower_scraper.js109
4 files changed, 306 insertions, 1 deletions
diff --git a/README.md b/README.md
index 608cb41..0d7e027 100644
--- a/README.md
+++ b/README.md
@@ -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;
+ }
+
+})();
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage