aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/reflecbeat/flower/reflecbeat_flower_scraper.user.js
blob: dd756df8f199a60ef4e686729c00696954349df2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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 <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,
                scorePercent,
                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