Skip to content

Instantly share code, notes, and snippets.

@clsource
Last active December 26, 2023 18:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clsource/d186654815795621598241e2caef8064 to your computer and use it in GitHub Desktop.
Save clsource/d186654815795621598241e2caef8064 to your computer and use it in GitHub Desktop.
Export Gameplay data from PIU Phoenix Website to CSV (Using TamperMonkey)
// ==UserScript==
// @name PIU.Phoenix.PlayData
// @namespace https://ninjas.cl
// @version 2023-12-25
// @description Save data from Phoenix Gameplays to local csv
// @author Camilo Castro <clsource>
// @match https://piugame.com/my_page/recently_played.php
// @match https://www.piugame.com/my_page/recently_played.php
// @icon https://www.google.com/s2/favicons?sz=64&domain=piugame.com
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/luxon/3.4.4/luxon.min.js
// @require https://cdn.jsdelivr.net/npm/excellentexport@3.4.3/dist/excellentexport.min.js
// @require https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js
// ==/UserScript==
// Eslint
// jQuery already present in website
// Imported Luxon Time Lib
/* global $, luxon, ExcellentExport, md5 */
const get_bg = (element) => {
return element.css("background-image").trim().replace('url("',"").replace('")',"");
};
// All dates are in Default Korea Timezone
const to_iso = (date, time) => {
const timestamp = luxon.DateTime.fromISO(date + "T" + time + "+09:00").setZone("Asia/Seoul");
return {
date: timestamp.toString(),
utc: timestamp.toUTC().toString()
};
};
const sanitize_number = (value) => {
return value.toString().trim().replaceAll(",","");
};
const get_filename = (value) => {
return value.split(/(\\|\/)/g).pop().replace(".png", "");
};
const export_data = () => {
const data = {
player: {},
songs: []
};
const data_name = $(".name_w").children(".t2").first().text().split("#");
data.player.name = data_name[0].trim();
data.player.id = data_name[1].trim();
data.player.title = $(".name_w").children(".t1").first().text();
data.player.points = sanitize_number($(".profile_etc").first().text());
data.player.avatar = get_bg($(".profile_img div.resize").children().first());
// Remove unwanted text and extract only the data then join for time and place
const access_at = $(".time_w ul").first().children("li").first().text().split(":").slice(1).join(":").trim().split(" ");
data.player.access = {
date: access_at[0].trim(),
time: access_at[1].trim(),
tz: "GMT+9",
at: to_iso(access_at[0].trim(), access_at[1].trim()),
in: $(".time_w ul").first().children("li").last().text().split(":").slice(1).join(":").trim(),
};
// Process Songs
const songs = $("ul.recently_playeList li div.wrap_in");
songs.each((index, el) => {
const song = {};
const song_time = $(el).children(".in2").text().trim().split(" ");
song.played = {
date: song_time[0].trim(),
time: song_time[1].trim(),
tz: song_time[2].trim().replace("(", "").replace(")", "")
};
song.played.at = to_iso(song.played.date, song.played.time);
const song_data = $(el).children(".in").first();
song.bg = get_bg(song_data);
song.name = song_data.find(".song_name").first().text();
song.difficulty = {
numbers: []
};
song_data.find(".stepBall_in div img").each((index, el) => {
const image = $(el).attr("src");
if(image.includes("s_text")) {
song.difficulty.type = "single";
} else if(image.includes("d_text")) {
song.difficulty.type = "double";
} else if(image.includes("c_text")) {
song.difficulty.type = "co-op";
}
if(image.includes("_num_")) {
const number = image.substring(image.indexOf("num_") + 4).replace(".png", "");
song.difficulty.numbers.push(number);
}
});
song.difficulty.value = song.difficulty.numbers.join("");
const grade = song_data.find(".li_in.ac img").first().attr("src") || "none";
const plate = song_data.find(".st1 img").first().attr("src") || "none";
const score = sanitize_number(song_data.find(".ac .tx").first().text());
const stage_break = (score == "STAGE BREAK");
song.evaluation = {
grade: get_filename(grade),
plate: get_filename(plate),
score: (stage_break ? "0" : score),
stage_break: stage_break
};
song.steps = {
perfect: sanitize_number(song_data.find(".fontCol1 .tx").first().text()),
great: sanitize_number(song_data.find(".fontCol2 .tx").first().text()),
good: sanitize_number(song_data.find(".fontCol3 .tx").first().text()),
bad: sanitize_number(song_data.find(".fontCol4 .tx").first().text()),
miss: sanitize_number(song_data.find(".fontCol5 .tx").first().text())
};
song.steps.total = parseInt(song.steps.perfect) + parseInt(song.steps.great) + parseInt(song.steps.good) + parseInt(song.steps.bad) + parseInt(song.steps.miss);
// Example total of 689 steps will give 38,023 kcal
// This means that 1 step is 0.0551857764876633 kcal
const kcal_per_step = 0.0551857764876633;
song.steps.kcal = song.steps.total * kcal_per_step;
// Last hash
song.hash = md5(data.player.id + song.name + song.difficulty.type + song.difficulty.value + song.evaluation.score + song.steps.total + song.played.at.utc)
data.songs.push(song);
});
return data;
};
const download_file = (data) => {
console.log("Begin Generating Data File");
// Delete previous table
$("#export-data-table").remove();
// Create a new one
$("body").append(`<table id="export-data-table" style="display:none">
<tr>
<th>Player Id</th>
<th>Player Name</th>
<th>Location</th>
<th>Song Name</th>
<th>Difficulty Type</th>
<th>Difficulty Value</th>
<th>Grade</th>
<th>Plate</th>
<th>Score</th>
<th>Stage Break?</th>
<th>Perfect</th>
<th>Great</th>
<th>Good</th>
<th>Bad</th>
<th>Miss</th>
<th>Total</th>
<th>Kcal</th>
<th>Date</th>
<th>Hash</th>
</tr>
</table>`);
const table = document.getElementById("export-data-table");
data.songs.forEach(song => {
const row = table.insertRow();
const playerId = row.insertCell();
playerId.innerHTML = data.player.id;
const playerName = row.insertCell();
playerName.innerHTML = data.player.name;
const location = row.insertCell();
location.innerHTML = data.player.access.in;
const songName = row.insertCell();
songName.innerHTML = song.name;
const diffType = row.insertCell();
diffType.innerHTML = song.difficulty.type;
const diffValue = row.insertCell();
diffValue.innerHTML = song.difficulty.value;
const grade = row.insertCell();
grade.innerHTML = song.evaluation.grade;
const plate = row.insertCell();
plate.innerHTML = song.evaluation.plate;
const score = row.insertCell();
score.innerHTML = song.evaluation.score;
const stageBreak = row.insertCell();
stageBreak.innerHTML = song.evaluation.stage_break;
const perfect = row.insertCell();
perfect.innerHTML = song.steps.perfect;
const great = row.insertCell();
great.innerHTML = song.steps.great;
const good = row.insertCell();
good.innerHTML = song.steps.good;
const bad = row.insertCell();
bad.innerHTML = song.steps.bad;
const miss = row.insertCell();
miss.innerHTML = song.steps.miss;
const total = row.insertCell();
total.innerHTML = song.steps.total;
const kcal = row.insertCell();
kcal.innerHTML = song.steps.kcal;
const date = row.insertCell();
date.innerHTML = song.played.at.utc;
const hash = row.insertCell();
hash.innerHTML = song.hash;
});
// Export File
return ExcellentExport.convert({
anchor: "btn-export-data",
filename: "piugame-phoenix-" + data.player.id + "-" + luxon.DateTime.now().toString(),
format: "csv"
}, [{
name: 'Gameplay Data',
from: {
table: 'export-data-table'
}
}]);
};
(function() {
'use strict';
console.log("PIU Data Export Ready");
// Add Export Button and Export Table
$(".profile_btn").append('<a href="#" id="btn-export-data" class="btn flex vc mg1" style="margin-left:0.5%"><i class="xi xi-external-link icon"></i><i class="tt">Export Data</i></a>');
$("#btn-export-data").click(() => {
download_file(export_data());
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment