add and run prettier

This commit is contained in:
Lee 2023-10-17 23:38:18 +01:00
parent 47a23f0484
commit f6f56aa09c
136 changed files with 8231 additions and 4493 deletions

@ -24,6 +24,7 @@
"json-stable-stringify": "^1.0.1",
"luxon": "^2.0.2",
"p-queue": "^7.1.0",
"prettier": "3.0.3",
"rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",

File diff suppressed because it is too large Load Diff

@ -54,7 +54,9 @@ select {
color: var(--decrease);
}
*[title]:not([title=""]):not(.clickable) {cursor: help;}
*[title]:not([title=""]):not(.clickable) {
cursor: help;
}
.scoresaber-icon {
width: 100%;
@ -69,8 +71,7 @@ select {
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='72.723976mm' height='63.291668mm' viewBox='0 0 72.723976 63.291668' version='1.1' id='svg3827' sodipodi:docname='bsicon_ter.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cdefs id='defs3821' /%3E%3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='1.4' inkscape:cx='40.905424' inkscape:cy='61.353566' inkscape:document-units='mm' inkscape:current-layer='layer1' showgrid='false' inkscape:window-width='1920' inkscape:window-height='1017' inkscape:window-x='-8' inkscape:window-y='-8' inkscape:window-maximized='1' /%3E%3Cmetadata id='metadata3824'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3Cdc:title%3E%3C/dc:title%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg inkscape:label='Calque 1' inkscape:groupmode='layer' id='layer1' style='opacity:1' transform='translate(-0.72553574,-0.71711111)'%3E%3Crect style='fill:%23000200;fill-opacity:1;stroke:%23000000;stroke-width:1.98928511;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4531' width='63.5' height='10.583332' x='5.4550524' y='10.242103' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Crect style='fill:%23ffffff;fill-opacity:1;stroke:none;stroke-width:3.35483217;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4533' width='6.6145835' height='6.6145835' x='25.298788' y='11.565023' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Crect style='fill:%23ffffff;fill-opacity:1;stroke:none;stroke-width:3.92078424;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4533-5' width='6.6145835' height='6.6145835' x='42.496708' y='11.565023' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Cg id='g4614' transform='rotate(-23.417079,-23.695385,307.31208)' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757'%3E%3Crect y='86.127083' x='125.67709' height='1.3229166' width='50.270832' id='rect4610' style='fill:%230000ff;fill-opacity:1;stroke:%230000ff;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Crect y='85.333328' x='109.80208' height='2.6458333' width='15.875' id='rect4605' style='fill:%23000000;fill-opacity:1;stroke:%23000000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3C/g%3E%3Cg transform='rotate(-156.98422,82.908484,73.919009)' id='g4614-3' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757'%3E%3Crect y='86.127083' x='125.67709' height='1.3229166' width='50.270832' id='rect4610-6' style='fill:%23ff0000;fill-opacity:1;stroke:%23ff0000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Crect y='85.333328' x='109.80208' height='2.6458333' width='15.875' id='rect4605-7' style='fill:%23000000;fill-opacity:1;stroke:%23000000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A"
);
background-image: url("data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='72.723976mm' height='63.291668mm' viewBox='0 0 72.723976 63.291668' version='1.1' id='svg3827' sodipodi:docname='bsicon_ter.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cdefs id='defs3821' /%3E%3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='1.4' inkscape:cx='40.905424' inkscape:cy='61.353566' inkscape:document-units='mm' inkscape:current-layer='layer1' showgrid='false' inkscape:window-width='1920' inkscape:window-height='1017' inkscape:window-x='-8' inkscape:window-y='-8' inkscape:window-maximized='1' /%3E%3Cmetadata id='metadata3824'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3Cdc:title%3E%3C/dc:title%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg inkscape:label='Calque 1' inkscape:groupmode='layer' id='layer1' style='opacity:1' transform='translate(-0.72553574,-0.71711111)'%3E%3Crect style='fill:%23000200;fill-opacity:1;stroke:%23000000;stroke-width:1.98928511;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4531' width='63.5' height='10.583332' x='5.4550524' y='10.242103' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Crect style='fill:%23ffffff;fill-opacity:1;stroke:none;stroke-width:3.35483217;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4533' width='6.6145835' height='6.6145835' x='25.298788' y='11.565023' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Crect style='fill:%23ffffff;fill-opacity:1;stroke:none;stroke-width:3.92078424;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4533-5' width='6.6145835' height='6.6145835' x='42.496708' y='11.565023' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Cg id='g4614' transform='rotate(-23.417079,-23.695385,307.31208)' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757'%3E%3Crect y='86.127083' x='125.67709' height='1.3229166' width='50.270832' id='rect4610' style='fill:%230000ff;fill-opacity:1;stroke:%230000ff;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Crect y='85.333328' x='109.80208' height='2.6458333' width='15.875' id='rect4605' style='fill:%23000000;fill-opacity:1;stroke:%23000000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3C/g%3E%3Cg transform='rotate(-156.98422,82.908484,73.919009)' id='g4614-3' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757'%3E%3Crect y='86.127083' x='125.67709' height='1.3229166' width='50.270832' id='rect4610-6' style='fill:%23ff0000;fill-opacity:1;stroke:%23ff0000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Crect y='85.333328' x='109.80208' height='2.6458333' width='15.875' id='rect4605-7' style='fill:%23000000;fill-opacity:1;stroke:%23000000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");
}
.accsaber-icon {

@ -6,4 +6,89 @@
* @author John Doherty <www.johndoherty.info>
* @license MIT
*/
!function(t,e){"use strict";"function"!=typeof t.CustomEvent&&(t.CustomEvent=function(t,n){n=n||{bubbles:!1,cancelable:!1,detail:void 0};var a=e.createEvent("CustomEvent");return a.initCustomEvent(t,n.bubbles,n.cancelable,n.detail),a},t.CustomEvent.prototype=t.Event.prototype),e.addEventListener("touchstart",function(t){if("true"===t.target.getAttribute("data-swipe-ignore"))return;s=t.target,r=Date.now(),n=t.touches[0].clientX,a=t.touches[0].clientY,u=0,i=0},!1),e.addEventListener("touchmove",function(t){if(!n||!a)return;var e=t.touches[0].clientX,r=t.touches[0].clientY;u=n-e,i=a-r},!1),e.addEventListener("touchend",function(t){if(s!==t.target)return;var e=parseInt(l(s,"data-swipe-threshold","20"),10),o=parseInt(l(s,"data-swipe-timeout","500"),10),c=Date.now()-r,d="",p=t.changedTouches||t.touches||[];Math.abs(u)>Math.abs(i)?Math.abs(u)>e&&c<o&&(d=u>0?"swiped-left":"swiped-right"):Math.abs(i)>e&&c<o&&(d=i>0?"swiped-up":"swiped-down");if(""!==d){var b={dir:d.replace(/swiped-/,""),xStart:parseInt(n,10),xEnd:parseInt((p[0]||{}).clientX||-1,10),yStart:parseInt(a,10),yEnd:parseInt((p[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent("swiped",{bubbles:!0,cancelable:!0,detail:b})),s.dispatchEvent(new CustomEvent(d,{bubbles:!0,cancelable:!0,detail:b}))}n=null,a=null,r=null},!1);var n=null,a=null,u=null,i=null,r=null,s=null;function l(t,n,a){for(;t&&t!==e.documentElement;){var u=t.getAttribute(n);if(u)return u;t=t.parentNode}return a}}(window,document);
!(function (t, e) {
"use strict";
"function" != typeof t.CustomEvent &&
((t.CustomEvent = function (t, n) {
n = n || { bubbles: !1, cancelable: !1, detail: void 0 };
var a = e.createEvent("CustomEvent");
return a.initCustomEvent(t, n.bubbles, n.cancelable, n.detail), a;
}),
(t.CustomEvent.prototype = t.Event.prototype)),
e.addEventListener(
"touchstart",
function (t) {
if ("true" === t.target.getAttribute("data-swipe-ignore")) return;
(s = t.target),
(r = Date.now()),
(n = t.touches[0].clientX),
(a = t.touches[0].clientY),
(u = 0),
(i = 0);
},
!1,
),
e.addEventListener(
"touchmove",
function (t) {
if (!n || !a) return;
var e = t.touches[0].clientX,
r = t.touches[0].clientY;
(u = n - e), (i = a - r);
},
!1,
),
e.addEventListener(
"touchend",
function (t) {
if (s !== t.target) return;
var e = parseInt(l(s, "data-swipe-threshold", "20"), 10),
o = parseInt(l(s, "data-swipe-timeout", "500"), 10),
c = Date.now() - r,
d = "",
p = t.changedTouches || t.touches || [];
Math.abs(u) > Math.abs(i)
? Math.abs(u) > e &&
c < o &&
(d = u > 0 ? "swiped-left" : "swiped-right")
: Math.abs(i) > e &&
c < o &&
(d = i > 0 ? "swiped-up" : "swiped-down");
if ("" !== d) {
var b = {
dir: d.replace(/swiped-/, ""),
xStart: parseInt(n, 10),
xEnd: parseInt((p[0] || {}).clientX || -1, 10),
yStart: parseInt(a, 10),
yEnd: parseInt((p[0] || {}).clientY || -1, 10),
};
s.dispatchEvent(
new CustomEvent("swiped", {
bubbles: !0,
cancelable: !0,
detail: b,
}),
),
s.dispatchEvent(
new CustomEvent(d, { bubbles: !0, cancelable: !0, detail: b }),
);
}
(n = null), (a = null), (r = null);
},
!1,
);
var n = null,
a = null,
u = null,
i = null,
r = null,
s = null;
function l(t, n, a) {
for (; t && t !== e.documentElement; ) {
var u = t.getAttribute(n);
if (u) return u;
t = t.parentNode;
}
return a;
}
})(window, document);

@ -1,21 +1,23 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>ScoreSaber Reloaded</title>
<link rel='icon' type='image/png' href='/assets/favicon.png' />
<link rel='stylesheet' href='/assets/ss-bulma.css' />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" />
<link rel="icon" type="image/png" href="/assets/favicon.png" />
<link rel="stylesheet" href="/assets/ss-bulma.css" />
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.15.4/css/all.css"
/>
<link rel="stylesheet" href="/assets/ssr.css?20210925" />
<link rel='stylesheet' href='/build/bundle.css' />
<link rel="stylesheet" href="/build/bundle.css" />
<script src="/assets/swiped-events.min.js"></script>
<script defer src='/build/bundle.js'></script>
<script defer src="/build/bundle.js"></script>
</head>
<body>
</body>
<body></body>
</html>

@ -1,22 +1,27 @@
const fs = require('fs');
const path = require('path');
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import css from 'rollup-plugin-css-only';
import svg from 'rollup-plugin-svg';
import svelte from "rollup-plugin-svelte";
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import livereload from "rollup-plugin-livereload";
import { terser } from "rollup-plugin-terser";
import sveltePreprocess from "svelte-preprocess";
import css from "rollup-plugin-css-only";
import svg from "rollup-plugin-svg";
const production = !process.env.ROLLUP_WATCH;
const buildVersion = execSync("git rev-parse --short HEAD").toString();
fs.writeFileSync('build-info.js', 'export default ' + JSON.stringify({
buildDate: (new Date()).toISOString().substr(0, 19).replace('T', ' ') + ' UTC',
buildVersion
}))
fs.writeFileSync(
"build-info.js",
"export default " +
JSON.stringify({
buildDate:
new Date().toISOString().substr(0, 19).replace("T", " ") + " UTC",
buildVersion,
}),
);
function serve() {
let server;
@ -28,25 +33,29 @@ function serve() {
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
server = require("child_process").spawn(
"npm",
["run", "start", "--", "--dev"],
{
stdio: ["ignore", "inherit", "inherit"],
shell: true,
},
);
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
process.on("SIGTERM", toExit);
process.on("exit", toExit);
},
};
}
export default [
{
input: 'src/main.js',
input: "src/main.js",
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js',
format: "iife",
name: "app",
file: "public/build/bundle.js",
},
plugins: [
svelte({
@ -58,7 +67,7 @@ export default [
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({output: 'bundle.css'}),
css({ output: "bundle.css" }),
svg(),
@ -69,7 +78,7 @@ export default [
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte'],
dedupe: ["svelte"],
}),
commonjs(),
@ -79,23 +88,23 @@ export default [
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
!production && livereload("public"),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser(),
{
name: 'copy-comlink',
name: "copy-comlink",
generateBundle() {
const buildDir = './public/build'
const buildDir = "./public/build";
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir);
}
fs.copyFileSync(
path.resolve('./node_modules/comlink/dist/umd/comlink.min.js'),
path.resolve('./public/build/comlink.min.js'),
path.resolve("./node_modules/comlink/dist/umd/comlink.min.js"),
path.resolve("./public/build/comlink.min.js"),
);
},
},
@ -106,12 +115,12 @@ export default [
},
{
input: 'src/workers/stats-worker.js',
input: "src/workers/stats-worker.js",
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/stats-worker.js',
format: "iife",
name: "app",
file: "public/build/stats-worker.js",
},
plugins: [
// If you have external dependencies installed from
@ -121,7 +130,7 @@ export default [
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte'],
dedupe: ["svelte"],
}),
commonjs(),
@ -129,19 +138,19 @@ export default [
// instead of npm run dev), minify
production && terser(),
{
name: 'copy-test-worker',
name: "copy-test-worker",
load() {
this.addWatchFile(path.resolve('./src/workers/stats-worker.js'));
this.addWatchFile(path.resolve("./src/workers/stats-worker.js"));
},
generateBundle() {
const buildDir = './public/build'
const buildDir = "./public/build";
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir);
}
fs.copyFileSync(
path.resolve('./src/workers/stats-worker.js'),
path.resolve('./public/build/stats-worker.js'),
path.resolve("./src/workers/stats-worker.js"),
path.resolve("./public/build/stats-worker.js"),
);
},
},

@ -17,15 +17,23 @@ export const onLegendClick = (event, legendItem, legend) => {
if (legend?.chart) {
const yAxisIdsToShow = (legend?.legendItems ?? [])
.sort((a,b) => (ci?.config?.data?.datasets?.[a?.datasetIndex]?.axisOrder ?? a?.datasetIndex) - (ci?.config?.data?.datasets?.[b?.datasetIndex]?.axisOrder ?? b?.datasetIndex))
.reduce((cum, legendItem) => {
.sort(
(a, b) =>
(ci?.config?.data?.datasets?.[a?.datasetIndex]?.axisOrder ??
a?.datasetIndex) -
(ci?.config?.data?.datasets?.[b?.datasetIndex]?.axisOrder ??
b?.datasetIndex),
)
.reduce(
(cum, legendItem) => {
// done
if (cum.second) return cum;
// skip hidden legend items
if (legendItem?.hidden) return cum;
const yAxisId = ci?.getDatasetMeta(legendItem?.datasetIndex)?.yAxisID ?? null;
const yAxisId =
ci?.getDatasetMeta(legendItem?.datasetIndex)?.yAxisID ?? null;
if (!yAxisId) return cum;
if (!cum.first) {
@ -35,20 +43,26 @@ export const onLegendClick = (event, legendItem, legend) => {
}
return cum;
}, {first: null, second: null});
},
{ first: null, second: null },
);
Object.keys(yAxes).forEach(currentAxisKey => {
if (![yAxisIdsToShow.first, yAxisIdsToShow.second].includes(currentAxisKey)) {
Object.keys(yAxes).forEach((currentAxisKey) => {
if (
![yAxisIdsToShow.first, yAxisIdsToShow.second].includes(currentAxisKey)
) {
yAxes[currentAxisKey].display = false;
return;
}
yAxes[currentAxisKey].display = true;
if (yAxisIdsToShow.first === currentAxisKey) yAxes[currentAxisKey].position = 'left';
if (yAxisIdsToShow.second === currentAxisKey) yAxes[currentAxisKey].position = 'right';
if (yAxisIdsToShow.first === currentAxisKey)
yAxes[currentAxisKey].position = "left";
if (yAxisIdsToShow.second === currentAxisKey)
yAxes[currentAxisKey].position = "right";
});
legend.chart.options.scales = {x: xAxis, ...yAxes}
legend.chart.options.scales = { x: xAxis, ...yAxes };
legend.chart.update();
}
}
};

@ -1,9 +1,13 @@
export default {
id: 'regions',
id: "regions",
beforeDraw(chart, args, options) {
if (!options?.regions || !Array.isArray(options.regions)) return;
const {ctx, chartArea: {left, top, right, bottom}, scales: {y}} = chart;
const {
ctx,
chartArea: { left, top, right, bottom },
scales: { y },
} = chart;
const width = right - left;
let fontSize = parseInt(ctx.font, 10);
@ -11,12 +15,15 @@ export default {
ctx.save();
options.regions.forEach(region => {
options.regions.forEach((region) => {
if (y.min <= region.max && y.max >= region.min) {
const minY = Math.max(region.min, y.min);
const maxY = Math.min(region.max, y.max);
const top = y.getPixelForValue(maxY);
const height = region.min === region.max ? 1 : y.getPixelForValue(minY) - y.getPixelForValue(maxY);
const height =
region.min === region.max
? 1
: y.getPixelForValue(minY) - y.getPixelForValue(maxY);
ctx.fillStyle = region.color;
ctx.fillRect(left, top, width, height);
@ -24,16 +31,20 @@ export default {
if (region.label) {
const labelWidth = ctx.measureText(region.label)?.width ?? 0;
ctx.textBaseline = 'top';
ctx.textBaseline = "top";
ctx.fillText(
region.label,
region?.position?.horizontal === 'right' ? right - labelWidth - 3 : left + 3,
region?.position?.vertical === 'bottom' ? top + 2 : top - fontSize - 1
region?.position?.horizontal === "right"
? right - labelWidth - 3
: left + 3,
region?.position?.vertical === "bottom"
? top + 2
: top - fontSize - 1,
);
}
}
})
});
ctx.restore();
}
}
},
};

@ -70,7 +70,7 @@ function updateScoresStats(playerData, playerStats) {
bgColor: "var(--ppColour)",
},
]
: []
: [],
)
.filter((s) => s && (!playerStats || s.label !== "Average"));
}

@ -2,27 +2,34 @@ export default () => {
let currentService = null;
let currentServiceParams = {};
const getAllServices = () => ['scoresaber', 'beatsavior', 'accsaber'];
const getAllServices = () => ["scoresaber", "beatsavior", "accsaber"];
const get = () => ({ service: currentService, params: currentServiceParams });
const getDefaultParams = service => {
const getDefaultParams = (service) => {
switch (service) {
case 'beatsavior':
return {sort: 'recent', order: 'desc', page: 1, filters: {}};
case "beatsavior":
return { sort: "recent", order: "desc", page: 1, filters: {} };
case 'accsaber':
return {type: 'overall', order: 'desc', sort: 'ap', page: 1, filters: {}}
case "accsaber":
return {
type: "overall",
order: "desc",
sort: "ap",
page: 1,
filters: {},
};
case 'scoresaber':
case "scoresaber":
default:
return {sort: 'recent', order: 'desc', page: 1, filters: {}}
}
return { sort: "recent", order: "desc", page: 1, filters: {} };
}
};
const update = (serviceParams = {}, service = currentService) => {
const availableServices = getAllServices();
if (!availableServices.includes(service)) service = availableServices?.[0] ?? 'scoresaber';
if (!availableServices.includes(service))
service = availableServices?.[0] ?? "scoresaber";
const defaultServiceParams = getDefaultParams(service);
@ -32,84 +39,103 @@ export default () => {
}
// preserve old filters
serviceParams = {...serviceParams}
serviceParams = { ...serviceParams };
serviceParams.filters = {
...(currentServiceParams?.filters ?? {}),
...(serviceParams?.filters ?? {}),
}
};
currentService = service;
currentServiceParams = {...defaultServiceParams, ...currentServiceParams, ...serviceParams}
currentServiceParams = {
...defaultServiceParams,
...currentServiceParams,
...serviceParams,
};
return get();
}
};
const clearServiceParams = () => currentServiceParams = {}
const clearServiceParams = () => (currentServiceParams = {});
const initFromUrl = (url = null) => {
const availableServices = getAllServices();
const defaultService = availableServices?.[0] ?? 'scoresaber';
const paramsArr = url ? url.split('/') : [defaultService];
const defaultService = availableServices?.[0] ?? "scoresaber";
const paramsArr = url ? url.split("/") : [defaultService];
const service = paramsArr[0] ?? 'scoresaber';
const service = paramsArr[0] ?? "scoresaber";
const serviceDefaultParams = getDefaultParams(service);
switch (service) {
case 'beatsavior':
case "beatsavior":
return update(
{
sort: paramsArr[1] ?? serviceDefaultParams?.sort,
order: 'desc',
order: "desc",
page: paramsArr[2] ?? serviceDefaultParams?.page,
},
service,
);
case 'accsaber':
case "accsaber":
return update(
{
type: paramsArr[1] ?? serviceDefaultParams?.type,
sort: paramsArr[2] ?? serviceDefaultParams?.sort,
order: (paramsArr[2] ?? serviceDefaultParams?.sort) === 'rank' ? 'asc' : 'desc',
order:
(paramsArr[2] ?? serviceDefaultParams?.sort) === "rank"
? "asc"
: "desc",
page: paramsArr[3] ?? serviceDefaultParams?.page,
},
service,
);
case 'scoresaber':
case "scoresaber":
default:
return update(
{
sort: paramsArr[1] ?? serviceDefaultParams?.sort,
order: (paramsArr[1] ?? serviceDefaultParams?.sort) === 'rank' ? 'asc' : 'desc',
order:
(paramsArr[1] ?? serviceDefaultParams?.sort) === "rank"
? "asc"
: "desc",
page: paramsArr[2] ?? serviceDefaultParams?.page,
},
service,
);
}
}
};
const getUrl = (service, params = {}, noPage = false) => {
if (!service) return '';
if (!service) return "";
const serviceDefaultParams = getDefaultParams(service);
switch (service) {
case 'beatsavior':
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
case "beatsavior":
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${
noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`
}`;
case 'accsaber':
return `${service}/${params?.type ?? serviceDefaultParams?.type}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
case "accsaber":
return `${service}/${params?.type ?? serviceDefaultParams?.type}/${
params?.sort ?? serviceDefaultParams?.sort
}${noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`}`;
case 'scoresaber':
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
}
case "scoresaber":
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${
noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`
}`;
}
};
const getCurrentServiceUrl = () => getUrl(currentService, currentServiceParams);
const getCurrentServiceUrlWithoutPage = () => getUrl(currentService, currentServiceParams, true);
const getDefaultServiceUrl = (service = currentService) => getUrl(service, {});
const getCurrentServiceUrl = () =>
getUrl(currentService, currentServiceParams);
const getCurrentServiceUrlWithoutPage = () =>
getUrl(currentService, currentServiceParams, true);
const getDefaultServiceUrl = (service = currentService) =>
getUrl(service, {});
return {
getAvailableServices: getAllServices,
@ -122,5 +148,5 @@ export default () => {
getParams: () => currentServiceParams,
update,
clearServiceParams,
}
}
};
};

@ -1,19 +1,29 @@
import eventBus from '../utils/broadcast-channel-pubsub'
import eventBus from "../utils/broadcast-channel-pubsub";
export default (name, getObjKey) => {
let cache = {};
// update data cached on another node
eventBus.on('cache-key-set-' + name, ({key, value}, isLocal) => !isLocal ? set(key, value, false) : null);
eventBus.on('cache-all-set' + name, ({data}, isLocal) => !isLocal ? setAll(data, false) : null);
eventBus.on('cache-merge-' + name, ({data}, isLocal) => !isLocal ? merge(data, false) : null);
eventBus.on('cache-key-forget-' + name, ({key}, isLocal) => !isLocal ? forget(key, false) : null);
eventBus.on('cache-flush-' + name, (_, isLocal) => !isLocal ? flush(false) : null);
eventBus.on("cache-key-set-" + name, ({ key, value }, isLocal) =>
!isLocal ? set(key, value, false) : null,
);
eventBus.on("cache-all-set" + name, ({ data }, isLocal) =>
!isLocal ? setAll(data, false) : null,
);
eventBus.on("cache-merge-" + name, ({ data }, isLocal) =>
!isLocal ? merge(data, false) : null,
);
eventBus.on("cache-key-forget-" + name, ({ key }, isLocal) =>
!isLocal ? forget(key, false) : null,
);
eventBus.on("cache-flush-" + name, (_, isLocal) =>
!isLocal ? flush(false) : null,
);
const set = (key, value, emitEvent = true) => {
cache[key] = value;
if (emitEvent) eventBus.publish('cache-key-set-' + name, {key, value});
if (emitEvent) eventBus.publish("cache-key-set-" + name, { key, value });
return value;
};
@ -21,17 +31,17 @@ export default (name, getObjKey) => {
const setAll = (data, emitEvent = true) => {
cache = data;
if (emitEvent) eventBus.publish('cache-all-set-' + name, {data});
if (emitEvent) eventBus.publish("cache-all-set-" + name, { data });
return cache;
}
};
const merge = (data, emitEvent = true) => {
cache = {...cache, ...data}
cache = { ...cache, ...data };
if (emitEvent) eventBus.publish('cache-merge-' + name, {data});
if (emitEvent) eventBus.publish("cache-merge-" + name, { data });
return cache;
}
};
const get = async (key, fetchFunc) => {
if (cache.hasOwnProperty(key)) return cache[key];
@ -53,42 +63,43 @@ export default (name, getObjKey) => {
const key = getObjKey(value);
return set(key, value);
}
};
const getAll = () => cache;
const has = key => cache[key] !== undefined;
const has = (key) => cache[key] !== undefined;
const getKeys = () => Object.keys(cache);
const forget = (key, emitEvent = true) => {
delete cache[key];
if (emitEvent) eventBus.publish('cache-key-forget-' + name, {key});
if (emitEvent) eventBus.publish("cache-key-forget-" + name, { key });
return cache;
}
};
const forgetByFilter = (filterFunc, emitEvent = true) => {
if (!filterFunc) return false;
Object.keys(cache).filter(key => filterFunc(cache[key]))
.forEach(key => {
delete cache[key]
Object.keys(cache)
.filter((key) => filterFunc(cache[key]))
.forEach((key) => {
delete cache[key];
if (emitEvent) eventBus.publish('cache-key-forget-' + name, {key});
if (emitEvent) eventBus.publish("cache-key-forget-" + name, { key });
});
return true;
}
};
const flush = (emitEvent = true) => {
cache = {};
if (emitEvent) eventBus.publish('cache-flush-' + name, {});
if (emitEvent) eventBus.publish("cache-flush-" + name, {});
return cache;
}
};
return {
has,
@ -102,5 +113,5 @@ export default (name, getObjKey) => {
forget,
forgetByFilter,
flush,
}
}
};
};

@ -1,195 +1,279 @@
import {openDB} from 'idb'
import log from '../utils/logger'
import {isDateObject} from '../utils/js'
import eventBus from '../utils/broadcast-channel-pubsub'
import { openDB } from "idb";
import log from "../utils/logger";
import { isDateObject } from "../utils/js";
import eventBus from "../utils/broadcast-channel-pubsub";
const SSR_DB_VERSION = 12;
export let db = null;
export default async () => {
IDBKeyRange.prototype.toString = function () {
return "IDBKeyRange-" + (isDateObject(this.lower) ? this.lower.getTime() : this.lower) + '-' + (isDateObject(this.upper) ? this.upper : this.upper);
}
return (
"IDBKeyRange-" +
(isDateObject(this.lower) ? this.lower.getTime() : this.lower) +
"-" +
(isDateObject(this.upper) ? this.upper : this.upper)
);
};
return await openDatabase();
}
};
async function openDatabase() {
try {
let dbNewVersion = 0, dbOldVersion = 0;
let dbNewVersion = 0,
dbOldVersion = 0;
db = await openDB('ssr', SSR_DB_VERSION, {
db = await openDB("ssr", SSR_DB_VERSION, {
async upgrade(db, oldVersion, newVersion, transaction) {
log.info(`Converting database from version ${oldVersion} to version ${newVersion}`);
log.info(
`Converting database from version ${oldVersion} to version ${newVersion}`,
);
dbNewVersion = newVersion;
dbOldVersion = oldVersion;
switch (true) {
case newVersion >= 1 && oldVersion <= 0:
db.createObjectStore('players', {
keyPath: 'id',
db.createObjectStore("players", {
keyPath: "id",
autoIncrement: false,
});
const playersHistory = db.createObjectStore('players-history', {
keyPath: '_idbId',
const playersHistory = db.createObjectStore("players-history", {
keyPath: "_idbId",
autoIncrement: true,
});
playersHistory.createIndex('players-history-playerId', 'playerId', {unique: false});
playersHistory.createIndex('players-history-timestamp', 'timestamp', {unique: false});
playersHistory.createIndex("players-history-playerId", "playerId", {
unique: false,
});
playersHistory.createIndex(
"players-history-timestamp",
"timestamp",
{ unique: false },
);
const scoresStore = db.createObjectStore('scores', {
keyPath: 'id',
const scoresStore = db.createObjectStore("scores", {
keyPath: "id",
autoIncrement: false,
});
scoresStore.createIndex('scores-leaderboardId', 'leaderboardId', {unique: false});
scoresStore.createIndex('scores-playerId', 'playerId', {unique: false});
scoresStore.createIndex('scores-timeset', 'timeset', {unique: false});
scoresStore.createIndex('scores-pp', 'pp', {unique: false});
scoresStore.createIndex("scores-leaderboardId", "leaderboardId", {
unique: false,
});
scoresStore.createIndex("scores-playerId", "playerId", {
unique: false,
});
scoresStore.createIndex("scores-timeset", "timeset", {
unique: false,
});
scoresStore.createIndex("scores-pp", "pp", { unique: false });
db.createObjectStore('rankeds', {
keyPath: 'leaderboardId',
db.createObjectStore("rankeds", {
keyPath: "leaderboardId",
autoIncrement: false,
});
const songsStore = db.createObjectStore('songs', {
keyPath: 'hash',
const songsStore = db.createObjectStore("songs", {
keyPath: "hash",
autoIncrement: false,
});
songsStore.createIndex('songs-key', 'key', {unique: true});
songsStore.createIndex("songs-key", "key", { unique: true });
db.createObjectStore('twitch', {
keyPath: 'playerId',
db.createObjectStore("twitch", {
keyPath: "playerId",
autoIncrement: false,
});
const rankedsChangesStore = db.createObjectStore('rankeds-changes', {
keyPath: '_idbId',
const rankedsChangesStore = db.createObjectStore(
"rankeds-changes",
{
keyPath: "_idbId",
autoIncrement: true,
});
rankedsChangesStore.createIndex('rankeds-changes-timestamp', 'timestamp', {unique: false});
rankedsChangesStore.createIndex('rankeds-changes-leaderboardId', 'leaderboardId', {unique: false});
},
);
rankedsChangesStore.createIndex(
"rankeds-changes-timestamp",
"timestamp",
{ unique: false },
);
rankedsChangesStore.createIndex(
"rankeds-changes-leaderboardId",
"leaderboardId",
{ unique: false },
);
// no autoIncrement, no keyPath - key must be provided
db.createObjectStore('key-value');
db.createObjectStore("key-value");
db.createObjectStore('cache');
db.createObjectStore("cache");
const groups = db.createObjectStore('groups', {keyPath: '_idbId', autoIncrement: true});
groups.createIndex('groups-name', 'name', {unique: false});
groups.createIndex('groups-playerId', 'playerId', {unique: false});
const groups = db.createObjectStore("groups", {
keyPath: "_idbId",
autoIncrement: true,
});
groups.createIndex("groups-name", "name", { unique: false });
groups.createIndex("groups-playerId", "playerId", {
unique: false,
});
const beatSaviorFiles = db.createObjectStore('beat-savior-files', {
keyPath: 'fileId',
const beatSaviorFiles = db.createObjectStore("beat-savior-files", {
keyPath: "fileId",
autoIncrement: false,
});
const beatSavior = db.createObjectStore('beat-savior', {
keyPath: 'beatSaviorId',
const beatSavior = db.createObjectStore("beat-savior", {
keyPath: "beatSaviorId",
autoIncrement: false,
});
beatSavior.createIndex('beat-savior-playerId', 'playerId', {unique: false});
beatSavior.createIndex('beat-savior-songId', 'songId', {unique: false});
beatSavior.createIndex('beat-savior-fileId', 'fileId', {unique: false});
beatSavior.createIndex("beat-savior-playerId", "playerId", {
unique: false,
});
beatSavior.createIndex("beat-savior-songId", "songId", {
unique: false,
});
beatSavior.createIndex("beat-savior-fileId", "fileId", {
unique: false,
});
// NO break here!
case newVersion >= 2 && oldVersion <= 1:
db.createObjectStore('beat-savior-players', {
keyPath: 'playerId',
db.createObjectStore("beat-savior-players", {
keyPath: "playerId",
autoIncrement: false,
});
// NO break here!
case newVersion >= 3 && oldVersion <= 2:
db.deleteObjectStore('players');
db.deleteObjectStore("players");
db.createObjectStore('players', {
keyPath: 'playerId',
db.createObjectStore("players", {
keyPath: "playerId",
autoIncrement: false,
});
const scoresStore4 = transaction.objectStore('scores');
scoresStore4.deleteIndex('scores-timeset');
scoresStore4.createIndex('scores-timeSet', 'timeSet', {unique: false});
const scoresStore4 = transaction.objectStore("scores");
scoresStore4.deleteIndex("scores-timeset");
scoresStore4.createIndex("scores-timeSet", "timeSet", {
unique: false,
});
// NO break here
case newVersion >= 4 && oldVersion <= 3:
db.deleteObjectStore('beat-savior-files');
db.deleteObjectStore("beat-savior-files");
const beatSaviorStore = transaction.objectStore('beat-savior');
beatSaviorStore.deleteIndex('beat-savior-fileId');
beatSaviorStore.deleteIndex('beat-savior-songId');
const beatSaviorStore = transaction.objectStore("beat-savior");
beatSaviorStore.deleteIndex("beat-savior-fileId");
beatSaviorStore.deleteIndex("beat-savior-songId");
// NO break here
case newVersion >= 5 && oldVersion <= 4:
const songsBeatMapsStore = db.createObjectStore('songs-beatmaps', {
keyPath: 'hash',
const songsBeatMapsStore = db.createObjectStore("songs-beatmaps", {
keyPath: "hash",
autoIncrement: false,
});
songsBeatMapsStore.createIndex('songs-beatmaps--key', 'key', {unique: true});
songsBeatMapsStore.createIndex("songs-beatmaps--key", "key", {
unique: true,
});
// NO break here
case newVersion >= 6 && oldVersion <= 5:
const songsBeatMapsStorev6 = transaction.objectStore('songs-beatmaps');
songsBeatMapsStorev6.deleteIndex('songs-beatmaps--key');
songsBeatMapsStorev6.createIndex('songs-beatmaps-key', 'key', {unique: true});
const songsBeatMapsStorev6 =
transaction.objectStore("songs-beatmaps");
songsBeatMapsStorev6.deleteIndex("songs-beatmaps--key");
songsBeatMapsStorev6.createIndex("songs-beatmaps-key", "key", {
unique: true,
});
// NO break here
case newVersion >= 7 && oldVersion <= 6:
const scoresUpdateQueue = db.createObjectStore('scores-update-queue', {
keyPath: 'id',
const scoresUpdateQueue = db.createObjectStore(
"scores-update-queue",
{
keyPath: "id",
autoIncrement: false,
});
scoresUpdateQueue.createIndex('scores-update-queue-fetchedAt', 'fetchedAt', {unique: false});
},
);
scoresUpdateQueue.createIndex(
"scores-update-queue-fetchedAt",
"fetchedAt",
{ unique: false },
);
case newVersion >= 8 && oldVersion <= 7:
const beatSaviorStorev8 = transaction.objectStore('beat-savior');
beatSaviorStorev8.createIndex('beat-savior-hash', 'hash', {unique: false});
const beatSaviorStorev8 = transaction.objectStore("beat-savior");
beatSaviorStorev8.createIndex("beat-savior-hash", "hash", {
unique: false,
});
// NO break here
case newVersion >= 9 && oldVersion <= 8:
const playersHistoryStorev9 = transaction.objectStore('players-history');
playersHistoryStorev9.deleteIndex('players-history-timestamp');
playersHistoryStorev9.createIndex('players-history-playerIdSsTimestamp', 'playerIdSsTimestamp', {unique: true});
const playersHistoryStorev9 =
transaction.objectStore("players-history");
playersHistoryStorev9.deleteIndex("players-history-timestamp");
playersHistoryStorev9.createIndex(
"players-history-playerIdSsTimestamp",
"playerIdSsTimestamp",
{ unique: true },
);
// NO break here
case newVersion >= 10 && oldVersion <= 9:
const songsBeatMapsStoreV10 = transaction.objectStore('songs-beatmaps');
songsBeatMapsStoreV10.deleteIndex('songs-beatmaps-key');
songsBeatMapsStoreV10.createIndex('songs-beatmaps-key', 'key', {unique: false});
const songsBeatMapsStoreV10 =
transaction.objectStore("songs-beatmaps");
songsBeatMapsStoreV10.deleteIndex("songs-beatmaps-key");
songsBeatMapsStoreV10.createIndex("songs-beatmaps-key", "key", {
unique: false,
});
// NO break here
case newVersion >= 11 && oldVersion <= 10:
db.createObjectStore('accsaber-categories', {
keyPath: 'name',
db.createObjectStore("accsaber-categories", {
keyPath: "name",
autoIncrement: false,
});
const accSaberPlayersStore = db.createObjectStore('accsaber-players', {
keyPath: 'id',
const accSaberPlayersStore = db.createObjectStore(
"accsaber-players",
{
keyPath: "id",
autoIncrement: false,
});
accSaberPlayersStore.createIndex('accsaber-players-playerId', 'playerId', {unique: false});
accSaberPlayersStore.createIndex('accsaber-players-category', 'category', {unique: false});
},
);
accSaberPlayersStore.createIndex(
"accsaber-players-playerId",
"playerId",
{ unique: false },
);
accSaberPlayersStore.createIndex(
"accsaber-players-category",
"category",
{ unique: false },
);
// NO break here
case newVersion >= 12 && oldVersion <= 11:
const accSaberPlayersHistoryStore = db.createObjectStore('accsaber-players-history', {
keyPath: 'playerIdTimestamp',
const accSaberPlayersHistoryStore = db.createObjectStore(
"accsaber-players-history",
{
keyPath: "playerIdTimestamp",
autoIncrement: false,
});
accSaberPlayersHistoryStore.createIndex('accsaber-players-history-playerId', 'playerId', {unique: false});
},
);
accSaberPlayersHistoryStore.createIndex(
"accsaber-players-history-playerId",
"playerId",
{ unique: false },
);
// NO break here
}
@ -198,28 +282,33 @@ async function openDatabase() {
},
blocked() {
console.warn('DB blocked')
console.warn("DB blocked");
},
blocking() {
// other tab tries to open newer db version - close connection
console.warn('DB blocking... will be closed')
console.warn("DB blocking... will be closed");
db.close();
eventBus.publish('dl-manager-pause-cmd');
eventBus.publish("dl-manager-pause-cmd");
// TODO: should be reopened with new version: event.newVersion
// TODO: or rather notify user / auto reload page
},
terminated() {
console.warn('DB terminated');
console.warn("DB terminated");
eventBus.publish('dl-manager-pause-cmd');
eventBus.publish("dl-manager-pause-cmd");
},
});
// Closure code should awaits DB operations ONLY or fail
// https://github.com/jakearchibald/idb#user-content-transaction-lifetime
db.runInTransaction = async (objectStores, closure, mode = 'readwrite', options = {durability: 'strict'}) => {
db.runInTransaction = async (
objectStores,
closure,
mode = "readwrite",
options = { durability: "strict" },
) => {
try {
const tx = db.transaction(objectStores, mode, options);
@ -231,12 +320,11 @@ async function openDatabase() {
} catch (e) {
throw e;
}
}
};
return db;
}
catch(e) {
log.error('Can not open DB.');
} catch (e) {
log.error("Can not open DB.");
throw e;
}

@ -1,51 +1,61 @@
import keyValueRepository from './repository/key-value';
import createBeatMapsService from '../services/beatmaps'
import log from '../utils/logger';
import {db} from './db'
import {isDateObject} from '../utils/js'
import twitchRepository from './repository/twitch'
import {correctOldSsDate} from '../utils/date'
import keyValueRepository from "./repository/key-value";
import createBeatMapsService from "../services/beatmaps";
import log from "../utils/logger";
import { db } from "./db";
import { isDateObject } from "../utils/js";
import twitchRepository from "./repository/twitch";
import { correctOldSsDate } from "../utils/date";
const FIXES_KEY = 'data-fix';
const FIXES_KEY = "data-fix";
const getAppliedFixes = async () => keyValueRepository().get(FIXES_KEY, true);
const setAppliedFixes = async fixes => keyValueRepository().set(fixes, FIXES_KEY);
const addAppliedFix = async fixName => {
const setAppliedFixes = async (fixes) =>
keyValueRepository().set(fixes, FIXES_KEY);
const addAppliedFix = async (fixName) => {
let allAppliedFixes = await getAppliedFixes();
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
allAppliedFixes =
allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
allAppliedFixes.push(fixName);
await setAppliedFixes(allAppliedFixes);
}
};
const allFixes = {
'rankeds-20210725': {
apply: async fixName => {
log.info('Apply rankeds refresh fix (20210725)')
"rankeds-20210725": {
apply: async (fixName) => {
log.info("Apply rankeds refresh fix (20210725)");
return db.runInTransaction(['rankeds-changes', 'rankeds', 'key-value'], async tx => {
await tx.objectStore('rankeds-changes').clear();
await tx.objectStore('rankeds').clear();
return db.runInTransaction(
["rankeds-changes", "rankeds", "key-value"],
async (tx) => {
await tx.objectStore("rankeds-changes").clear();
await tx.objectStore("rankeds").clear();
const keyValueStore = tx.objectStore('key-value')
const keyValueStore = tx.objectStore("key-value");
keyValueStore.delete('rankedsLastUpdated');
keyValueStore.delete("rankedsLastUpdated");
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
allAppliedFixes =
allAppliedFixes && Array.isArray(allAppliedFixes)
? allAppliedFixes
: [];
allAppliedFixes.push(fixName);
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
});
},
);
},
},
'beatsaver-20210804': {
apply: async fixName => {
log.info('Converting BeatSaver songs to a new format...', 'DBFix')
"beatsaver-20210804": {
apply: async (fixName) => {
log.info("Converting BeatSaver songs to a new format...", "DBFix");
return db.runInTransaction(['songs', 'songs-beatmaps', 'key-value'], async tx => {
const songsBeatMapsStore = tx.objectStore('songs-beatmaps');
return db.runInTransaction(
["songs", "songs-beatmaps", "key-value"],
async (tx) => {
const songsBeatMapsStore = tx.objectStore("songs-beatmaps");
let cursor = await tx.objectStore('songs').openCursor();
let cursor = await tx.objectStore("songs").openCursor();
let songCount = 0;
@ -55,90 +65,109 @@ const allFixes = {
const beatSaverSong = cursor.value;
if (beatSaverSong?.metadata?.characteristics) {
const beatMapsSong = beatmapsService.convertOldBeatSaverToBeatMaps(beatSaverSong);
const beatMapsSong =
beatmapsService.convertOldBeatSaverToBeatMaps(beatSaverSong);
if (beatMapsSong) {
songsBeatMapsStore.put(beatMapsSong)
songsBeatMapsStore.put(beatMapsSong);
songCount++;
} else {
log.info(`Unable to convert, deleting a song`, 'DBFix', beatSaverSong);
log.info(
`Unable to convert, deleting a song`,
"DBFix",
beatSaverSong,
);
}
} else {
log.info(`No metadata characteristics, skipping a song`, 'DBFix', beatSaverSong);
log.info(
`No metadata characteristics, skipping a song`,
"DBFix",
beatSaverSong,
);
}
cursor = await cursor.continue();
}
const keyValueStore = tx.objectStore('key-value')
const keyValueStore = tx.objectStore("key-value");
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
allAppliedFixes =
allAppliedFixes && Array.isArray(allAppliedFixes)
? allAppliedFixes
: [];
allAppliedFixes.push(fixName);
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
log.info(`${songCount} BeatSaver song(s) converted`, 'DBFix')
});
}
log.info(`${songCount} BeatSaver song(s) converted`, "DBFix");
},
);
},
},
'twitch-20210808': {
apply: async fixName => {
"twitch-20210808": {
apply: async (fixName) => {
const predefinedProfiles = {
'76561198059659922': 'patian25',
'1994101560659098': 'xoxobluff',
'76561198138327464': 'altrowilddog',
'76561198855288628': 'inbourne',
'76561198136177445': 'riviengt',
'76561199004224834': 'nyaanos',
'76561198023909718': 'danielduel',
'76561198212019365': 'fnyt',
'76561197966674102': 'maciekvr',
'76561198025451538': 'drakonno',
'76561197994110158': 'sanorek',
'76561198034203862': 'vr_agent',
'3702342373170767': 'xjedam',
'76561197995161445': 'mediekore',
'76561198087710981': 'shreddyfreddy',
'76561198999385463': 'woltixo',
'76561198035381239': 'motzel',
'76561198178407566' : 'acetari',
'76561198045386379': 'duhhello',
'76561198835772160': 'tornadoef6',
'76561198187936410': 'garsh_',
'76561198362923485': 'tseska_',
'76561198154190170': 'tieeli',
'76561198333869741': 'cerret07',
'76561197995162898': 'electrostats',
'76561198166289091': 'rocker1904',
'2538637699496776': 'astrella_',
'76561198171842815': 'coolpickb',
'76561198145281261': 'harbgy'
}
"76561198059659922": "patian25",
1994101560659098: "xoxobluff",
"76561198138327464": "altrowilddog",
"76561198855288628": "inbourne",
"76561198136177445": "riviengt",
"76561199004224834": "nyaanos",
"76561198023909718": "danielduel",
"76561198212019365": "fnyt",
"76561197966674102": "maciekvr",
"76561198025451538": "drakonno",
"76561197994110158": "sanorek",
"76561198034203862": "vr_agent",
3702342373170767: "xjedam",
"76561197995161445": "mediekore",
"76561198087710981": "shreddyfreddy",
"76561198999385463": "woltixo",
"76561198035381239": "motzel",
"76561198178407566": "acetari",
"76561198045386379": "duhhello",
76561198835772160: "tornadoef6",
"76561198187936410": "garsh_",
"76561198362923485": "tseska_",
"76561198154190170": "tieeli",
"76561198333869741": "cerret07",
"76561197995162898": "electrostats",
"76561198166289091": "rocker1904",
2538637699496776: "astrella_",
"76561198171842815": "coolpickb",
"76561198145281261": "harbgy",
};
log.info('Adding predefined Twitch profiles...', 'DBFix')
log.info("Adding predefined Twitch profiles...", "DBFix");
const updatePlayerTwitchProfile = async (twitchProfile) => twitchRepository().set(twitchProfile);
const updatePlayerTwitchProfile = async (twitchProfile) =>
twitchRepository().set(twitchProfile);
await Promise.all(Object.entries(predefinedProfiles).map(async ([playerId, twitchLogin]) => updatePlayerTwitchProfile(
{
await Promise.all(
Object.entries(predefinedProfiles).map(
async ([playerId, twitchLogin]) =>
updatePlayerTwitchProfile({
lastUpdated: null,
login: twitchLogin,
playerId
}
)))
playerId,
}),
),
);
await addAppliedFix(fixName);
log.info('Twitch profiles added.', 'DBFix')
}
log.info("Twitch profiles added.", "DBFix");
},
},
'player-history-20211022': {
apply: async fixName => {
log.info('Apply player ss history fix (20211022)')
"player-history-20211022": {
apply: async (fixName) => {
log.info("Apply player ss history fix (20211022)");
return db.runInTransaction(['players-history', 'key-value'], async tx => {
const playersHistoryStore = tx.objectStore('players-history');
return db.runInTransaction(
["players-history", "key-value"],
async (tx) => {
const playersHistoryStore = tx.objectStore("players-history");
let cursor = await playersHistoryStore.openCursor();
@ -157,33 +186,46 @@ const allFixes = {
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
await cursor.delete();
playersHistoryStore.put({...history, ssDate, playerIdSsTimestamp});
playersHistoryStore.put({
...history,
ssDate,
playerIdSsTimestamp,
});
cursor = await cursor.continue();
}
const keyValueStore = tx.objectStore('key-value')
const keyValueStore = tx.objectStore("key-value");
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
allAppliedFixes =
allAppliedFixes && Array.isArray(allAppliedFixes)
? allAppliedFixes
: [];
allAppliedFixes.push(fixName);
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
});
},
);
},
},
};
export default async () => {
let appliedDbFixes = await getAppliedFixes();
const appliedFixes = appliedDbFixes && Array.isArray(appliedDbFixes) ? appliedDbFixes : [];
const neededFixes = Object.keys(allFixes).filter(f => !appliedFixes.includes(f) && (!allFixes[f].validTo || allFixes[f].validTo > new Date()));
const appliedFixes =
appliedDbFixes && Array.isArray(appliedDbFixes) ? appliedDbFixes : [];
const neededFixes = Object.keys(allFixes).filter(
(f) =>
!appliedFixes.includes(f) &&
(!allFixes[f].validTo || allFixes[f].validTo > new Date()),
);
if (!neededFixes.length) return;
document.body.innerHTML = '<p>Database conversion. Please wait...</p>';
document.body.innerHTML = "<p>Database conversion. Please wait...</p>";
for (let key of neededFixes) {
await allFixes[key].apply(key);
}
document.body.innerHTML = '';
}
document.body.innerHTML = "";
};

@ -1,19 +1,29 @@
import cacheRepository from './repository/cache';
import groupsRepository from './repository/groups';
import keyValueRepository from './repository/key-value';
import playersRepository from './repository/players';
import playersHistoryRepository from './repository/players-history';
import rankedsRepository from './repository/rankeds';
import rankedsChangesRepository from './repository/rankeds-changes';
import scoresRepository from './repository/scores';
import songsRepository from './repository/songs';
import twitchRepository from './repository/twitch';
import log from '../utils/logger';
import cacheRepository from "./repository/cache";
import groupsRepository from "./repository/groups";
import keyValueRepository from "./repository/key-value";
import playersRepository from "./repository/players";
import playersHistoryRepository from "./repository/players-history";
import rankedsRepository from "./repository/rankeds";
import rankedsChangesRepository from "./repository/rankeds-changes";
import scoresRepository from "./repository/scores";
import songsRepository from "./repository/songs";
import twitchRepository from "./repository/twitch";
import log from "../utils/logger";
export default () => {
log.debug('Initialize DB repositories');
log.debug("Initialize DB repositories");
// initialize all repositories in order to create cache to sync
[cacheRepository, groupsRepository, keyValueRepository, playersRepository, playersHistoryRepository, rankedsRepository, rankedsChangesRepository, scoresRepository, songsRepository, twitchRepository].map(repository => repository());
}
[
cacheRepository,
groupsRepository,
keyValueRepository,
playersRepository,
playersHistoryRepository,
rankedsRepository,
rankedsChangesRepository,
scoresRepository,
songsRepository,
twitchRepository,
].map((repository) => repository());
};

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('accsaber-categories', 'name');
export default () => createRepository("accsaber-categories", "name");

@ -1,6 +1,7 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('accsaber-players-history', 'playerIdTimestamp', {
'accsaber-players-history-playerId': 'playerId',
'accsaber-players-history-playerIdTimestamp': 'playerIdTimestamp'
export default () =>
createRepository("accsaber-players-history", "playerIdTimestamp", {
"accsaber-players-history-playerId": "playerId",
"accsaber-players-history-playerIdTimestamp": "playerIdTimestamp",
});

@ -1,10 +1,7 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository(
'accsaber-players',
'id',
{
'accsaber-players-playerId': 'playerId',
'accsaber-players-category': 'category',
},
);
export default () =>
createRepository("accsaber-players", "id", {
"accsaber-players-playerId": "playerId",
"accsaber-players-category": "category",
});

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('beat-savior-files', 'fileId');
export default () => createRepository("beat-savior-files", "fileId");

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('beat-savior-players', 'playerId');
export default () => createRepository("beat-savior-players", "playerId");

@ -1,6 +1,7 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('beat-savior', 'beatSaviorId', {
'beat-savior-playerId': 'playerId',
'beat-savior-hash': 'hash',
export default () =>
createRepository("beat-savior", "beatSaviorId", {
"beat-savior-playerId": "playerId",
"beat-savior-hash": "hash",
});

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('cache');
export default () => createRepository("cache");

@ -1,11 +1,11 @@
import cache from '../cache';
import {db} from '../db';
import {convertArrayToObjectByKey} from '../../utils/js'
import makePendingPromisePool from '../../utils/pending-promises'
import eventBus from '../../utils/broadcast-channel-pubsub'
import cache from "../cache";
import { db } from "../db";
import { convertArrayToObjectByKey } from "../../utils/js";
import makePendingPromisePool from "../../utils/pending-promises";
import eventBus from "../../utils/broadcast-channel-pubsub";
export const ALL_KEY = '__ALL';
const NONE_KEY = '__NONE';
export const ALL_KEY = "__ALL";
const NONE_KEY = "__NONE";
let repositories = {};
@ -20,46 +20,52 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
const getKeyName = () => inlineKeyName;
const hasOutOfLineKey = () => getKeyName() === undefined;
const getObjKey = (obj, outOfLineKey = undefined) => {
const key = hasOutOfLineKey() ? outOfLineKey : obj[inlineKeyName]
const key = hasOutOfLineKey() ? outOfLineKey : obj[inlineKeyName];
return key ? key : outOfLineKey;
}
};
let repositoryCache = cache(repositoryName, getObjKey);
const getCacheKeyFor = (query, indexName) => (indexName ? indexName : ALL_KEY) + '-' + (query ? query : NONE_KEY);
const getCacheKeyFor = (query, indexName) =>
(indexName ? indexName : ALL_KEY) + "-" + (query ? query : NONE_KEY);
const getFieldForIndexName = indexName => indexesKeyNames[indexName];
const isFieldForIndexDefined = indexName => !!getFieldForIndexName(indexName);
const getFieldForIndexName = (indexName) => indexesKeyNames[indexName];
const isFieldForIndexDefined = (indexName) =>
!!getFieldForIndexName(indexName);
const setDataAvailabilityStatus = cacheKey => dataAvailableFor[cacheKey] = true;
const setAllDataAvailabilityStatus = () => setDataAvailabilityStatus(getCacheKeyFor());
const removeDataAvailabilityStatus = cacheKey => {
const setDataAvailabilityStatus = (cacheKey) =>
(dataAvailableFor[cacheKey] = true);
const setAllDataAvailabilityStatus = () =>
setDataAvailabilityStatus(getCacheKeyFor());
const removeDataAvailabilityStatus = (cacheKey) => {
delete dataAvailableFor[cacheKey];
delete dataAvailableFor[getCacheKeyFor()];
}
const flushDataAvailabilityStatus = () => dataAvailableFor = {};
const isIndexDataAvailable = cacheKey => !!dataAvailableFor[cacheKey];
};
const flushDataAvailabilityStatus = () => (dataAvailableFor = {});
const isIndexDataAvailable = (cacheKey) => !!dataAvailableFor[cacheKey];
const isAllDataAvailable = () => isIndexDataAvailable(getCacheKeyFor());
const flushCache = () => {
repositoryCache.flush();
flushDataAvailabilityStatus();
}
};
const forgetCacheKey = key => repositoryCache.forget(key);
const forgetCacheKey = (key) => repositoryCache.forget(key);
const forgetObject = async obj => {
if (hasOutOfLineKey()) throw 'forgetObject function is not available in repositories with out-of-line keys';
const forgetObject = async (obj) => {
if (hasOutOfLineKey())
throw "forgetObject function is not available in repositories with out-of-line keys";
const key = getObjKey(obj);
if (!key) throw `Object does not contain ${inlineKeyName} field which is repository key`;
if (!key)
throw `Object does not contain ${inlineKeyName} field which is repository key`;
forgetCacheKey(key);
}
};
const getStoreName = () => storeName;
const getCachedKeys = _ => repositoryCache.getKeys();
const getCachedKeys = (_) => repositoryCache.getKeys();
const getAllKeys = async () => db.getAllKeys(storeName);
@ -68,16 +74,23 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
const cacheKey = getCacheKeyFor(key);
return repositoryCache.get(key, () => resolvePromiseOrWaitForPending(cacheKey, () => db.get(storeName, key)));
return repositoryCache.get(key, () =>
resolvePromiseOrWaitForPending(cacheKey, () => db.get(storeName, key)),
);
};
const getFromIndex = async (indexName, query, refreshCache = false) => {
if (hasOutOfLineKey()) throw `getFromIndex() is not available for stores with out-of-line key`;
if (!isFieldForIndexDefined(indexName)) throw `Index ${indexName} has no field set`;
if (hasOutOfLineKey())
throw `getFromIndex() is not available for stores with out-of-line key`;
if (!isFieldForIndexDefined(indexName))
throw `Index ${indexName} has no field set`;
const cacheKey = getCacheKeyFor(query, indexName + '-single');
const cacheKey = getCacheKeyFor(query, indexName + "-single");
const getFromDb = () => resolvePromiseOrWaitForPending(cacheKey, () => db.getFromIndex(storeName, indexName, query));
const getFromDb = () =>
resolvePromiseOrWaitForPending(cacheKey, () =>
db.getFromIndex(storeName, indexName, query),
);
if (query && query instanceof IDBKeyRange) return getFromDb();
@ -85,7 +98,8 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
const fullIndexCacheKey = getCacheKeyFor(query, indexName);
const filterItems = item => item !== undefined && (!query || item[field] === query);
const filterItems = (item) =>
item !== undefined && (!query || item[field] === query);
if (refreshCache) {
removeDataAvailabilityStatus(cacheKey);
@ -94,24 +108,34 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
repositoryCache.forgetByFilter(filterItems);
}
return repositoryCache.getByFilter(getFromDb, isAllDataAvailable() || isIndexDataAvailable(cacheKey) || isIndexDataAvailable(fullIndexCacheKey) ? filterItems : null);
return repositoryCache.getByFilter(
getFromDb,
isAllDataAvailable() ||
isIndexDataAvailable(cacheKey) ||
isIndexDataAvailable(fullIndexCacheKey)
? filterItems
: null,
);
};
const getAll = async (refreshCache = false) => {
const cacheKey = getCacheKeyFor();
const getFromDb = () => resolvePromiseOrWaitForPending(cacheKey, () => db.getAll(storeName))
const getFromDb = () =>
resolvePromiseOrWaitForPending(cacheKey, () => db.getAll(storeName));
if (hasOutOfLineKey()) return getFromDb();
if (refreshCache) flushCache();
const filterUndefined = item => item !== undefined;
const filterUndefined = (item) => item !== undefined;
if (!isAllDataAvailable()) {
const data = convertArrayToObjectByKey(await getFromDb(), inlineKeyName);
const ret = Object.values(repositoryCache.setAll(data)).filter(filterUndefined);
const ret = Object.values(repositoryCache.setAll(data)).filter(
filterUndefined,
);
setAllDataAvailabilityStatus();
@ -119,28 +143,39 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
}
return Object.values(repositoryCache.getAll()).filter(filterUndefined);
}
};
const getAllFromIndex = async(indexName, query = undefined, refreshCache = false) => {
if (hasOutOfLineKey()) throw `getAllFromIndex() is not available for stores with out-of-line key`;
if (!isFieldForIndexDefined(indexName)) throw `Index ${indexName} has no field set`;
const getAllFromIndex = async (
indexName,
query = undefined,
refreshCache = false,
) => {
if (hasOutOfLineKey())
throw `getAllFromIndex() is not available for stores with out-of-line key`;
if (!isFieldForIndexDefined(indexName))
throw `Index ${indexName} has no field set`;
const cacheKey = getCacheKeyFor(query, indexName);
const getFromDb = async () => resolvePromiseOrWaitForPending(cacheKey, () => db.getAllFromIndex(storeName, indexName, query));
const getFromDb = async () =>
resolvePromiseOrWaitForPending(cacheKey, () =>
db.getAllFromIndex(storeName, indexName, query),
);
if (query && query instanceof IDBKeyRange) return getFromDb();
const field = getFieldForIndexName(indexName);
const filterItems = item => item !== undefined && (!query || item[field] === query);
const filterItems = (item) =>
item !== undefined && (!query || item[field] === query);
if (refreshCache) {
removeDataAvailabilityStatus(cacheKey);
repositoryCache.forgetByFilter(filterItems);
}
const getFromDbAndUpdateCache = async () => resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => {
const getFromDbAndUpdateCache = async () =>
resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => {
const data = await getFromDb();
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
@ -148,63 +183,71 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
setDataAvailabilityStatus(cacheKey);
return data;
})
});
if (!isAllDataAvailable() && !isIndexDataAvailable(cacheKey)) return await getFromDbAndUpdateCache();
if (!isAllDataAvailable() && !isIndexDataAvailable(cacheKey))
return await getFromDbAndUpdateCache();
return Object.values(repositoryCache.getAll()).filter(filterItems);
}
};
const set = async (value, key = undefined, tx = null) => {
const txStores = tx ? [...tx.objectStoreNames] : null;
let putKey;
if (tx && txStores.includes(storeName)) {
putKey = await tx.objectStore(storeName).put(value, inlineKeyName ? undefined : key);
putKey = await tx
.objectStore(storeName)
.put(value, inlineKeyName ? undefined : key);
} else {
putKey = await db.put(storeName, value, inlineKeyName ? undefined : key)
putKey = await db.put(storeName, value, inlineKeyName ? undefined : key);
}
if (!hasOutOfLineKey() && !getObjKey(value)) value[inlineKeyName] = putKey;
return repositoryCache.set(getObjKey(value, key), value);
}
};
const del = async key => {
const del = async (key) => {
await db.delete(storeName, key);
return repositoryCache.forget(key);
}
};
const deleteObject = async obj => {
if (hasOutOfLineKey()) throw 'deleteObject function is not available in repositories with out-of-line keys';
const deleteObject = async (obj) => {
if (hasOutOfLineKey())
throw "deleteObject function is not available in repositories with out-of-line keys";
const key = getObjKey(obj);
if (!key) throw `Object does not contain ${inlineKeyName} field which is repository key`;
if (!key)
throw `Object does not contain ${inlineKeyName} field which is repository key`;
return del(key);
}
};
const openCursor = async (mode = 'readonly') => db.transaction(storeName, mode).store.openCursor();
const openCursor = async (mode = "readonly") =>
db.transaction(storeName, mode).store.openCursor();
const setCache = (value, key) => {
if (hasOutOfLineKey()) {
if (!key) throw `setCache() needs a key for stores (${storeName}) with out-of-line keys`;
if (!key)
throw `setCache() needs a key for stores (${storeName}) with out-of-line keys`;
} else {
key = getObjKey(value, key);
}
repositoryCache.set(key, value);
}
const addToCache = data => {
if (hasOutOfLineKey()) throw `addToCache() is not available for stores (${storeName}) with out-of-line key`;
};
const addToCache = (data) => {
if (hasOutOfLineKey())
throw `addToCache() is not available for stores (${storeName}) with out-of-line key`;
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
}
};
const getCache = () => repositoryCache;
return repositories[repositoryName] = {
return (repositories[repositoryName] = {
getStoreName,
hasOutOfLineKey,
getAllKeys,
@ -224,5 +267,5 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
setCache,
addToCache,
getCache,
};
});
};

@ -1,5 +1,11 @@
import createRepository from './generic';
import createRepository from "./generic";
let repository;
export default () => repository ? repository : repository = createRepository('groups', '_idbId', {'groups-name': 'name', 'groups-playerId': 'playerId'});
export default () =>
repository
? repository
: (repository = createRepository("groups", "_idbId", {
"groups-name": "name",
"groups-playerId": "playerId",
}));

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('key-value');
export default () => createRepository("key-value");

@ -1,6 +1,7 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('players-history', '_idbId', {
'players-history-playerId': 'playerId',
'players-history-playerIdSsTimestamp': 'playerIdSsTimestamp'
export default () =>
createRepository("players-history", "_idbId", {
"players-history-playerId": "playerId",
"players-history-playerIdSsTimestamp": "playerIdSsTimestamp",
});

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('players', 'playerId');
export default () => createRepository("players", "playerId");

@ -1,3 +1,7 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('rankeds-changes', '_idbId', {'rankeds-changes-timestamp': 'timestamp', 'rankeds-changes-leaderboardId': 'leaderboardId'});
export default () =>
createRepository("rankeds-changes", "_idbId", {
"rankeds-changes-timestamp": "timestamp",
"rankeds-changes-leaderboardId": "leaderboardId",
});

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('rankeds', 'leaderboardId');
export default () => createRepository("rankeds", "leaderboardId");

@ -1,9 +1,6 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository(
'scores-update-queue',
'id',
{
'scores-update-queue-fetchedAt': 'fetchedAt',
},
)
export default () =>
createRepository("scores-update-queue", "id", {
"scores-update-queue-fetchedAt": "fetchedAt",
});

@ -1,12 +1,9 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository(
'scores',
'id',
{
'scores-timeset': 'timeset',
'scores-leaderboardId': 'leaderboardId',
'scores-playerId': 'playerId',
'scores-pp': 'pp',
},
)
export default () =>
createRepository("scores", "id", {
"scores-timeset": "timeset",
"scores-leaderboardId": "leaderboardId",
"scores-playerId": "playerId",
"scores-pp": "pp",
});

@ -1,3 +1,4 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('songs-beatmaps', 'hash', {'songs-beatmaps-key': 'key'});
export default () =>
createRepository("songs-beatmaps", "hash", { "songs-beatmaps-key": "key" });

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('songs', 'hash', {'songs-key': 'key'});
export default () => createRepository("songs", "hash", { "songs-key": "key" });

@ -1,3 +1,3 @@
import createRepository from './generic';
import createRepository from "./generic";
export default () => createRepository('twitch', 'playerId');
export default () => createRepository("twitch", "playerId");

@ -1,18 +1,18 @@
import App from './App.svelte';
import log from './utils/logger'
import initDb from './db/db'
import initializeRepositories from './db/repositories-init';
import setupDataFixes from './db/fix-data'
import createConfigStore from './stores/config'
import createPlayerService from './services/scoresaber/player'
import createBeatSaviorService from './services/beatsavior'
import createRankedsStore from './stores/scoresaber/rankeds'
import initDownloadManager from './network/download-manager'
import initCommandProcessor from './network/command-processor'
import {enablePatches, setAutoFreeze} from 'immer'
import {initCompareEnhancer} from './stores/http/enhancers/scores/compare'
import ErrorComponent from './components/Common/Error.svelte'
import initializeWorkers from './utils/worker-wrappers'
import App from "./App.svelte";
import log from "./utils/logger";
import initDb from "./db/db";
import initializeRepositories from "./db/repositories-init";
import setupDataFixes from "./db/fix-data";
import createConfigStore from "./stores/config";
import createPlayerService from "./services/scoresaber/player";
import createBeatSaviorService from "./services/beatsavior";
import createRankedsStore from "./stores/scoresaber/rankeds";
import initDownloadManager from "./network/download-manager";
import initCommandProcessor from "./network/command-processor";
import { enablePatches, setAutoFreeze } from "immer";
import { initCompareEnhancer } from "./stores/http/enhancers/scores/compare";
import ErrorComponent from "./components/Common/Error.svelte";
import initializeWorkers from "./utils/worker-wrappers";
let app = null;
@ -22,7 +22,7 @@ let app = null;
// log.setLevel(log.TRACE);
// log.logOnly(['AccSaberService']);
log.info('Starting up...', 'Main')
log.info("Starting up...", "Main");
await initDb();
await initializeRepositories();
@ -47,7 +47,7 @@ let app = null;
initCommandProcessor(await initDownloadManager());
log.info('Site initialized', 'Main')
log.info("Site initialized", "Main");
app = new App({
target: document.body,
@ -56,8 +56,14 @@ let app = null;
} catch (error) {
console.error(error);
if (error instanceof DOMException && error.toString() === 'InvalidStateError: A mutation operation was attempted on a database that did not allow mutations.')
error = new Error('Firefox in private mode does not support the database. Please run the site in normal mode.')
if (
error instanceof DOMException &&
error.toString() ===
"InvalidStateError: A mutation operation was attempted on a database that did not allow mutations."
)
error = new Error(
"Firefox in private mode does not support the database. Please run the site in normal mode.",
);
app = new ErrorComponent({
target: document.body,
@ -66,5 +72,4 @@ let app = null;
}
})();
export default app;

@ -1,5 +1,5 @@
// import eventBus from '../utils/broadcast-channel-pubsub'
import {addToDate, MINUTE} from '../utils/date'
import { addToDate, MINUTE } from "../utils/date";
const DEFAULT_CACHE_SIZE = 100;
@ -7,20 +7,25 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
let cache = {};
let cacheSize = size;
const isWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope
const isWorker =
typeof WorkerGlobalScope !== "undefined" &&
self instanceof WorkerGlobalScope;
const defaultExpiryIn = expiryIn;
const packValue = value => {
if (!value || typeof value !== 'object') return value;
const packValue = (value) => {
if (!value || typeof value !== "object") return value;
const newValue = { ...value };
if (value.headers && value.headers instanceof Headers) {
newValue.headers = [...value.headers.entries()].reduce((cum, [key, value]) => {
newValue.headers = [...value.headers.entries()].reduce(
(cum, [key, value]) => {
cum[key] = value;
return cum;
}, {})
},
{},
);
}
if (value.body && value.body instanceof Document) {
@ -28,25 +33,29 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
}
return newValue;
}
};
const unpackValue = value => {
if (!value || typeof value !== 'object') return value;
const unpackValue = (value) => {
if (!value || typeof value !== "object") return value;
const newValue = { ...value };
if (value.headers) {
const headers = new Headers();
Object.keys(value.headers).map(k => headers.append(k, value.headers[k]));
Object.keys(value.headers).map((k) =>
headers.append(k, value.headers[k]),
);
newValue.headers = headers;
}
if (value.body) {
newValue.body = !isWorker ? new DOMParser().parseFromString(value.body, 'text/html') : value.body;
newValue.body = !isWorker
? new DOMParser().parseFromString(value.body, "text/html")
: value.body;
}
return newValue;
}
};
// update data cached on another node
// const setUnsubscribe = eventBus.on('net-cache-key-set', ({key, value, expiryIn}, isLocal) => !isLocal ? set(key, unpackValue(value), expiryIn, false) : null);
@ -54,14 +63,25 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
// const flushUnsubscribe = eventBus.on('net-cache-flush', (_, isLocal) => !isLocal ? flush(false) : null);
const has = (key, maxAge = null, withExpired = false) =>
cache.hasOwnProperty(key) && cache[key] &&
(withExpired || !cache[key].expiryAt || cache[key].expiryAt >= new Date()) &&
(!Number.isFinite(maxAge) || !cache[key].cachedAt || addToDate(maxAge, cache[key].cachedAt) >= new Date());
cache.hasOwnProperty(key) &&
cache[key] &&
(withExpired ||
!cache[key].expiryAt ||
cache[key].expiryAt >= new Date()) &&
(!Number.isFinite(maxAge) ||
!cache[key].cachedAt ||
addToDate(maxAge, cache[key].cachedAt) >= new Date());
const set = (key, value, expiryIn = null, emitEvent = true) => {
expiryIn = expiryIn ? expiryIn : defaultExpiryIn;
cache[key] = {key, cachedAt: new Date(), expiryIn, expiryAt: addToDate(expiryIn, new Date()), value};
cache[key] = {
key,
cachedAt: new Date(),
expiryIn,
expiryAt: addToDate(expiryIn, new Date()),
value,
};
// if (emitEvent) eventBus.publish('net-cache-key-set', {key, value: packValue(value), expiryIn});
@ -70,7 +90,12 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
return value;
};
const get = (key, maxAge = null, withExpired = false, valueOnly = true) => has(key, maxAge, withExpired) ? (valueOnly ? cache[key].value : cache[key]) : undefined;
const get = (key, maxAge = null, withExpired = false, valueOnly = true) =>
has(key, maxAge, withExpired)
? valueOnly
? cache[key].value
: cache[key]
: undefined;
const getAll = () => cache;
@ -82,7 +107,7 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
// if (emitEvent) eventBus.publish('net-cache-key-forget', {key});
return cache;
}
};
const flush = (emitEvent = true) => {
cache = {};
@ -90,7 +115,7 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
// if (emitEvent) eventBus.publish('net-cache-flush', {});
return cache;
}
};
const garbageCollect = (size = cacheSize) => {
const values = Object.values(cache);
@ -99,14 +124,17 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
cache = values
.sort((a, b) => b.expiryAt - a.expiryAt)
.slice(0, size)
.reduce((cum, item) => {cum[item.key] = item; return cum;}, {});
}
.reduce((cum, item) => {
cum[item.key] = item;
return cum;
}, {});
};
const destroy = () => {
// setUnsubscribe();
// forgetUnsubscribe();
// flushUnsubscribe();
}
};
return {
has,
@ -117,5 +145,5 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
forget,
flush,
destroy,
}
}
};
};

@ -1,18 +1,21 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import queue from "../../queues/queues";
import createClient from "../generic";
const process = response => {
const process = (response) => {
if (!response || !Array.isArray(response)) return [];
return response.map(c => ({
return response.map((c) => ({
name: c.categoryName,
displayName: c.categoryDisplayName,
countsTowardsOverall: c.countsTowardsOverall,
description: c.description
description: c.description,
}));
}
};
const get = async ({priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.ACCSABER.categories(priority, queueOptions);
const get = async ({
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.ACCSABER.categories(priority, queueOptions);
const client = createClient(get, process);

@ -1,10 +1,16 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import {dateFromString, formatDateRelative} from '../../../utils/date'
import {LEADERBOARD_SCORES_PER_PAGE} from '../../../utils/accsaber/consts'
import queue from "../../queues/queues";
import createClient from "../generic";
import { dateFromString, formatDateRelative } from "../../../utils/date";
import { LEADERBOARD_SCORES_PER_PAGE } from "../../../utils/accsaber/consts";
const process = response => {
if (!response || !Array.isArray(response.responses) || response.responses.length !== 2 || !Array.isArray(response.responses[0])) return [];
const process = (response) => {
if (
!response ||
!Array.isArray(response.responses) ||
response.responses.length !== 2 ||
!Array.isArray(response.responses[0])
)
return [];
const page = response?.fetchOptions.page ?? 1;
const totalItems = response.responses[0].length;
@ -25,16 +31,32 @@ const process = response => {
difficulty,
} = mapInfo;
const song = {hash, name, subName, authorName, levelAuthorName, beatsaverKey};
const diffInfo = {type: 'Standard', diff: difficulty?.toLowerCase()?.replace('plus', 'Plus')}
const leaderboard = {leaderboardId, song, diffInfo, complexity, categoryDisplayName};
const song = {
hash,
name,
subName,
authorName,
levelAuthorName,
beatsaverKey,
};
const diffInfo = {
type: "Standard",
diff: difficulty?.toLowerCase()?.replace("plus", "Plus"),
};
const leaderboard = {
leaderboardId,
song,
diffInfo,
complexity,
categoryDisplayName,
};
return {
page,
pageQty,
totalItems,
leaderboard,
scores: response.responses[0].map(s => {
scores: response.responses[0].map((s) => {
let {
accuracy: acc,
ap,
@ -48,14 +70,16 @@ const process = response => {
if (acc && Number.isFinite(acc)) acc *= 100;
timeSet = dateFromString(timeSet)
timeSet = dateFromString(timeSet);
const timeSetString = formatDateRelative(timeSet);
return {
player: {
name,
playerId,
playerInfo: {avatar: `https://cdn.accsaber.com/avatars/${playerId}.jpg`},
playerInfo: {
avatar: `https://cdn.accsaber.com/avatars/${playerId}.jpg`,
},
},
score: {
acc,
@ -67,19 +91,30 @@ const process = response => {
timeSetString,
},
other: rest,
}
};
}),
};
}
};
const get = async ({leaderboardId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
const get = async ({
leaderboardId,
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => {
const responses = await Promise.all([
queue.ACCSABER.leaderboard(leaderboardId, page, priority, queueOptions),
queue.ACCSABER.leaderboardInfo(leaderboardId, priority, queueOptions)
queue.ACCSABER.leaderboardInfo(leaderboardId, priority, queueOptions),
]);
return {...responses[0], body: {responses: responses.map(r => r.body), fetchOptions: {leaderboardId, page}}}
}
return {
...responses[0],
body: {
responses: responses.map((r) => r.body),
fetchOptions: { leaderboardId, page },
},
};
};
const client = createClient(get, process);

@ -1,28 +1,43 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import {fromAccSaberDateString} from '../../../utils/date'
import {isDateObject} from '../../../utils/js'
import queue from "../../queues/queues";
import createClient from "../generic";
import { fromAccSaberDateString } from "../../../utils/date";
import { isDateObject } from "../../../utils/js";
const process = response => {
const process = (response) => {
const playerId = response?.fetchOptions?.playerId ?? null;
if (!response?.response || !Object.keys(response.response)?.length || !playerId) return [];
if (
!response?.response ||
!Object.keys(response.response)?.length ||
!playerId
)
return [];
return {
playerId,
history: Object.entries(response.response)
.map(([date, rank]) => ({ date: fromAccSaberDateString(date), rank }))
.filter(obj => isDateObject(obj?.date))
.sort((a,b) => a.date.getTime() - b.date.getTime())
,
}
}
.filter((obj) => isDateObject(obj?.date))
.sort((a, b) => a.date.getTime() - b.date.getTime()),
};
};
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
const response = await queue.ACCSABER.playerRankHistory(playerId, priority, queueOptions);
const get = async ({
playerId,
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => {
const response = await queue.ACCSABER.playerRankHistory(
playerId,
priority,
queueOptions,
);
return {...response, body: {response: response.body, fetchOptions: {playerId}}}
}
return {
...response,
body: { response: response.body, fetchOptions: { playerId } },
};
};
const client = createClient(get, process);

@ -1,23 +1,36 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import queue from "../../queues/queues";
import createClient from "../generic";
const process = response => {
const category = response?.fetchOptions?.category ?? 'overall';
const process = (response) => {
const category = response?.fetchOptions?.category ?? "overall";
if (!response?.response || !Array.isArray(response.response)) return [];
return response.response.map(p => ({
return response.response.map((p) => ({
...p,
id: `${p.playerId}-${category}`,
category,
lastUpdated: new Date(),
}));
}
};
const get = async ({category = 'overall', page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
const response = await queue.ACCSABER.ranking(category, page, priority, queueOptions);
const get = async ({
category = "overall",
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => {
const response = await queue.ACCSABER.ranking(
category,
page,
priority,
queueOptions,
);
return {...response, body: {response: response.body, fetchOptions: {category}}}
}
return {
...response,
body: { response: response.body, fetchOptions: { category } },
};
};
const client = createClient(get, process);

@ -1,12 +1,13 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import {dateFromString} from '../../../utils/date'
import queue from "../../queues/queues";
import createClient from "../generic";
import { dateFromString } from "../../../utils/date";
const process = response => {
const process = (response) => {
const playerId = response?.fetchOptions?.playerId ?? null;
if (!response?.response || !Array.isArray(response.response) || !playerId) return [];
if (!response?.response || !Array.isArray(response.response) || !playerId)
return [];
return response.response.map(s => {
return response.response.map((s) => {
let {
songHash: hash,
songName: name,
@ -28,11 +29,27 @@ const process = response => {
leaderboardId = parseInt(leaderboardId, 10);
if (isNaN(leaderboardId)) leaderboardId = null;
const song = {hash, name, subName: '', authorName, levelAuthorName, beatsaverKey};
const diffInfo = {type: 'Standard', diff: difficulty?.toLowerCase()?.replace('plus', 'Plus')}
const leaderboard = {leaderboardId, song, diffInfo, complexity, categoryDisplayName};
const song = {
hash,
name,
subName: "",
authorName,
levelAuthorName,
beatsaverKey,
};
const diffInfo = {
type: "Standard",
diff: difficulty?.toLowerCase()?.replace("plus", "Plus"),
};
const leaderboard = {
leaderboardId,
song,
diffInfo,
complexity,
categoryDisplayName,
};
const timeSet = dateFromString(s.timeSet)
const timeSet = dateFromString(s.timeSet);
return {
id: `${playerId}-${s.leaderboardId}`,
playerId,
@ -41,18 +58,41 @@ const process = response => {
ap,
acc,
leaderboard,
score: {...originalScore, ap, unmodifiedScore: score, score, mods: null, timeSet, acc, percentage: acc, weightedAp},
score: {
...originalScore,
ap,
unmodifiedScore: score,
score,
mods: null,
timeSet,
acc,
percentage: acc,
weightedAp,
},
fetchedAt: new Date(),
lastUpdated: new Date(),
}
};
});
}
};
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
const response = await queue.ACCSABER.scores(playerId, page, priority, queueOptions);
const get = async ({
playerId,
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => {
const response = await queue.ACCSABER.scores(
playerId,
page,
priority,
queueOptions,
);
return {...response, body: {response: response.body, fetchOptions: {playerId, page}}}
}
return {
...response,
body: { response: response.body, fetchOptions: { playerId, page } },
};
};
const client = createClient(get, process);

@ -1,8 +1,12 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import process from './utils/process'
import queue from "../../queues/queues";
import createClient from "../generic";
import process from "./utils/process";
const get = async ({hash, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATMAPS.byHash(hash, priority, queueOptions);
const get = async ({
hash,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.BEATMAPS.byHash(hash, priority, queueOptions);
const client = createClient(get, process);

@ -1,8 +1,12 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import process from './utils/process'
import queue from "../../queues/queues";
import createClient from "../generic";
import process from "./utils/process";
const get = async ({key, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATMAPS.byKey(key, priority, queueOptions);
const get = async ({
key,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.BEATMAPS.byKey(key, priority, queueOptions);
const client = createClient(get, process);

@ -1,15 +1,15 @@
import {opt} from '../../../../utils/js'
import { opt } from "../../../../utils/js";
export default response => {
const versions = opt(response, 'versions');
export default (response) => {
const versions = opt(response, "versions");
if (!versions || !Array.isArray(versions) || !versions.length) return null;
const lastIdx = versions.length - 1;
const hash = opt(versions, `${lastIdx}.hash`);
const key = opt(response, 'id');
const key = opt(response, "id");
if (!hash || !key || !hash.toLowerCase) return null;
return {...response, hash: hash.toLowerCase(), key}
}
return { ...response, hash: hash.toLowerCase(), key };
};

@ -1,6 +1,6 @@
import queue from '../../queues/queues'
import {dateFromString} from '../../../utils/date'
import createClient from '../generic'
import queue from "../../queues/queues";
import { dateFromString } from "../../../utils/date";
import createClient from "../generic";
const SONG_DATA_TYPES = {
None: 0,
@ -8,14 +8,14 @@ const SONG_DATA_TYPES = {
Fail: 2,
Practice: 3,
Replay: 4,
Campaign: 5
}
Campaign: 5,
};
const process = response => {
const process = (response) => {
if (!response || !Array.isArray(response)) return null;
return response
.map(s => {
.map((s) => {
let {
_id: beatSaviorId,
playerID: playerId,
@ -29,28 +29,65 @@ const process = response => {
timeSet,
trackers,
trackers: {
accuracyTracker: {accLeft, accRight, leftAverageCut, rightAverageCut, leftTimeDependence, rightTimeDependence, leftPreswing, leftPostswing, rightPreswing, rightPostswing},
accuracyTracker: {
accLeft,
accRight,
leftAverageCut,
rightAverageCut,
leftTimeDependence,
rightTimeDependence,
leftPreswing,
leftPostswing,
rightPreswing,
rightPostswing,
},
winTracker: { won, nbOfPause: pauses, rank },
hitTracker: {bombHit, miss, missedNotes, badCuts, nbOfWallHit: wallHit, maxCombo},
hitTracker: {
bombHit,
miss,
missedNotes,
badCuts,
nbOfWallHit: wallHit,
maxCombo,
},
scoreTracker: { score },
},
} = s;
if (![SONG_DATA_TYPES.Pass, SONG_DATA_TYPES.Fail, SONG_DATA_TYPES.Campaign].includes(type)) return null;
if (
![
SONG_DATA_TYPES.Pass,
SONG_DATA_TYPES.Fail,
SONG_DATA_TYPES.Campaign,
].includes(type)
)
return null;
const leaderboardId = null;
hash = hash ? hash.toLowerCase() : null;
if (!playerId || !playerId.length || !hash || !hash.length || !diff || !diff.length || !score) return null;
if (
!playerId ||
!playerId.length ||
!hash ||
!hash.length ||
!diff ||
!diff.length ||
!score
)
return null;
const song = {hash, name, subName: '', authorName, levelAuthorName};
const song = { hash, name, subName: "", authorName, levelAuthorName };
const leaderboard = {
leaderboardId,
difficulty,
diffInfo: {diff: diff === 'expertplus' ? 'expertPlus' : diff, type: 'Standard'},
diffInfo: {
diff: diff === "expertplus" ? "expertPlus" : diff,
type: "Standard",
},
song,
}
};
const stats = {
won,
@ -62,9 +99,17 @@ const process = response => {
bombHit,
wallHit,
maxCombo,
accLeft, accRight, leftAverageCut, rightAverageCut, leftTimeDependence, rightTimeDependence,
leftPreswing, leftPostswing, rightPreswing, rightPostswing,
}
accLeft,
accRight,
leftAverageCut,
rightAverageCut,
leftTimeDependence,
rightTimeDependence,
leftPreswing,
leftPostswing,
rightPreswing,
rightPostswing,
};
return {
beatSaviorId,
@ -72,24 +117,27 @@ const process = response => {
leaderboardId,
scoreId: null,
hash,
diff: diff === 'expertplus' ? 'expertPlus' : diff,
diff: diff === "expertplus" ? "expertPlus" : diff,
score,
type,
leaderboard,
timeSet: dateFromString(timeSet),
stats,
trackers,
}
};
})
.filter(s => s);
.filter((s) => s);
};
const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATSAVIOR.player(playerId, priority, queueOptions);
const get = async ({
playerId,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.BEATSAVIOR.player(playerId, priority, queueOptions);
const client = createClient(get, process);
export default {
...client,
SONG_DATA_TYPES
SONG_DATA_TYPES,
};

@ -1,19 +1,35 @@
import queue, {getResponseBody, isResponseCached, updateResponseBody} from '../queues/queues'
import queue, {
getResponseBody,
isResponseCached,
updateResponseBody,
} from "../queues/queues";
export default (get, process) => {
const clientGet = async ({priority = queue.PRIORITY.FG_LOW, fullResponse = false, ...getOptions} = {}) => {
const clientGet = async ({
priority = queue.PRIORITY.FG_LOW,
fullResponse = false,
...getOptions
} = {}) => {
const response = await get({ ...getOptions, priority });
return fullResponse ? response : getResponseBody(response);
}
};
const clientGetProcessed = async ({priority = queue.PRIORITY.FG_LOW, fullResponse = false, ...getOptions} = {}) => {
const clientGetProcessed = async ({
priority = queue.PRIORITY.FG_LOW,
fullResponse = false,
...getOptions
} = {}) => {
const response = await clientGet({ ...getOptions, priority, fullResponse });
const processedResponse = process(fullResponse ? getResponseBody(response) : response);
const processedResponse = process(
fullResponse ? getResponseBody(response) : response,
);
return fullResponse ? updateResponseBody(response, processedResponse) : processedResponse;
}
return fullResponse
? updateResponseBody(response, processedResponse)
: processedResponse;
};
return {
get: clientGet,
@ -21,5 +37,5 @@ export default (get, process) => {
getProcessed: clientGetProcessed,
getDataFromResponse: getResponseBody,
isResponseCached,
}
}
};
};

@ -1,34 +1,63 @@
import queue from '../../../queues/queues'
import {opt} from '../../../../utils/js'
import createClient from '../../generic'
import queue from "../../../queues/queues";
import { opt } from "../../../../utils/js";
import createClient from "../../generic";
const process = response => {
if (!opt(response, 'scores') || !Array.isArray(response.scores)) return null;
const process = (response) => {
if (!opt(response, "scores") || !Array.isArray(response.scores)) return null;
const scores = response.scores.map(s => {
const scores = response.scores.map((s) => {
let { unmodififiedScore: unmodifiedScore, mods, ...score } = s.score;
if (mods && typeof mods === 'string') mods = mods.split(',').map(m => m.trim().toUpperCase()).filter(m => m.length);
if (mods && typeof mods === "string")
mods = mods
.split(",")
.map((m) => m.trim().toUpperCase())
.filter((m) => m.length);
else if (!mods) mods = null;
const acc = unmodifiedScore && opt(score, 'maxScore') ? unmodifiedScore / score.maxScore * 100 : opt(score, 'acc', null);
const percentage = opt(score, 'score') && opt(score, 'maxScore') ? score.score / score.maxScore * 100 : opt(score, 'percentage', null);
const acc =
unmodifiedScore && opt(score, "maxScore")
? (unmodifiedScore / score.maxScore) * 100
: opt(score, "acc", null);
const percentage =
opt(score, "score") && opt(score, "maxScore")
? (score.score / score.maxScore) * 100
: opt(score, "percentage", null);
const ppWeighted = opt(score, 'pp') && opt(score, 'weight') ? score.pp * score.weight : null;
const ppWeighted =
opt(score, "pp") && opt(score, "weight") ? score.pp * score.weight : null;
return {
...s,
score: {...score, unmodifiedScore: unmodifiedScore || null, mods, acc, percentage, ppWeighted},
score: {
...score,
unmodifiedScore: unmodifiedScore || null,
mods,
acc,
percentage,
ppWeighted,
},
};
});
return {
...response,
scores
}
}
scores,
};
};
const get = async ({leaderboardId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.leaderboard(leaderboardId, page, priority, queueOptions);
const get = async ({
leaderboardId,
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) =>
queue.SCORESABER_PAGE.leaderboard(
leaderboardId,
page,
priority,
queueOptions,
);
const client = createClient(get, process);

@ -1,34 +1,56 @@
import queue from '../../../queues/queues'
import createClient from '../../generic'
import {opt} from '../../../../utils/js'
import queue from "../../../queues/queues";
import createClient from "../../generic";
import { opt } from "../../../../utils/js";
const process = response => {
if (!opt(response, 'playerInfo')) return null;
const process = (response) => {
if (!opt(response, "playerInfo")) return null;
const { playerInfo: info, scoreStats } = response;
const {playerId, playerName: name, country, countryRank, avatar, permissions, ...playerInfo} = info;
const {
playerId,
playerName: name,
country,
countryRank,
avatar,
permissions,
...playerInfo
} = info;
if (avatar) {
if (!avatar.startsWith('http'))
playerInfo.avatar = `${queue.SCORESABER_API.SS_API_HOST}${!avatar.startsWith('/') ? '/' : ''}${avatar}`;
else
playerInfo.avatar = avatar;
if (!avatar.startsWith("http"))
playerInfo.avatar = `${queue.SCORESABER_API.SS_API_HOST}${
!avatar.startsWith("/") ? "/" : ""
}${avatar}`;
else playerInfo.avatar = avatar;
}
playerInfo.banned = !!playerInfo.banned;
playerInfo.inactive = !!playerInfo.inactive;
playerInfo.rankHistory = playerInfo.history && playerInfo.history.length
? playerInfo.history.split(',').map(r => parseInt(r, 10)).filter(r => !isNaN(r))
playerInfo.rankHistory =
playerInfo.history && playerInfo.history.length
? playerInfo.history
.split(",")
.map((r) => parseInt(r, 10))
.filter((r) => !isNaN(r))
: [];
delete playerInfo.history;
playerInfo.externalProfileUrl = null;
playerInfo.countries = [{ country, rank: countryRank }];
return {playerId, name, playerInfo, scoreStats: scoreStats ? scoreStats : null};
return {
playerId,
name,
playerInfo,
scoreStats: scoreStats ? scoreStats : null,
};
};
const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.player(playerId, priority, queueOptions);
const get = async ({
playerId,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.SCORESABER_API.player(playerId, priority, queueOptions);
const client = createClient(get, process);

@ -1,21 +1,27 @@
import queue from '../../../queues/queues'
import api from './api'
import {opt} from '../../../../utils/js'
import createClient from '../../generic'
import queue from "../../../queues/queues";
import api from "./api";
import { opt } from "../../../../utils/js";
import createClient from "../../generic";
const process = response => {
const apiProcessedResponse = api.process(response && response.player ? response.player : null);
const process = (response) => {
const apiProcessedResponse = api.process(
response && response.player ? response.player : null,
);
if (!opt(apiProcessedResponse, 'player.playerInfo')) return null;
if (!opt(apiProcessedResponse, "player.playerInfo")) return null;
const recentPlay = opt(response, 'player.recentPlay');
const recentPlayLastUpdated = opt(response, 'player.recentPlayLastUpdated');
const recentPlay = opt(response, "player.recentPlay");
const recentPlayLastUpdated = opt(response, "player.recentPlayLastUpdated");
if (recentPlay && recentPlayLastUpdated) {
apiProcessedResponse.playerInfo.recentPlay = recentPlay;
apiProcessedResponse.playerInfo.recentPlayLastUpdated = recentPlayLastUpdated;
apiProcessedResponse.playerInfo.recentPlayLastUpdated =
recentPlayLastUpdated;
}
const externalProfileUrl = opt(response, 'player.playerInfo.externalProfileUrl');
const externalProfileUrl = opt(
response,
"player.playerInfo.externalProfileUrl",
);
if (externalProfileUrl) {
apiProcessedResponse.playerInfo.externalProfileUrl = externalProfileUrl;
}
@ -23,7 +29,11 @@ const process = response => {
return apiProcessedResponse;
};
const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.player(playerId, priority, queueOptions);
const get = async ({
playerId,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.SCORESABER_PAGE.player(playerId, priority, queueOptions);
const client = createClient(get, process);

@ -1,8 +1,12 @@
import queue from '../../../queues/queues'
import process from './utils/process'
import createClient from '../../generic'
import queue from "../../../queues/queues";
import process from "./utils/process";
import createClient from "../../generic";
const get = async ({query, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.findPlayer(query, priority, queueOptions);
const get = async ({
query,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.SCORESABER_API.findPlayer(query, priority, queueOptions);
const client = createClient(get, process);

@ -1,10 +1,13 @@
import queue from '../../../queues/queues'
import {opt} from '../../../../utils/js'
import createClient from '../../generic'
import queue from "../../../queues/queues";
import { opt } from "../../../../utils/js";
import createClient from "../../generic";
const process = response => opt(response, 'pages', null)
const process = (response) => opt(response, "pages", null);
const get = async ({priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.rankingGlobalPages(priority, queueOptions);
const get = async ({
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.SCORESABER_API.rankingGlobalPages(priority, queueOptions);
const client = createClient(get, process);

@ -1,8 +1,12 @@
import queue from '../../../queues/queues'
import process from './utils/process'
import createClient from '../../generic'
import queue from "../../../queues/queues";
import process from "./utils/process";
import createClient from "../../generic";
const get = async ({page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.rankingGlobal(page, priority, queueOptions);
const get = async ({
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.SCORESABER_API.rankingGlobal(page, priority, queueOptions);
const client = createClient(get, process);

@ -1,17 +1,23 @@
import queue from '../../../queues/queues'
import api from './api-ranking-global'
import {opt} from '../../../../utils/js'
import createClient from '../../generic'
import queue from "../../../queues/queues";
import api from "./api-ranking-global";
import { opt } from "../../../../utils/js";
import createClient from "../../generic";
const process = response => {
const process = (response) => {
const apiProcessedResponse = api.process(response);
if (!opt(response, 'players')) return null;
if (!opt(response, "players")) return null;
return apiProcessedResponse;
}
};
const get = async ({country, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.countryRanking(country, page, priority, queueOptions);
const get = async ({
country,
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) =>
queue.SCORESABER_PAGE.countryRanking(country, page, priority, queueOptions);
const client = createClient(get, process);

@ -1,17 +1,28 @@
import {opt} from '../../../../../utils/js'
import queue from '../../../../queues/queues'
import { opt } from "../../../../../utils/js";
import queue from "../../../../queues/queues";
export default response => {
if (!opt(response, 'players')) return null;
export default (response) => {
if (!opt(response, "players")) return null;
if (!Array.isArray(response.players)) return null;
return response.players.map(player => {
let {avatar, country, difference, history, playerId, playerName: name, pp, rank} = player;
return response.players.map((player) => {
let {
avatar,
country,
difference,
history,
playerId,
playerName: name,
pp,
rank,
} = player;
if (avatar) {
if (!avatar.startsWith('http'))
avatar = `${queue.SCORESABER_API.SS_API_HOST}${!avatar.startsWith('/') ? '/' : ''}${avatar}`;
if (!avatar.startsWith("http"))
avatar = `${queue.SCORESABER_API.SS_API_HOST}${
!avatar.startsWith("/") ? "/" : ""
}${avatar}`;
}
return {
@ -22,13 +33,17 @@ export default response => {
countries: [{ country, rank: null }],
pp,
rank,
rankHistory: history && history.length
? history.split(',').map(r => parseInt(r, 10)).filter(r => !isNaN(r))
rankHistory:
history && history.length
? history
.split(",")
.map((r) => parseInt(r, 10))
.filter((r) => !isNaN(r))
: [],
},
others: {
difference,
},
}
};
});
};

@ -1,9 +1,13 @@
import createClient from '../../generic'
import queues from '../../../queues/queues'
import createClient from "../../generic";
import queues from "../../../queues/queues";
const process = response => response;
const process = (response) => response;
const get = async ({page = 1, priority = queues.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queues.SCORESABER_PAGE.rankeds(page, priority, queueOptions)
const get = async ({
page = 1,
priority = queues.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queues.SCORESABER_PAGE.rankeds(page, priority, queueOptions);
const client = createClient(get, process);

@ -1,8 +1,14 @@
import queue from '../../../queues/queues'
import process from './utils/process';
import createClient from '../../generic'
import queue from "../../../queues/queues";
import process from "./utils/process";
import createClient from "../../generic";
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.recentScores(playerId, page, priority, queueOptions);
const get = async ({
playerId,
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) =>
queue.SCORESABER_API.recentScores(playerId, page, priority, queueOptions);
const client = createClient(get, process);

@ -1,8 +1,14 @@
import queue from '../../../queues/queues'
import createClient from '../../generic'
import process from './utils/process'
import queue from "../../../queues/queues";
import createClient from "../../generic";
import process from "./utils/process";
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.topScores(playerId, page, priority, queueOptions);
const get = async ({
playerId,
page = 1,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) =>
queue.SCORESABER_API.topScores(playerId, page, priority, queueOptions);
const client = createClient(get, process);

@ -1,11 +1,16 @@
import {dateFromString} from '../../../../../utils/date'
import {extractDiffAndType} from '../../../../../utils/scoresaber/format'
import {opt} from '../../../../../utils/js'
import { dateFromString } from "../../../../../utils/date";
import { extractDiffAndType } from "../../../../../utils/scoresaber/format";
import { opt } from "../../../../../utils/js";
export default response => {
if (!opt(response, 'scores') || !Array.isArray(response.scores) || !opt(response, 'scores.0.scoreId')) return [];
export default (response) => {
if (
!opt(response, "scores") ||
!Array.isArray(response.scores) ||
!opt(response, "scores.0.scoreId")
)
return [];
return response.scores.map(s => {
return response.scores.map((s) => {
const {
songHash: hash,
songName: name,
@ -24,19 +29,38 @@ export default response => {
let { unmodififiedScore: unmodifiedScore, mods, ...score } = originalScore;
if (mods && typeof mods === 'string') mods = mods.split(',').map(m => m.trim().toUpperCase()).filter(m => m.length);
if (mods && typeof mods === "string")
mods = mods
.split(",")
.map((m) => m.trim().toUpperCase())
.filter((m) => m.length);
else if (!mods) mods = null;
const acc = unmodifiedScore && opt(score, 'maxScore') ? unmodifiedScore / score.maxScore * 100 : null;
const percentage = opt(score, 'score') && opt(score, 'maxScore') ? score.score / score.maxScore * 100 : null;
const acc =
unmodifiedScore && opt(score, "maxScore")
? (unmodifiedScore / score.maxScore) * 100
: null;
const percentage =
opt(score, "score") && opt(score, "maxScore")
? (score.score / score.maxScore) * 100
: null;
const ppWeighted = opt(score, 'pp') && opt(score, 'weight') ? score.pp * score.weight : null;
const ppWeighted =
opt(score, "pp") && opt(score, "weight") ? score.pp * score.weight : null;
return {
leaderboard,
score: {...score, unmodifiedScore, mods, timeSet: dateFromString(score.timeSet), acc, percentage, ppWeighted},
score: {
...score,
unmodifiedScore,
mods,
timeSet: dateFromString(score.timeSet),
acc,
percentage,
ppWeighted,
},
fetchedAt: new Date(),
lastUpdated: new Date(),
};
});
}
};

@ -1,17 +1,22 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import {opt} from '../../../utils/js'
import queue from "../../queues/queues";
import createClient from "../generic";
import { opt } from "../../../utils/js";
const process = response => {
if (!opt(response, 'data.0')) return null;
const process = (response) => {
if (!opt(response, "data.0")) return null;
return { ...response.data[0], profileLastUpdated: new Date() };
};
const get = async ({accessToken, login, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.TWITCH.profile(accessToken, login, priority, queueOptions);
const get = async ({
accessToken,
login,
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.TWITCH.profile(accessToken, login, priority, queueOptions);
const client = createClient(get, process);
export default {
...client
}
...client,
};

@ -1,16 +1,22 @@
import queue from '../../queues/queues'
import createClient from '../generic'
import queue from "../../queues/queues";
import createClient from "../generic";
const process = response => {
const process = (response) => {
if (!response || !response.data || !Array.isArray(response.data)) return null;
return response.data;
};
const get = async ({accessToken, userId, type = 'archive', priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.TWITCH.videos(accessToken, userId, type, queueOptions);
const get = async ({
accessToken,
userId,
type = "archive",
priority = queue.PRIORITY.FG_HIGH,
...queueOptions
} = {}) => queue.TWITCH.videos(accessToken, userId, type, queueOptions);
const client = createClient(get, process);
export default {
...client,
}
};

@ -1,45 +1,48 @@
import eventBus from '../utils/broadcast-channel-pubsub'
import createPlayerService from '../services/scoresaber/player'
import log from '../utils/logger'
import eventBus from "../utils/broadcast-channel-pubsub";
import createPlayerService from "../services/scoresaber/player";
import log from "../utils/logger";
let initialized = false;
export default (dlManager) => {
if (initialized) {
log.debug(`Command processor already initialized.`, 'CmdProcessor');
log.debug(`Command processor already initialized.`, "CmdProcessor");
return;
}
const playerService = createPlayerService();
eventBus.on('data-imported', () => {
if (window) window.location.reload()
eventBus.on("data-imported", () => {
if (window) window.location.reload();
});
eventBus.on('player-add-cmd', async ({playerId}) => {
eventBus.on("player-add-cmd", async ({ playerId }) => {
await dlManager.enqueuePlayer(playerId);
});
eventBus.on('player-remove-cmd', async ({playerId, purgeScores = false}) => {
eventBus.on(
"player-remove-cmd",
async ({ playerId, purgeScores = false }) => {
if (!playerId) return;
await playerService.remove(playerId, purgeScores);
});
},
);
eventBus.on('dl-manager-pause-cmd', () => {
log.debug('Pause Dl Manager', 'CmdProcessor');
eventBus.on("dl-manager-pause-cmd", () => {
log.debug("Pause Dl Manager", "CmdProcessor");
dlManager.pause();
});
eventBus.on('dl-manager-unpause-cmd', () => {
log.debug('Unpause Dl Manager', 'CmdProcessor');
eventBus.on("dl-manager-unpause-cmd", () => {
log.debug("Unpause Dl Manager", "CmdProcessor");
dlManager.start();
});
initialized = true;
log.info(`Command processor initialized`, 'CmdProcessor');
}
log.info(`Command processor initialized`, "CmdProcessor");
};

@ -1,15 +1,15 @@
import eventBus from '../utils/broadcast-channel-pubsub'
import log from '../utils/logger'
import createQueue, {PRIORITY} from '../utils/queue'
import {configStore} from '../stores/config'
import createRankedsStore from '../stores/scoresaber/rankeds'
import createPlayerService from '../services/scoresaber/player'
import createScoresService from '../services/scoresaber/scores'
import createBeatSaviorService from '../services/beatsavior'
import createAccSaberService from '../services/accsaber'
import {PRIORITY as HTTP_QUEUE_PRIORITY} from './queues/http-queue'
import {HOUR, MINUTE} from '../utils/date'
import {opt} from '../utils/js'
import eventBus from "../utils/broadcast-channel-pubsub";
import log from "../utils/logger";
import createQueue, { PRIORITY } from "../utils/queue";
import { configStore } from "../stores/config";
import createRankedsStore from "../stores/scoresaber/rankeds";
import createPlayerService from "../services/scoresaber/player";
import createScoresService from "../services/scoresaber/scores";
import createBeatSaviorService from "../services/beatsavior";
import createAccSaberService from "../services/accsaber";
import { PRIORITY as HTTP_QUEUE_PRIORITY } from "./queues/http-queue";
import { HOUR, MINUTE } from "../utils/date";
import { opt } from "../utils/js";
const INTERVAL_TICK = MINUTE;
@ -22,110 +22,198 @@ let beatSaviorService = null;
let accSaberService = null;
const TYPES = {
BEATSAVIOR: {name: 'BEATSAVIOR', priority: PRIORITY.LOW},
RANKEDS: {name: 'RANKEDS', priority: PRIORITY.LOW},
ACCSABER: {name: 'ACCSABER', priority: PRIORITY.NORMAL},
PLAYER_SCORES: {name: 'PLAYER-SCORES', priority: PRIORITY.NORMAL},
PLAYER_SCORES_UPDATE_QUEUE: {name: 'PLAYER_SCORES_UPDATE_QUEUE', priority: PRIORITY.LOWEST},
ACTIVE_PLAYERS: {name: 'ACTIVE-PLAYERS', priority: PRIORITY.HIGH},
MAIN_PLAYER: {name: 'MAIN-PLAYER', priority: PRIORITY.HIGHEST},
}
BEATSAVIOR: { name: "BEATSAVIOR", priority: PRIORITY.LOW },
RANKEDS: { name: "RANKEDS", priority: PRIORITY.LOW },
ACCSABER: { name: "ACCSABER", priority: PRIORITY.NORMAL },
PLAYER_SCORES: { name: "PLAYER-SCORES", priority: PRIORITY.NORMAL },
PLAYER_SCORES_UPDATE_QUEUE: {
name: "PLAYER_SCORES_UPDATE_QUEUE",
priority: PRIORITY.LOWEST,
},
ACTIVE_PLAYERS: { name: "ACTIVE-PLAYERS", priority: PRIORITY.HIGH },
MAIN_PLAYER: { name: "MAIN-PLAYER", priority: PRIORITY.HIGHEST },
};
const enqueue = async (queue, type, force = false, data = null, then = null) => {
const enqueue = async (
queue,
type,
force = false,
data = null,
then = null,
) => {
if (!type || !type.name || !Number.isFinite(type.priority)) {
log.warn(`Unknown type enqueued.`, 'DlManager', type);
log.warn(`Unknown type enqueued.`, "DlManager", type);
return;
}
log.debug(`Try to enqueue type ${type.name}. Forced: ${force}, data: ${JSON.stringify(data)}`, 'DlManager');
log.debug(
`Try to enqueue type ${type.name}. Forced: ${force}, data: ${JSON.stringify(
data,
)}`,
"DlManager",
);
const priority = force ? PRIORITY.HIGHEST : type.priority;
const networkPriority = priority === PRIORITY.HIGHEST ? HTTP_QUEUE_PRIORITY.BG_HIGH : HTTP_QUEUE_PRIORITY.BG_NORMAL;
const networkPriority =
priority === PRIORITY.HIGHEST
? HTTP_QUEUE_PRIORITY.BG_HIGH
: HTTP_QUEUE_PRIORITY.BG_NORMAL;
const processThen = async (promise, then = null) => {
promise.then(result => {
if(then) log.debug('Processing then command...', 'DlManager');
promise.then((result) => {
if (then) log.debug("Processing then command...", "DlManager");
return then ? { result, thenResult: then() } : result;
})
}
});
};
switch (type) {
case TYPES.MAIN_PLAYER:
if (mainPlayerId) {
log.debug(`Enqueue main player`, 'DlManager');
log.debug(`Enqueue main player`, "DlManager");
await Promise.all([
enqueue(queue, {...TYPES.ACTIVE_PLAYERS, priority: PRIORITY.HIGHEST}, force, {playerId: mainPlayerId}),
enqueue(queue, {...TYPES.PLAYER_SCORES, priority: PRIORITY.HIGHEST}, force, {playerId: mainPlayerId}),
enqueue(
queue,
{ ...TYPES.ACTIVE_PLAYERS, priority: PRIORITY.HIGHEST },
force,
{ playerId: mainPlayerId },
),
enqueue(
queue,
{ ...TYPES.PLAYER_SCORES, priority: PRIORITY.HIGHEST },
force,
{ playerId: mainPlayerId },
),
]);
}
break;
case TYPES.RANKEDS:
log.debug(`Enqueue rankeds`, 'DlManager');
log.debug(`Enqueue rankeds`, "DlManager");
if (!rankedsStore) rankedsStore = await createRankedsStore();
processThen(queue.add(async () => rankedsStore.refresh(force, networkPriority), priority), then)
.then(_ => log.debug('Enqueued rankeds processed.', 'DlManager'));
processThen(
queue.add(
async () => rankedsStore.refresh(force, networkPriority),
priority,
),
then,
).then((_) => log.debug("Enqueued rankeds processed.", "DlManager"));
break;
case TYPES.ACTIVE_PLAYERS:
log.debug(`Enqueue active players`, 'DlManager');
log.debug(`Enqueue active players`, "DlManager");
if (data && data.playerId) {
if (data.add)
processThen(queue.add(async () => playerService.add(data.playerId, networkPriority), priority), then)
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
processThen(
queue.add(
async () => playerService.add(data.playerId, networkPriority),
priority,
),
then,
).then((_) =>
log.debug("Enqueued active players processed.", "DlManager"),
);
else
processThen(queue.add(async () => playerService.refresh(data.playerId, force, networkPriority), priority), then)
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
processThen(
queue.add(
async () =>
playerService.refresh(data.playerId, force, networkPriority),
priority,
),
then,
).then((_) =>
log.debug("Enqueued active players processed.", "DlManager"),
);
} else
processThen(queue.add(async () => playerService.refreshAll(force, networkPriority), priority), then)
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
processThen(
queue.add(
async () => playerService.refreshAll(force, networkPriority),
priority,
),
then,
).then((_) =>
log.debug("Enqueued active players processed.", "DlManager"),
);
break;
case TYPES.PLAYER_SCORES:
log.debug(`Enqueue players scores`, 'DlManager');
log.debug(`Enqueue players scores`, "DlManager");
if (data && data.playerId)
processThen(queue.add(async () => scoresService.refresh(data.playerId, force, networkPriority), priority), then)
.then(_ => log.debug('Enqueued players scores processed.', 'DlManager'));
processThen(
queue.add(
async () =>
scoresService.refresh(data.playerId, force, networkPriority),
priority,
),
then,
).then((_) =>
log.debug("Enqueued players scores processed.", "DlManager"),
);
else
processThen(queue.add(async () => scoresService.refreshAll(force, networkPriority), priority), then)
.then(_ => log.debug('Enqueued players scores processed.', 'DlManager'));
processThen(
queue.add(
async () => scoresService.refreshAll(force, networkPriority),
priority,
),
then,
).then((_) =>
log.debug("Enqueued players scores processed.", "DlManager"),
);
break;
case TYPES.BEATSAVIOR:
log.debug(`Enqueue Beat Savior`, 'DlManager');
log.debug(`Enqueue Beat Savior`, "DlManager");
processThen(queue.add(async () => beatSaviorService.refreshAll(force, networkPriority), priority), then)
.then(_ => log.debug('Enqueued Beat Savior processed.', 'DlManager'));
processThen(
queue.add(
async () => beatSaviorService.refreshAll(force, networkPriority),
priority,
),
then,
).then((_) => log.debug("Enqueued Beat Savior processed.", "DlManager"));
break;
case TYPES.PLAYER_SCORES_UPDATE_QUEUE:
log.debug(`Enqueue player scores rank && pp updates`, 'DlManager');
log.debug(`Enqueue player scores rank && pp updates`, "DlManager");
processThen(queue.add(async () => scoresService.updateRankAndPpFromTheQueue(), priority), then)
.then(_ => log.debug('Enqueued player scores rank & pp updates processed.', 'DlManager'));
processThen(
queue.add(
async () => scoresService.updateRankAndPpFromTheQueue(),
priority,
),
then,
).then((_) =>
log.debug(
"Enqueued player scores rank & pp updates processed.",
"DlManager",
),
);
break;
case TYPES.ACCSABER:
log.debug(`Enqueue AccSaber updates`, 'DlManager');
log.debug(`Enqueue AccSaber updates`, "DlManager");
processThen(queue.add(async () => accSaberService.refreshAll(), priority), then)
.then(_ => log.debug('Enqueued AccSaber updates processed.', 'DlManager'));
processThen(
queue.add(async () => accSaberService.refreshAll(), priority),
then,
).then((_) =>
log.debug("Enqueued AccSaber updates processed.", "DlManager"),
);
break;
}
}
};
const enqueueAllJobs = async queue => {
log.debug(`Try to enqueue & process queue.`, 'DlManager');
const enqueueAllJobs = async (queue) => {
log.debug(`Try to enqueue & process queue.`, "DlManager");
await Promise.all([
enqueue(queue, TYPES.MAIN_PLAYER),
@ -137,18 +225,18 @@ const enqueueAllJobs = async queue => {
// it should be at the end of the queue
enqueue(queue, TYPES.PLAYER_SCORES_UPDATE_QUEUE),
])
}
]);
};
let intervalId;
const startSyncing = async queue => {
const startSyncing = async (queue) => {
await enqueueAllJobs(queue);
intervalId = setInterval(() => enqueueAllJobs(queue), INTERVAL_TICK);
}
};
export default async () => {
if (initialized) {
log.debug(`Download manager already initialized.`, 'DlManager');
log.debug(`Download manager already initialized.`, "DlManager");
return;
}
@ -161,49 +249,54 @@ export default async () => {
mainPlayerId = configStore.getMainPlayerId();
configStore.subscribe(config => {
const newMainPlayerId = opt(config, 'users.main')
configStore.subscribe((config) => {
const newMainPlayerId = opt(config, "users.main");
if (mainPlayerId !== newMainPlayerId) {
mainPlayerId = newMainPlayerId;
log.debug(`Main player changed to ${mainPlayerId}`, 'DlManager')
log.debug(`Main player changed to ${mainPlayerId}`, "DlManager");
}
})
});
playerService = createPlayerService();
scoresService = createScoresService();
beatSaviorService = createBeatSaviorService();
accSaberService = createAccSaberService();
eventBus.leaderStore.subscribe(async isLeader => {
eventBus.leaderStore.subscribe(async (isLeader) => {
if (isLeader) {
queue.clear();
queue.start();
const nodeId = eventBus.getNodeId();
log.info(`Node ${nodeId} is a leader, queue processing enabled`, 'DlManager')
log.info(
`Node ${nodeId} is a leader, queue processing enabled`,
"DlManager",
);
await startSyncing(queue)
await startSyncing(queue);
}
})
});
const enqueuePlayer = async playerId => {
const enqueuePlayer = async (playerId) => {
await enqueue(
queue, TYPES.ACTIVE_PLAYERS, true,
queue,
TYPES.ACTIVE_PLAYERS,
true,
{ playerId, add: true },
async () => enqueue(queue, TYPES.PLAYER_SCORES, true, { playerId }),
);
}
};
const pause = () => {
log.debug('Pause Dl Manager', 'DlManager');
log.debug("Pause Dl Manager", "DlManager");
queue.clear();
queue.pause();
};
const start = () => {
log.debug('Unpause Dl Manager', 'DlManager');
log.debug("Unpause Dl Manager", "DlManager");
queue.clear();
queue.start();
@ -213,11 +306,11 @@ export default async () => {
initialized = true;
log.info(`Download manager initialized`, 'DlManager');
log.info(`Download manager initialized`, "DlManager");
return {
start,
pause,
enqueuePlayer
}
}
enqueuePlayer,
};
};

@ -1,6 +1,6 @@
import {SsrError} from '../others/errors'
import {delay} from '../utils/promise'
import {parseRateLimitHeaders} from './utils'
import { SsrError } from "../others/errors";
import { delay } from "../utils/promise";
import { parseRateLimitHeaders } from "./utils";
export class SsrNetworkError extends SsrError {
constructor(message) {
@ -20,7 +20,7 @@ export class SsrNetworkError extends SsrError {
export class SsrNetworkTimeoutError extends SsrNetworkError {
constructor(timeout, message) {
super(message && message.length ? message : `Timeout Error (${timeout}ms)`)
super(message && message.length ? message : `Timeout Error (${timeout}ms)`);
this.name = "SsrNetworkTimeoutError";
this.timeout = timeout;
@ -29,9 +29,14 @@ export class SsrNetworkTimeoutError extends SsrNetworkError {
export class SsrHttpResponseError extends SsrNetworkError {
constructor(response, ...args) {
super(`HTTP Error Response: ${response && response.status ? response.status : 'None'} ${response && response.statusText ? response.statusText : ''}`, ...args);
super(
`HTTP Error Response: ${
response && response.status ? response.status : "None"
} ${response && response.statusText ? response.statusText : ""}`,
...args,
);
this.name = 'SsrHttpResponseError';
this.name = "SsrHttpResponseError";
this.response = response;
const { remaining, limit, resetAt } = parseRateLimitHeaders(response);
@ -50,7 +55,7 @@ export class SsrHttpClientError extends SsrHttpResponseError {
constructor(...args) {
super(...args);
this.name = 'SsrHttpClientError';
this.name = "SsrHttpClientError";
}
shouldRetry() {
@ -66,7 +71,7 @@ export class SsrHttpRateLimitError extends SsrHttpClientError {
constructor(response, ...args) {
super(response, ...args);
this.name = 'SsrHttpRateLimitError';
this.name = "SsrHttpRateLimitError";
}
shouldRetry() {
@ -119,6 +124,6 @@ export class SsrHttpServerError extends SsrHttpResponseError {
constructor(...args) {
super(...args);
this.name = 'SsrHttpServerError';
this.name = "SsrHttpServerError";
}
}

@ -97,12 +97,12 @@ export async function fetchUrl(url, options = {}, cors = true) {
export async function fetchJson(
url,
{ cacheTtl = null, maxAge = null, ...restOptions } = {}
{ cacheTtl = null, maxAge = null, ...restOptions } = {},
) {
const options = getOptionsWithCacheKey(
url,
{ cacheTtl, maxAge, ...restOptions },
"json"
"json",
);
const {
@ -129,7 +129,7 @@ export async function fetchJson(
body,
},
fetchCacheKey,
fetchCacheTtl
fetchCacheTtl,
);
})
.catch((err) => {
@ -141,12 +141,12 @@ export async function fetchJson(
export async function fetchHtml(
url,
{ cacheTtl = null, maxAge = null, ...restOptions } = {}
{ cacheTtl = null, maxAge = null, ...restOptions } = {},
) {
const options = getOptionsWithCacheKey(
url,
{ cacheTtl, maxAge, ...restOptions },
"json"
"json",
);
const {
@ -172,7 +172,7 @@ export async function fetchHtml(
body: new DOMParser().parseFromString(body, "text/html"),
},
fetchCacheKey,
fetchCacheTtl
fetchCacheTtl,
);
});
}

@ -1,25 +1,75 @@
import {default as createQueue, PRIORITY} from '../http-queue';
import { default as createQueue, PRIORITY } from "../http-queue";
import { substituteVars } from "../../../utils/format";
const ACCSABER_API_URL = 'https://api.accsaber.com';
const CATEGORIES_URL = ACCSABER_API_URL + '/categories';
const RANKING_URL = ACCSABER_API_URL + '/categories/${category}/standings';
const PLAYER_SCORES_URL = ACCSABER_API_URL + '/players/${playerId}/scores';
const PLAYER_RANK_HISTORY = ACCSABER_API_URL + '/players/${playerId}/recent-rank-history'
const LEADERBOARD_URL = ACCSABER_API_URL + '/map-leaderboards/${leaderboardId}';
const LEADERBOARD_INFO_URL = ACCSABER_API_URL + '/ranked-maps/${leaderboardId}';
const ACCSABER_API_URL = "https://api.accsaber.com";
const CATEGORIES_URL = ACCSABER_API_URL + "/categories";
const RANKING_URL = ACCSABER_API_URL + "/categories/${category}/standings";
const PLAYER_SCORES_URL = ACCSABER_API_URL + "/players/${playerId}/scores";
const PLAYER_RANK_HISTORY =
ACCSABER_API_URL + "/players/${playerId}/recent-rank-history";
const LEADERBOARD_URL = ACCSABER_API_URL + "/map-leaderboards/${leaderboardId}";
const LEADERBOARD_INFO_URL = ACCSABER_API_URL + "/ranked-maps/${leaderboardId}";
export default (options = {}) => {
const queue = createQueue(options);
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
const categories = async (priority = PRIORITY.FG_LOW, options = {}) => fetchJson(CATEGORIES_URL, options, priority)
const ranking = async (category = 'overall', page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(RANKING_URL, {category, page}), options, priority)
const scores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(PLAYER_SCORES_URL, {playerId, page}), options, priority)
const playerRankHistory = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(PLAYER_RANK_HISTORY, {playerId}), options, priority)
const leaderboard = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(LEADERBOARD_URL, {leaderboardId, page}), options, priority)
const leaderboardInfo = async (leaderboardId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(LEADERBOARD_INFO_URL, {leaderboardId}), options, priority)
const categories = async (priority = PRIORITY.FG_LOW, options = {}) =>
fetchJson(CATEGORIES_URL, options, priority);
const ranking = async (
category = "overall",
page = 1,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(
substituteVars(RANKING_URL, { category, page }),
options,
priority,
);
const scores = async (
playerId,
page = 1,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(
substituteVars(PLAYER_SCORES_URL, { playerId, page }),
options,
priority,
);
const playerRankHistory = async (
playerId,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(
substituteVars(PLAYER_RANK_HISTORY, { playerId }),
options,
priority,
);
const leaderboard = async (
leaderboardId,
page = 1,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(
substituteVars(LEADERBOARD_URL, { leaderboardId, page }),
options,
priority,
);
const leaderboardInfo = async (
leaderboardId,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(
substituteVars(LEADERBOARD_INFO_URL, { leaderboardId }),
options,
priority,
);
return {
categories,
@ -29,5 +79,5 @@ export default (options = {}) => {
leaderboard,
leaderboardInfo,
...queueToReturn,
}
}
};
};

@ -1,21 +1,23 @@
import {default as createQueue, PRIORITY} from '../http-queue';
import { default as createQueue, PRIORITY } from "../http-queue";
import { substituteVars } from "../../../utils/format";
const BEATMAPS_API_URL = 'https://api.beatsaver.com/';
const SONG_BY_HASH_URL = BEATMAPS_API_URL + '/maps/hash/${hash}';
const SONG_BY_KEY_URL = BEATMAPS_API_URL + '/maps/id/${key}'
const BEATMAPS_API_URL = "https://api.beatsaver.com/";
const SONG_BY_HASH_URL = BEATMAPS_API_URL + "/maps/hash/${hash}";
const SONG_BY_KEY_URL = BEATMAPS_API_URL + "/maps/id/${key}";
export default (options = {}) => {
const queue = createQueue(options);
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
const byHash = async (hash, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SONG_BY_HASH_URL, {hash}), options, priority)
const byKey = async (key, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SONG_BY_KEY_URL, {key}), options, priority)
const byHash = async (hash, priority = PRIORITY.FG_LOW, options = {}) =>
fetchJson(substituteVars(SONG_BY_HASH_URL, { hash }), options, priority);
const byKey = async (key, priority = PRIORITY.FG_LOW, options = {}) =>
fetchJson(substituteVars(SONG_BY_KEY_URL, { key }), options, priority);
return {
byHash,
byKey,
...queueToReturn,
}
}
};
};

@ -1,9 +1,17 @@
import {default as createQueue, PRIORITY as QUEUE_PRIORITY} from '../../utils/queue';
import {SsrError, SsrTimeoutError} from '../../others/errors'
import {SsrHttpRateLimitError, SsrHttpResponseError, SsrNetworkError, SsrNetworkTimeoutError} from '../errors'
import {fetchHtml, fetchJson} from '../fetch';
import makePendingPromisePool from '../../utils/pending-promises'
import {AbortError} from '../../utils/promise'
import {
default as createQueue,
PRIORITY as QUEUE_PRIORITY,
} from "../../utils/queue";
import { SsrError, SsrTimeoutError } from "../../others/errors";
import {
SsrHttpRateLimitError,
SsrHttpResponseError,
SsrNetworkError,
SsrNetworkTimeoutError,
} from "../errors";
import { fetchHtml, fetchJson } from "../fetch";
import makePendingPromisePool from "../../utils/pending-promises";
import { AbortError } from "../../utils/promise";
const DEFAULT_RETRIES = 2;
@ -13,24 +21,41 @@ export const PRIORITY = {
BG_HIGH: QUEUE_PRIORITY.NORMAL,
BG_NORMAL: QUEUE_PRIORITY.LOW,
BG_LOW: QUEUE_PRIORITY.LOWEST,
}
};
const resolvePromiseOrWaitForPending = makePendingPromisePool();
export default (options = {}) => {
const {retries, rateLimitTick, ...queueOptions} = {retries: DEFAULT_RETRIES, rateLimitTick: 500, ...options};
const { retries, rateLimitTick, ...queueOptions } = {
retries: DEFAULT_RETRIES,
rateLimitTick: 500,
...options,
};
const queue = createQueue(queueOptions);
const { add, emitter, ...queueToReturn } = queue;
let lastRateLimitError = null;
let rateLimitTimerId = null;
let currentRateLimit = {waiting: 0, remaining: null, limit: null, resetAt: null};
let currentRateLimit = {
waiting: 0,
remaining: null,
limit: null,
resetAt: null,
};
const rateLimitTicker = () => {
const expiresInMs = lastRateLimitError && lastRateLimitError.resetAt ? lastRateLimitError.resetAt - new Date() + 1000 : 0;
const expiresInMs =
lastRateLimitError && lastRateLimitError.resetAt
? lastRateLimitError.resetAt - new Date() + 1000
: 0;
if (expiresInMs <= 0) {
emitter.emit('waiting', {waiting: 0, remaining: null, limit: null, resetAt: null});
emitter.emit("waiting", {
waiting: 0,
remaining: null,
limit: null,
resetAt: null,
});
if (rateLimitTimerId) clearTimeout(rateLimitTimerId);
@ -38,13 +63,23 @@ export default (options = {}) => {
}
const { remaining, limit, resetAt } = lastRateLimitError;
emitter.emit('waiting', {waiting: expiresInMs, remaining, limit, resetAt});
emitter.emit("waiting", {
waiting: expiresInMs,
remaining,
limit,
resetAt,
});
if (rateLimitTimerId) clearTimeout(rateLimitTimerId);
rateLimitTimerId = setTimeout(rateLimitTicker, rateLimitTick);
}
};
const retriedFetch = async (fetchFunc, url, options, priority = PRIORITY.FG_LOW) => {
const retriedFetch = async (
fetchFunc,
url,
options,
priority = PRIORITY.FG_LOW,
) => {
for (let i = 0; i <= retries; i++) {
try {
return await add(async () => {
@ -55,19 +90,18 @@ export default (options = {}) => {
}
return fetchFunc(url, options)
.then(response => {
.then((response) => {
currentRateLimit = { ...response.rateLimit, waiting: 0 };
return response;
})
.catch(err => {
if (err instanceof SsrTimeoutError) throw new SsrNetworkTimeoutError(err.timeout);
.catch((err) => {
if (err instanceof SsrTimeoutError)
throw new SsrNetworkTimeoutError(err.timeout);
throw err;
})
},
priority,
)
});
}, priority);
} catch (err) {
if (err instanceof SsrHttpResponseError) {
const { remaining, limit, resetAt } = err;
@ -79,7 +113,13 @@ export default (options = {}) => {
if (!shouldRetry || i === retries) throw err;
if (err instanceof SsrHttpRateLimitError) {
if (err.remaining <= 0 && err.resetAt && (!lastRateLimitError || !lastRateLimitError.resetAt || lastRateLimitError.resetAt < err.resetAt)) {
if (
err.remaining <= 0 &&
err.resetAt &&
(!lastRateLimitError ||
!lastRateLimitError.resetAt ||
lastRateLimitError.resetAt < err.resetAt)
) {
lastRateLimitError = err;
rateLimitTicker();
@ -95,11 +135,17 @@ export default (options = {}) => {
}
}
throw new SsrError('Unknown error');
}
throw new SsrError("Unknown error");
};
const queuedFetchJson = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchJson, url, options, priority));
const queuedFetchHtml = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchHtml, url, options, priority));
const queuedFetchJson = async (url, options, priority = PRIORITY.FG_LOW) =>
resolvePromiseOrWaitForPending(url, () =>
retriedFetch(fetchJson, url, options, priority),
);
const queuedFetchHtml = async (url, options, priority = PRIORITY.FG_LOW) =>
resolvePromiseOrWaitForPending(url, () =>
retriedFetch(fetchHtml, url, options, priority),
);
const getRateLimit = () => currentRateLimit;
@ -108,5 +154,5 @@ export default (options = {}) => {
fetchHtml: queuedFetchHtml,
getRateLimit,
...queueToReturn,
}
}
};
};

@ -1,17 +1,18 @@
import {writable} from 'svelte/store'
import {PRIORITY} from './http-queue'
import createScoreSaberApiQueue from './scoresaber/api-queue'
import createScoreSaberPageQueue from './scoresaber/page-queue'
import createBeatMapsApiQueue from './beatmaps/api-queue'
import createBeatSaviorApiQueue from './beatsavior/api-queue'
import createTwitchApiQueue from './twitch/api-queue'
import createAccSaberApiQueue from './accsaber/api-queue'
import { writable } from "svelte/store";
import { PRIORITY } from "./http-queue";
import createScoreSaberApiQueue from "./scoresaber/api-queue";
import createScoreSaberPageQueue from "./scoresaber/page-queue";
import createBeatMapsApiQueue from "./beatmaps/api-queue";
import createBeatSaviorApiQueue from "./beatsavior/api-queue";
import createTwitchApiQueue from "./twitch/api-queue";
import createAccSaberApiQueue from "./accsaber/api-queue";
export const getResponseBody = response => response ? response.body : null;
export const isResponseCached = response => !!(response && response.cached)
export const updateResponseBody = (response, body) => response ? {...response, body} : null;
export const getResponseBody = (response) => (response ? response.body : null);
export const isResponseCached = (response) => !!(response && response.cached);
export const updateResponseBody = (response, body) =>
response ? { ...response, body } : null;
const initQueue = queue => {
const initQueue = (queue) => {
let queueState = {
size: 0,
pending: 0,
@ -21,35 +22,78 @@ const initQueue = queue => {
const { subscribe, set } = writable(queueState);
queue.on('change', ({size, pending}) => {
const {rateLimit: {waiting}} = queueState;
queue.on("change", ({ size, pending }) => {
const {
rateLimit: { waiting },
} = queueState;
const { remaining, limit, resetAt } = queue.getRateLimit();
queueState = {...queueState, size, pending, rateLimit: {waiting, remaining, limit, resetAt}};
queueState = {
...queueState,
size,
pending,
rateLimit: { waiting, remaining, limit, resetAt },
};
set(queueState);
});
queue.on('progress', ({progress, num, count}) => {
const {rateLimit: {waiting}} = queueState;
queue.on("progress", ({ progress, num, count }) => {
const {
rateLimit: { waiting },
} = queueState;
const { remaining, limit, resetAt } = queue.getRateLimit();
queueState = {...queueState, progress: {num, count, progress}, rateLimit: {waiting, remaining, limit, resetAt}}
queueState = {
...queueState,
progress: { num, count, progress },
rateLimit: { waiting, remaining, limit, resetAt },
};
set(queueState);
});
queue.on('waiting', ({waiting, remaining, limit, resetAt}) => {
queueState = {...queueState, rateLimit: {waiting, remaining, limit, resetAt}}
queue.on("waiting", ({ waiting, remaining, limit, resetAt }) => {
queueState = {
...queueState,
rateLimit: { waiting, remaining, limit, resetAt },
};
set(queueState);
})
});
return {
subscribe,
...queue,
}
}
};
};
export default {
SCORESABER_API: initQueue(createScoreSaberApiQueue({concurrency: 3, timeout: 95000})),
SCORESABER_PAGE: initQueue(createScoreSaberPageQueue({concurrency: 3, timeout: 30000})),
BEATMAPS: initQueue(createBeatMapsApiQueue({concurrency: 1, timeout: 10000, intervalCap: 10, interval: 1000})),
BEATSAVIOR: initQueue(createBeatSaviorApiQueue({concurrency: 1, timeout: 10000, intervalCap: 60, interval: 60000})),
TWITCH: initQueue(createTwitchApiQueue({concurrency: 8, timeout: 8000, intervalCap: 800, interval: 60000})),
ACCSABER: initQueue(createAccSaberApiQueue({concurrency: 2, timeout: 10000})),
SCORESABER_API: initQueue(
createScoreSaberApiQueue({ concurrency: 3, timeout: 95000 }),
),
SCORESABER_PAGE: initQueue(
createScoreSaberPageQueue({ concurrency: 3, timeout: 30000 }),
),
BEATMAPS: initQueue(
createBeatMapsApiQueue({
concurrency: 1,
timeout: 10000,
intervalCap: 10,
interval: 1000,
}),
),
BEATSAVIOR: initQueue(
createBeatSaviorApiQueue({
concurrency: 1,
timeout: 10000,
intervalCap: 60,
interval: 60000,
}),
),
TWITCH: initQueue(
createTwitchApiQueue({
concurrency: 8,
timeout: 8000,
intervalCap: 800,
interval: 60000,
}),
),
ACCSABER: initQueue(
createAccSaberApiQueue({ concurrency: 2, timeout: 10000 }),
),
PRIORITY,
}
};

@ -1,35 +1,79 @@
import {default as createQueue, PRIORITY} from '../http-queue';
import {substituteVars} from '../../../utils/format'
import {PLAYER_SCORES_PER_PAGE, PLAYERS_PER_PAGE} from '../../../utils/scoresaber/consts'
import { default as createQueue, PRIORITY } from "../http-queue";
import { substituteVars } from "../../../utils/format";
import {
PLAYER_SCORES_PER_PAGE,
PLAYERS_PER_PAGE,
} from "../../../utils/scoresaber/consts";
export const SS_API_HOST = 'https://new.scoresaber.com';
export const SS_API_HOST = "https://new.scoresaber.com";
export const SS_API_URL = `${SS_API_HOST}/api`;
export const SS_API_PLAYER_INFO_URL = SS_API_URL + '/player/${playerId}/full';
export const SS_API_RECENT_SCORES_URL = SS_API_URL + '/player/${playerId}/scores/recent/${page}';
export const SS_API_TOP_SCORES_URL = SS_API_URL + '/player/${playerId}/scores/top/${page}';
export const SS_API_FIND_PLAYER_URL = SS_API_URL + '/players/by-name/${query}'
export const SS_API_RANKING_GLOBAL_URL = SS_API_URL + '/players/${page}'
export const SS_API_RANKING_GLOBAL_PAGES_URL = SS_API_URL + '/players/pages'
export const SS_API_PLAYER_INFO_URL = SS_API_URL + "/player/${playerId}/full";
export const SS_API_RECENT_SCORES_URL =
SS_API_URL + "/player/${playerId}/scores/recent/${page}";
export const SS_API_TOP_SCORES_URL =
SS_API_URL + "/player/${playerId}/scores/top/${page}";
export const SS_API_FIND_PLAYER_URL = SS_API_URL + "/players/by-name/${query}";
export const SS_API_RANKING_GLOBAL_URL = SS_API_URL + "/players/${page}";
export const SS_API_RANKING_GLOBAL_PAGES_URL = SS_API_URL + "/players/pages";
export default (options = {}) => {
const queue = createQueue(options);
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
const fetchScores = async (baseUrl, playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(baseUrl, {playerId, page}), options, priority);
const fetchScores = async (
baseUrl,
playerId,
page = 1,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(substituteVars(baseUrl, { playerId, page }), options, priority);
const player = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_PLAYER_INFO_URL, {playerId}), options, priority);
const player = async (playerId, priority = PRIORITY.FG_LOW, options = {}) =>
fetchJson(
substituteVars(SS_API_PLAYER_INFO_URL, { playerId }),
options,
priority,
);
const recentScores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchScores(SS_API_RECENT_SCORES_URL, playerId, page, priority, options);
const recentScores = async (
playerId,
page = 1,
priority = PRIORITY.FG_LOW,
options = {},
) => fetchScores(SS_API_RECENT_SCORES_URL, playerId, page, priority, options);
const topScores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchScores(SS_API_TOP_SCORES_URL, playerId, page, priority, options);
const topScores = async (
playerId,
page = 1,
priority = PRIORITY.FG_LOW,
options = {},
) => fetchScores(SS_API_TOP_SCORES_URL, playerId, page, priority, options);
const findPlayer = async (query, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_FIND_PLAYER_URL, {query: encodeURIComponent(query)}), options, priority);
const findPlayer = async (query, priority = PRIORITY.FG_LOW, options = {}) =>
fetchJson(
substituteVars(SS_API_FIND_PLAYER_URL, {
query: encodeURIComponent(query),
}),
options,
priority,
);
const rankingGlobal = async (page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_RANKING_GLOBAL_URL, {page}), options, priority);
const rankingGlobal = async (
page = 1,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(
substituteVars(SS_API_RANKING_GLOBAL_URL, { page }),
options,
priority,
);
const rankingGlobalPages = async (priority = PRIORITY.FG_LOW, options = {}) => fetchJson(SS_API_RANKING_GLOBAL_PAGES_URL, options, priority);
const rankingGlobalPages = async (priority = PRIORITY.FG_LOW, options = {}) =>
fetchJson(SS_API_RANKING_GLOBAL_PAGES_URL, options, priority);
return {
player,
@ -42,5 +86,5 @@ export default (options = {}) => {
PLAYER_SCORES_PER_PAGE,
PLAYERS_PER_PAGE,
...queueToReturn,
}
}
};
};

@ -24,7 +24,7 @@ export const parseSsInt = (text) => {
export const parseSsFloat = (text) =>
text
? parseFloat(
getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, ""))
getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, "")),
)
: null;
@ -78,32 +78,32 @@ export default (options = {}) => {
const rankeds = async (
page = 1,
priority = PRIORITY.BG_NORMAL,
options = {}
options = {},
) =>
fetchJson(substituteVars(RANKEDS_URL, { page }), options, priority).then(
(r) => {
r.body = processRankeds(r.body);
return r;
}
},
);
const processPlayerProfile = (playerId, doc) => {
cfDecryptEmail(doc);
let avatar = getImgUrl(
opt(doc.querySelector(".column.avatar img"), "src", null)
opt(doc.querySelector(".column.avatar img"), "src", null),
);
let playerName = opt(
doc.querySelector(".content .column:not(.avatar) .title a"),
"innerText"
"innerText",
);
playerName = playerName ? playerName.trim() : null;
let country = getFirstRegexpMatch(
/^.*?\/flags\/([^.]+)\..*$/,
opt(doc.querySelector(".content .column .title img"), "src")
opt(doc.querySelector(".content .column .title img"), "src"),
);
country = country ? country.toUpperCase() : null;
@ -111,8 +111,8 @@ export default (options = {}) => {
opt(
doc.querySelector(".pagination .pagination-list li a.is-current"),
"innerText",
null
)
null,
),
);
pageNum = !isNaN(pageNum) ? pageNum : null;
@ -120,8 +120,8 @@ export default (options = {}) => {
opt(
doc.querySelector(".pagination .pagination-list li:last-of-type"),
"innerText",
null
)
null,
),
);
pageQty = !isNaN(pageQty) ? pageQty : null;
@ -130,31 +130,31 @@ export default (options = {}) => {
/^\s*<strong>(?:[^:]+)\s*:?\s*<\/strong>\s*(.*)$/,
opt(
doc.querySelector(
".columns .column:not(.is-narrow) ul li:nth-of-type(3)"
".columns .column:not(.is-narrow) ul li:nth-of-type(3)",
),
"innerHTML",
),
),
"innerHTML"
)
)
);
totalItems = !isNaN(totalItems) ? totalItems : 0;
let playerRank = parseSsInt(
opt(
doc.querySelector(
".content .column ul li:first-of-type a:first-of-type"
".content .column ul li:first-of-type a:first-of-type",
),
"innerText",
),
"innerText"
)
);
playerRank = !isNaN(playerRank) ? playerRank : null;
let countryRank = parseSsInt(
opt(
doc.querySelector(
'.content .column ul li:first-of-type a[href^="/global?country="]'
'.content .column ul li:first-of-type a[href^="/global?country="]',
),
"innerText",
),
"innerText"
)
);
countryRank = !isNaN(countryRank) ? countryRank : null;
@ -170,7 +170,7 @@ export default (options = {}) => {
[...doc.querySelectorAll(".content .column ul li")]
.map((li) => {
const matches = li.innerHTML.match(
/^\s*<strong>([^:]+)\s*:?\s*<\/strong>\s*(.*)$/
/^\s*<strong>([^:]+)\s*:?\s*<\/strong>\s*(.*)$/,
);
if (!matches) return null;
@ -219,7 +219,7 @@ export default (options = {}) => {
const item = mapping.find((m) => m.key === matches[1]);
return item ? { ...item, value } : { label: matches[1], value };
})
.filter((s) => s)
.filter((s) => s),
)
.reduce(
(cum, item) => {
@ -255,7 +255,7 @@ export default (options = {}) => {
return cum;
},
{ inactiveAccount: false, bannedAccount: false }
{ inactiveAccount: false, bannedAccount: false },
);
const scores = [...doc.querySelectorAll("table.ranking tbody tr")].map(
@ -274,7 +274,7 @@ export default (options = {}) => {
if (song) {
const leaderboardId = parseInt(
getFirstRegexpMatch(/leaderboard\/(\d+)/, song.href),
10
10,
);
ret.leaderboardId = leaderboardId ? leaderboardId : null;
} else {
@ -293,7 +293,7 @@ export default (options = {}) => {
.replace(/&amp;/g, "&")
.replace(
/<span class="__cf_email__" data-cfemail="[^"]+">\[email&nbsp;protected]<\/span>/g,
""
"",
)
.match(/^(.*?)\s*<span[^>]+>(.*?)<\/span>/)
: null;
@ -328,7 +328,7 @@ export default (options = {}) => {
ret.timeSet = songDate ? dateFromString(songDate.title) : null;
const pp = parseSsFloat(
opt(tr.querySelector("th.score .scoreTop.ppValue"), "innerText")
opt(tr.querySelector("th.score .scoreTop.ppValue"), "innerText"),
);
ret.pp = !isNaN(pp) ? pp : null;
@ -337,9 +337,9 @@ export default (options = {}) => {
/^\(([0-9.]+)pp\)$/,
opt(
tr.querySelector("th.score .scoreTop.ppWeightedValue"),
"innerText"
)
)
"innerText",
),
),
);
ret.ppWeighted = !isNaN(ppWeighted) ? ppWeighted : null;
@ -380,7 +380,7 @@ export default (options = {}) => {
}
return ret;
}
},
);
const recentPlay =
scores && scores.length && scores[0].timeSet ? scores[0].timeSet : null;
@ -394,18 +394,18 @@ export default (options = {}) => {
externalProfileUrl: opt(
doc.querySelector(".content .column:not(.avatar) .title a"),
"href",
null
null,
),
history: getFirstRegexpMatch(
/data:\s*\[([0-9,]+)\]/,
doc.body.innerHTML
doc.body.innerHTML,
),
country,
badges: [...doc.querySelectorAll(".column.avatar center img")].map(
(img) => ({
image: getImgUrl(img.src),
description: img.title,
})
}),
),
rank: stats.rank ? stats.rank : null,
countryRank: stats.countryRank ? stats.countryRank : null,
@ -435,7 +435,7 @@ export default (options = {}) => {
fetchHtml(
substituteVars(PLAYER_PROFILE_URL, { playerId }),
options,
priority
priority,
).then((r) => {
r.body = processPlayerProfile(playerId, r.body);
@ -451,17 +451,17 @@ export default (options = {}) => {
const id = getFirstRegexpMatch(/\/(\d+)$/, a.href);
const avatar = getImgUrl(
opt(tr.querySelector("td.picture img"), "src", null)
opt(tr.querySelector("td.picture img"), "src", null),
);
let country = getFirstRegexpMatch(
/^.*?\/flags\/([^.]+)\..*$/,
opt(tr.querySelector("td.player img"), "src", null)
opt(tr.querySelector("td.player img"), "src", null),
);
country = country ? country.toUpperCase() : null;
let difference = parseSsInt(
opt(tr.querySelector("td.diff"), "innerText", null)
opt(tr.querySelector("td.diff"), "innerText", null),
);
difference = !isNaN(difference) ? difference : null;
@ -469,15 +469,15 @@ export default (options = {}) => {
playerName = playerName || playerName === "" ? playerName.trim() : null;
let pp = parseSsFloat(
opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText")
opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText"),
);
pp = !isNaN(pp) ? pp : null;
let rank = parseSsInt(
getFirstRegexpMatch(
/^\s*#(\d+)\s*$/,
opt(tr.querySelector("td.rank"), "innerText", null)
)
opt(tr.querySelector("td.rank"), "innerText", null),
),
);
rank = !isNaN(rank) ? rank : null;
@ -491,7 +491,7 @@ export default (options = {}) => {
pp,
rank,
};
}
},
);
return { players: data };
@ -501,12 +501,12 @@ export default (options = {}) => {
country,
page = 1,
priority = PRIORITY.FG_LOW,
options = {}
options = {},
) =>
fetchHtml(
substituteVars(COUNTRY_RANKING_URL, { country, page }),
options,
priority
priority,
).then((r) => {
r.body = processCountryRanking(country, r.body);
@ -529,11 +529,11 @@ export default (options = {}) => {
};
ret.player.playerInfo.avatar = getImgUrl(
opt(tr.querySelector(".picture img"), "src", null)
opt(tr.querySelector(".picture img"), "src", null),
);
ret.score.rank = parseSsInt(
opt(tr.querySelector("td.rank"), "innerText")
opt(tr.querySelector("td.rank"), "innerText"),
);
if (isNaN(ret.score.rank)) ret.score.rank = null;
@ -541,7 +541,7 @@ export default (options = {}) => {
if (player) {
let country = getFirstRegexpMatch(
/^.*?\/flags\/([^.]+)\..*$/,
opt(player.querySelector("img"), "src", "")
opt(player.querySelector("img"), "src", ""),
);
country = country ? country.toUpperCase() : null;
if (country) {
@ -551,14 +551,14 @@ export default (options = {}) => {
ret.player.name = opt(
player.querySelector("span.songTop.pp"),
"innerText"
"innerText",
);
ret.player.name = ret.player.name
? ret.player.name.trim().replace("&#039;", "'")
: null;
ret.player.playerId = getFirstRegexpMatch(
/\/u\/(\d+)((\?|&|#).*)?$/,
opt(player, "href", "")
opt(player, "href", ""),
);
ret.player.playerId = ret.player.playerId
? ret.player.playerId.trim()
@ -574,7 +574,7 @@ export default (options = {}) => {
ret.score.timeSetString = opt(
tr.querySelector("td.timeset"),
"innerText",
null
null,
);
if (ret.score.timeSetString)
ret.score.timeSetString = ret.score.timeSetString.trim();
@ -602,7 +602,7 @@ export default (options = {}) => {
const diffs = [...doc.querySelectorAll(".tabs ul li a")].map((a) => {
let leaderboardId = parseInt(
getFirstRegexpMatch(/leaderboard\/(\d+)$/, a.href),
10
10,
);
if (isNaN(leaderboardId)) leaderboardId = null;
@ -615,7 +615,7 @@ export default (options = {}) => {
const currentDiffHuman = opt(
doc.querySelector(".tabs li.is-active a span"),
"innerText",
null
null,
);
let diff = null;
@ -628,20 +628,20 @@ export default (options = {}) => {
const songName = opt(
doc.querySelector(
".column.is-one-third-desktop .box:first-of-type .title a"
".column.is-one-third-desktop .box:first-of-type .title a",
),
"innerText",
null
null,
);
const imageUrl = getImgUrl(
opt(
doc.querySelector(
".column.is-one-third-desktop .box:first-of-type .columns .column.is-one-quarter img"
".column.is-one-third-desktop .box:first-of-type .columns .column.is-one-quarter img",
),
"src",
null
)
null,
),
);
const songInfo = [
@ -656,13 +656,13 @@ export default (options = {}) => {
]
.map((sid) => {
let songInfoBox = doc.querySelector(
".column.is-one-third-desktop .box:first-of-type"
".column.is-one-third-desktop .box:first-of-type",
);
return {
...sid,
value: songInfoBox
? songInfoBox.innerHTML.match(
new RegExp(sid.label + ":\\s*<b>(.*?)</b>", "i")
new RegExp(sid.label + ":\\s*<b>(.*?)</b>", "i"),
)
: null,
};
@ -708,7 +708,7 @@ export default (options = {}) => {
return cum;
},
{ imageUrl, stats: {} }
{ imageUrl, stats: {} },
);
const { stats, ...song } = songInfo;
@ -718,9 +718,9 @@ export default (options = {}) => {
opt(
doc.querySelector(".pagination .pagination-list li:last-of-type"),
"innerText",
null
null,
),
10
10,
);
if (isNaN(pageQty)) pageQty = null;
@ -736,7 +736,7 @@ export default (options = {}) => {
let diffChartText = getFirstRegexpMatch(
/'difficulty',\s*([0-9.,\s]+)\s*\]/,
doc.body.innerHTML
doc.body.innerHTML,
);
let diffChart = (diffChartText ? diffChartText : "")
.split(",")
@ -758,12 +758,12 @@ export default (options = {}) => {
leaderboardId,
page = 1,
priority = PRIORITY.FG_LOW,
options = {}
options = {},
) =>
fetchHtml(
substituteVars(LEADERBOARD_URL, { leaderboardId, page }),
options,
priority
priority,
).then((r) => {
r.body = processLeaderboard(leaderboardId, page, r.body);

@ -1,44 +1,104 @@
import {default as createQueue, PRIORITY} from '../http-queue';
import ssrConfig from '../../../ssr-config'
import { default as createQueue, PRIORITY } from "../http-queue";
import ssrConfig from "../../../ssr-config";
import { substituteVars } from "../../../utils/format";
const CLIENT_ID = 'u0swxz56n4iumc634at1osoqdk31qt';
const CLIENT_ID = "u0swxz56n4iumc634at1osoqdk31qt";
const TWITCH_AUTH_URL = 'https://id.twitch.tv/oauth2'
const AUTHORIZATION_URL = `${TWITCH_AUTH_URL}/authorize?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(ssrConfig.domain + '/twitch')}&response_type=token` + '&scope=${scopes}&state=${state}';
const VALIDATE_URL = `${TWITCH_AUTH_URL}/validate`
const TWITCH_AUTH_URL = "https://id.twitch.tv/oauth2";
const AUTHORIZATION_URL =
`${TWITCH_AUTH_URL}/authorize?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
ssrConfig.domain + "/twitch",
)}&response_type=token` + "&scope=${scopes}&state=${state}";
const VALIDATE_URL = `${TWITCH_AUTH_URL}/validate`;
const TWITCH_API_URL = 'https://api.twitch.tv/helix';
const PROFILE_URL = TWITCH_API_URL + '/users?login=${login}';
const VIDEOS_URL = TWITCH_API_URL + '/videos?user_id=${userId}&type=${type}&first=100';
const STREAMS_URL = TWITCH_API_URL + '/streams?user_id=${userId}';
const TWITCH_API_URL = "https://api.twitch.tv/helix";
const PROFILE_URL = TWITCH_API_URL + "/users?login=${login}";
const VIDEOS_URL =
TWITCH_API_URL + "/videos?user_id=${userId}&type=${type}&first=100";
const STREAMS_URL = TWITCH_API_URL + "/streams?user_id=${userId}";
export default (options = {}) => {
const queue = createQueue(options);
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
const fetchApi = (url, accessToken, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(
const fetchApi = (
url,
accessToken,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(
url,
{
...options,
headers: {
'Client-ID': CLIENT_ID,
'Authorization': `Bearer ${accessToken}`
}
"Client-ID": CLIENT_ID,
Authorization: `Bearer ${accessToken}`,
},
},
priority,
)
);
const getAuthUrl = (state = '', scopes = '') => substituteVars(AUTHORIZATION_URL, {state: encodeURIComponent(state), scopes: encodeURIComponent(scopes)});
const getAuthUrl = (state = "", scopes = "") =>
substituteVars(AUTHORIZATION_URL, {
state: encodeURIComponent(state),
scopes: encodeURIComponent(scopes),
});
const validateToken = async (accessToken, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(VALIDATE_URL, {...options, headers: {'Authorization': `OAuth ${accessToken}`}}, priority)
const validateToken = async (
accessToken,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchJson(
VALIDATE_URL,
{ ...options, headers: { Authorization: `OAuth ${accessToken}` } },
priority,
);
const profile = async (accessToken, login, priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(PROFILE_URL, {login: encodeURIComponent(login)}), accessToken, priority, options)
const profile = async (
accessToken,
login,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchApi(
substituteVars(PROFILE_URL, { login: encodeURIComponent(login) }),
accessToken,
priority,
options,
);
const videos = async (accessToken, userId, type = 'archive', priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(VIDEOS_URL, {userId: encodeURIComponent(userId), type: encodeURIComponent(type)}), accessToken, priority, options)
const videos = async (
accessToken,
userId,
type = "archive",
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchApi(
substituteVars(VIDEOS_URL, {
userId: encodeURIComponent(userId),
type: encodeURIComponent(type),
}),
accessToken,
priority,
options,
);
const streams = async (accessToken, userId, priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(STREAMS_URL, {userId: encodeURIComponent(userId)}), accessToken, priority, options)
const streams = async (
accessToken,
userId,
priority = PRIORITY.FG_LOW,
options = {},
) =>
fetchApi(
substituteVars(STREAMS_URL, { userId: encodeURIComponent(userId) }),
accessToken,
priority,
options,
);
return {
getAuthUrl,
@ -47,5 +107,5 @@ export default (options = {}) => {
videos,
streams,
...queueToReturn,
}
}
};
};

@ -1,13 +1,13 @@
export const parseRateLimitHeaders = response => {
export const parseRateLimitHeaders = (response) => {
if (!response || !response.headers) return null;
const remaining = parseInt(response.headers.get('x-ratelimit-remaining'), 10);
const limit = parseInt(response.headers.get('x-ratelimit-limit'), 10);
const resetAt = parseInt(response.headers.get('x-ratelimit-reset'), 10);
const remaining = parseInt(response.headers.get("x-ratelimit-remaining"), 10);
const limit = parseInt(response.headers.get("x-ratelimit-limit"), 10);
const resetAt = parseInt(response.headers.get("x-ratelimit-reset"), 10);
return {
remaining: !isNaN(remaining) ? remaining : null,
limit: !isNaN(limit) ? limit : null,
resetAt: !isNaN(resetAt) ? new Date(resetAt * 1000) : null,
}
}
};
};

@ -12,7 +12,7 @@ export class SsrError extends Error {
export class SsrTimeoutError extends SsrError {
constructor(timeout, message) {
super(message && message.length ? message : `Timeout Error (${timeout}ms)`)
super(message && message.length ? message : `Timeout Error (${timeout}ms)`);
this.name = "SsrTimeoutError";
this.timeout = timeout;
@ -21,7 +21,7 @@ export class SsrTimeoutError extends SsrError {
export class SsrDataFormatError extends SsrError {
constructor(message, previous = null) {
super(message && message.length ? message : `Data format error`)
super(message && message.length ? message : `Data format error`);
this.name = "SsrDataFormatError";
this.previous = previous;

@ -1,35 +1,36 @@
import {db} from '../db/db'
import queues from '../network/queues/queues';
import accSaberCategoriesApiClient from '../network/clients/accsaber/api-categories';
import accSaberRankingApiClient from '../network/clients/accsaber/api-ranking';
import accSaberScoresApiClient from '../network/clients/accsaber/api-scores';
import accSaberPlayerRankHistoryApiClient from '../network/clients/accsaber/api-player-rank-history';
import accSaberCategoriesRepository from '../db/repository/accsaber-categories'
import accSaberPlayersRepository from '../db/repository/accsaber-players'
import accSaberPlayersHistoryRepository from '../db/repository/accsaber-players-history';
import keyValueRepository from '../db/repository/key-value'
import createPlayerService from '../services/scoresaber/player';
import {capitalize, convertArrayToObjectByKey} from '../utils/js'
import log from '../utils/logger'
import { db } from "../db/db";
import queues from "../network/queues/queues";
import accSaberCategoriesApiClient from "../network/clients/accsaber/api-categories";
import accSaberRankingApiClient from "../network/clients/accsaber/api-ranking";
import accSaberScoresApiClient from "../network/clients/accsaber/api-scores";
import accSaberPlayerRankHistoryApiClient from "../network/clients/accsaber/api-player-rank-history";
import accSaberCategoriesRepository from "../db/repository/accsaber-categories";
import accSaberPlayersRepository from "../db/repository/accsaber-players";
import accSaberPlayersHistoryRepository from "../db/repository/accsaber-players-history";
import keyValueRepository from "../db/repository/key-value";
import createPlayerService from "../services/scoresaber/player";
import { capitalize, convertArrayToObjectByKey } from "../utils/js";
import log from "../utils/logger";
import {
addToDate,
toAccSaberMidnight,
formatDate,
HOUR,
MINUTE,
dateFromString, truncateDate,
} from '../utils/date'
import {PRIORITY} from '../network/queues/http-queue'
import makePendingPromisePool from '../utils/pending-promises'
import {getServicePlayerGain, serviceFilterFunc} from './utils'
import {PLAYER_SCORES_PER_PAGE} from '../utils/accsaber/consts'
import {roundToPrecision} from '../utils/format'
dateFromString,
truncateDate,
} from "../utils/date";
import { PRIORITY } from "../network/queues/http-queue";
import makePendingPromisePool from "../utils/pending-promises";
import { getServicePlayerGain, serviceFilterFunc } from "./utils";
import { PLAYER_SCORES_PER_PAGE } from "../utils/accsaber/consts";
import { roundToPrecision } from "../utils/format";
const REFRESH_INTERVAL = HOUR;
const SCORES_NETWORK_TTL = MINUTE * 5;
const HISTOGRAM_AP_PRECISION = 5;
const CATEGORIES_ORDER = ['overall', 'true', 'standard', 'tech'];
const CATEGORIES_ORDER = ["overall", "true", "standard", "tech"];
let service = null;
export default () => {
@ -40,61 +41,120 @@ export default () => {
const resolvePromiseOrWaitForPending = makePendingPromisePool();
const getCategories = async () => {
const categories = await resolvePromiseOrWaitForPending(`accSaberCategories`, () => accSaberCategoriesRepository().getAll());
const categories = await resolvePromiseOrWaitForPending(
`accSaberCategories`,
() => accSaberCategoriesRepository().getAll(),
);
const getIdx = category => {
const idx = CATEGORIES_ORDER.findIndex(v => v === category?.name);
const getIdx = (category) => {
const idx = CATEGORIES_ORDER.findIndex((v) => v === category?.name);
return idx >= 0 ? idx : 100000;
}
};
return categories.sort((a, b) => getIdx(a) - getIdx(b));
}
};
const getPlayer = async playerId => resolvePromiseOrWaitForPending(`accSaberPlayer/${playerId}`, () => accSaberPlayersRepository().getAllFromIndex('accsaber-players-playerId', playerId));
const getRanking = async (category = 'overall') => accSaberPlayersRepository().getAllFromIndex('accsaber-players-category', category);
const getPlayerHistory = async playerId => resolvePromiseOrWaitForPending(`accSaberPlayerHistory/${playerId}`, () => accSaberPlayersHistoryRepository().getAllFromIndex('accsaber-players-history-playerId', playerId))
const getPlayer = async (playerId) =>
resolvePromiseOrWaitForPending(`accSaberPlayer/${playerId}`, () =>
accSaberPlayersRepository().getAllFromIndex(
"accsaber-players-playerId",
playerId,
),
);
const getRanking = async (category = "overall") =>
accSaberPlayersRepository().getAllFromIndex(
"accsaber-players-category",
category,
);
const getPlayerHistory = async (playerId) =>
resolvePromiseOrWaitForPending(`accSaberPlayerHistory/${playerId}`, () =>
accSaberPlayersHistoryRepository().getAllFromIndex(
"accsaber-players-history-playerId",
playerId,
),
);
const isDataForPlayerAvailable = async playerId => (await Promise.all([getPlayer(playerId), getCategories()])).every(d => d?.length)
const isDataForPlayerAvailable = async (playerId) =>
(await Promise.all([getPlayer(playerId), getCategories()])).every(
(d) => d?.length,
);
const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) => getServicePlayerGain(playerHistory, toAccSaberMidnight, 'accSaberDate', daysAgo, maxDaysAgo);
const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) =>
getServicePlayerGain(
playerHistory,
toAccSaberMidnight,
"accSaberDate",
daysAgo,
maxDaysAgo,
);
const getLastUpdatedKey = type => `accSaber${capitalize(type)}LastUpdated`;
const getLastUpdated = async (type = 'all') => keyValueRepository().get(getLastUpdatedKey(type));
const setLastUpdated = async (type = 'all', date) => keyValueRepository().set(date, getLastUpdatedKey(type));
const getLastUpdatedKey = (type) => `accSaber${capitalize(type)}LastUpdated`;
const getLastUpdated = async (type = "all") =>
keyValueRepository().get(getLastUpdatedKey(type));
const setLastUpdated = async (type = "all", date) =>
keyValueRepository().set(date, getLastUpdatedKey(type));
const shouldRefresh = async (type = 'all', forceUpdate = false) => {
const shouldRefresh = async (type = "all", forceUpdate = false) => {
if (!forceUpdate) {
const lastUpdated = await getLastUpdated(type);
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
log.debug(`Refresh interval for ${type} not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'AccSaberService')
log.debug(
`Refresh interval for ${type} not yet expired, skipping. Next refresh on ${formatDate(
addToDate(REFRESH_INTERVAL, lastUpdated),
)}`,
"AccSaberService",
);
return false;
}
}
return true;
}
};
const fetchScoresPage = async (playerId, page = 1, priority = PRIORITY.FG_LOW, {...options} = {}) => {
const fetchScoresPage = async (
playerId,
page = 1,
priority = PRIORITY.FG_LOW,
{ ...options } = {},
) => {
if (!options) options = {};
if (!options.hasOwnProperty('cacheTtl')) options.cacheTtl = SCORES_NETWORK_TTL;
if (!options.hasOwnProperty("cacheTtl"))
options.cacheTtl = SCORES_NETWORK_TTL;
const categoriesByDisplayName = convertArrayToObjectByKey(await getCategories(), 'displayName');
const categoriesByDisplayName = convertArrayToObjectByKey(
await getCategories(),
"displayName",
);
return (await resolvePromiseOrWaitForPending(`fetchPlayerScores/${playerId}/${page}`, () => accSaberScoresApiClient.getProcessed({...options, playerId, page, priority})))
.map(s => ({
return (
await resolvePromiseOrWaitForPending(
`fetchPlayerScores/${playerId}/${page}`,
() =>
accSaberScoresApiClient.getProcessed({
...options,
playerId,
page,
priority,
}),
)
).map((s) => ({
...s,
leaderboard: {
...s?.leaderboard,
category: categoriesByDisplayName[s?.leaderboard?.categoryDisplayName]?.name ?? null,
}
}))
}
category:
categoriesByDisplayName[s?.leaderboard?.categoryDisplayName]?.name ??
null,
},
}));
};
const getScoresHistogramDefinition = (serviceParams = {type: 'overall', sort: 'ap', order: 'desc'}) => {
const scoreType = serviceParams?.type ?? 'overall';
const sort = serviceParams?.sort ?? 'ap';
const order = serviceParams?.order ?? 'desc';
const getScoresHistogramDefinition = (
serviceParams = { type: "overall", sort: "ap", order: "desc" },
) => {
const scoreType = serviceParams?.type ?? "overall";
const sort = serviceParams?.sort ?? "ap";
const order = serviceParams?.order ?? "desc";
const commonFilterFunc = serviceFilterFunc(serviceParams);
@ -104,68 +164,75 @@ export default () => {
let maxBucketSize = null;
let bucketSizeStep = null;
let bucketSizeValues = null;
let type = 'linear';
let valFunc = s => s;
let filterFunc = s => commonFilterFunc(s) && (scoreType === 'overall' || s?.leaderboard?.category === scoreType);
let histogramFilterFunc = s => s;
let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear'
let type = "linear";
let valFunc = (s) => s;
let filterFunc = (s) =>
commonFilterFunc(s) &&
(scoreType === "overall" || s?.leaderboard?.category === scoreType);
let histogramFilterFunc = (s) => s;
let roundedValFunc = (s, type = type, precision = bucketSize) =>
type === "linear"
? roundToPrecision(valFunc(s), precision)
: truncateDate(valFunc(s), precision);
let prefix = '';
let prefixLong = '';
let suffix = '';
let suffixLong = '';
let prefix = "";
let prefixLong = "";
let suffix = "";
let suffixLong = "";
switch (sort) {
case 'ap':
valFunc = s => s?.ap;
type = 'linear';
case "ap":
valFunc = (s) => s?.ap;
type = "linear";
bucketSize = HISTOGRAM_AP_PRECISION;
minBucketSize = 1;
maxBucketSize = 100;
bucketSizeStep = 1;
round = 0;
suffix = ' AP';
suffixLong = ' AP';
suffix = " AP";
suffixLong = " AP";
break;
case 'recent':
valFunc = s => s?.timeSet;
type = 'time';
bucketSize = 'day'
case "recent":
valFunc = (s) => s?.timeSet;
type = "time";
bucketSize = "day";
break;
case 'acc':
valFunc = s => s?.acc;
type = 'linear';
case "acc":
valFunc = (s) => s?.acc;
type = "linear";
bucketSize = 0.05;
minBucketSize = 0.05;
maxBucketSize = 1;
bucketSizeStep = 0.05;
round = 2;
suffix = '%';
suffixLong = '%';
suffix = "%";
suffixLong = "%";
break;
case 'rank':
valFunc = s => s?.score?.rank;
type = 'linear';
case "rank":
valFunc = (s) => s?.score?.rank;
type = "linear";
bucketSize = 5;
minBucketSize = 1;
maxBucketSize = 100;
bucketSizeStep = 1;
round = 0;
prefix = '';
prefixLong = '#';
prefix = "";
prefixLong = "#";
break;
}
return {
getValue: valFunc,
getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize),
getRoundedValue:
(bucketSize = bucketSize) =>
(s) =>
roundedValFunc(s, type, bucketSize),
filter: filterFunc,
histogramFilter: histogramFilterFunc,
sort: (a, b) => order === 'asc' ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a),
sort: (a, b) =>
order === "asc" ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a),
type,
bucketSize,
minBucketSize,
@ -177,36 +244,38 @@ export default () => {
prefixLong,
suffix,
suffixLong,
order
}
}
order,
};
};
const getPlayerScores = async playerId => {
const getPlayerScores = async (playerId) => {
try {
return fetchScoresPage(playerId, 1);
}
catch (err) {
} catch (err) {
return [];
}
}
};
const getPlayerScoresPage = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}) => {
const getPlayerScoresPage = async (
playerId,
serviceParams = { sort: "recent", order: "desc", page: 1 },
) => {
let page = serviceParams?.page ?? 1;
if (page < 1) page = 1;
let playerScores;
try {
playerScores = await fetchScoresPage(playerId, page);
}
catch (err) {
} catch (err) {
return { total: 0, scores: [] };
}
if (!playerScores?.length) return { total: 0, scores: [] };
const {sort: sortFunc, filter: filterFunc} = getScoresHistogramDefinition(serviceParams);
const { sort: sortFunc, filter: filterFunc } =
getScoresHistogramDefinition(serviceParams);
playerScores = playerScores.filter(filterFunc).sort(sortFunc)
playerScores = playerScores.filter(filterFunc).sort(sortFunc);
const startIdx = (page - 1) * PLAYER_SCORES_PER_PAGE;
if (playerScores.length < startIdx + 1) return { total: 0, scores: [] };
@ -214,127 +283,185 @@ export default () => {
return {
total: playerScores.length,
itemsPerPage: PLAYER_SCORES_PER_PAGE,
scores: playerScores
.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE)
}
}
scores: playerScores.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE),
};
};
const fetchPlayerRankHistory = async (playerId, priority = PRIORITY.FG_LOW, {...options} = {}) => {
const fetchPlayerRankHistory = async (
playerId,
priority = PRIORITY.FG_LOW,
{ ...options } = {},
) => {
if (!options) options = {};
if (!options.hasOwnProperty('cacheTtl')) options.cacheTtl = SCORES_NETWORK_TTL;
if (!options.hasOwnProperty("cacheTtl"))
options.cacheTtl = SCORES_NETWORK_TTL;
return accSaberPlayerRankHistoryApiClient.getProcessed({...options, playerId, priority});
}
return accSaberPlayerRankHistoryApiClient.getProcessed({
...options,
playerId,
priority,
});
};
const refreshCategories = async (forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
log.debug(`Starting AccSaber categories refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
const refreshCategories = async (
forceUpdate = false,
priority = queues.PRIORITY.BG_NORMAL,
throwErrors = false,
) => {
log.debug(
`Starting AccSaber categories refreshing${
forceUpdate ? " (forced)" : ""
}...`,
"AccSaberService",
);
try {
log.trace(`Fetching categories from DB...`, 'AccSaberService');
log.trace(`Fetching categories from DB...`, "AccSaberService");
const dbCategories = await getCategories();
log.trace(`DB categories fetched`, 'AccSaberService', dbCategories);
log.trace(`DB categories fetched`, "AccSaberService", dbCategories);
if (!await shouldRefresh('categories', forceUpdate)) return {changed: [], all: dbCategories};
if (!(await shouldRefresh("categories", forceUpdate)))
return { changed: [], all: dbCategories };
log.trace(`Fetching current categories from AccSaber...`, 'AccSaberService');
log.trace(
`Fetching current categories from AccSaber...`,
"AccSaberService",
);
let categories = await accSaberCategoriesApiClient.getProcessed({priority});
let categories = await accSaberCategoriesApiClient.getProcessed({
priority,
});
if (!categories || !categories.length) {
log.warn(`AccSaber returned empty categories list`, 'AccSaberService')
log.warn(`AccSaber returned empty categories list`, "AccSaberService");
return null;
}
categories = categories.concat([{
name: 'overall',
displayName: 'Overall',
categories = categories.concat([
{
name: "overall",
displayName: "Overall",
countsTowardsOverall: null,
description: 'Overall'
}]);
description: "Overall",
},
]);
log.trace(`Categories fetched`, 'AccSaberService', categories);
log.trace(`Categories fetched`, "AccSaberService", categories);
const dbCategoriesNames = dbCategories.map(c => c.name);
const newCategories = categories.filter(c => !dbCategories || !dbCategoriesNames.includes(c.name));
const dbCategoriesNames = dbCategories.map((c) => c.name);
const newCategories = categories.filter(
(c) => !dbCategories || !dbCategoriesNames.includes(c.name),
);
if (newCategories && newCategories.length)
log.debug(`${newCategories.length} new categories found`, 'AccSaberService');
log.debug(
`${newCategories.length} new categories found`,
"AccSaberService",
);
await db.runInTransaction(['accsaber-categories', 'key-value'], async tx => {
const newCategoriesNames = categories.map(c => c.name);
await db.runInTransaction(
["accsaber-categories", "key-value"],
async (tx) => {
const newCategoriesNames = categories.map((c) => c.name);
const accSaberCategoriesStore = tx.objectStore('accsaber-categories');
const accSaberCategoriesStore = tx.objectStore("accsaber-categories");
let cursor = await accSaberCategoriesStore.openCursor();
log.trace(`Remove old categories from DB`, 'AccSaberService');
log.trace(`Remove old categories from DB`, "AccSaberService");
while (cursor) {
const category = cursor.value;
if (!newCategoriesNames.includes(category.name)) await cursor.delete();
if (!newCategoriesNames.includes(category.name))
await cursor.delete();
cursor = await cursor.continue();
}
log.trace(`Old categories removed from DB`, 'AccSaberService');
log.trace(`Old categories removed from DB`, "AccSaberService");
log.trace(`Updating categories in DB...`, 'AccSaberService');
log.trace(`Updating categories in DB...`, "AccSaberService");
await Promise.all(categories.map(async c => accSaberCategoriesStore.put(c)));
await Promise.all(
categories.map(async (c) => accSaberCategoriesStore.put(c)),
);
log.trace(`Categories updated`, 'AccSaberService');
log.trace(`Categories updated`, "AccSaberService");
log.trace(`Updating categories last update date in DB...`, 'AccSaberService');
log.trace(
`Updating categories last update date in DB...`,
"AccSaberService",
);
await tx.objectStore('key-value').put(new Date(), getLastUpdatedKey('categories'));
await tx
.objectStore("key-value")
.put(new Date(), getLastUpdatedKey("categories"));
log.debug(`Categories last update date updated`, 'AccSaberService');
});
log.debug(`Categories last update date updated`, "AccSaberService");
},
);
accSaberCategoriesRepository().addToCache(categories);
keyValueRepository().setCache(getLastUpdatedKey('categories'), new Date());
keyValueRepository().setCache(
getLastUpdatedKey("categories"),
new Date(),
);
log.debug(`Categories refreshing completed`, 'AccSaberService');
log.debug(`Categories refreshing completed`, "AccSaberService");
return { changed: newCategories, all: categories };
}
catch(e) {
} catch (e) {
if (throwErrors) throw e;
log.debug(`Categories refreshing error`, 'AccSaberService', e)
log.debug(`Categories refreshing error`, "AccSaberService", e);
return null;
}
}
};
const updatePlayerHistory = async player => {
const updatePlayerHistory = async (player) => {
if (!player?.playerId) return;
try {
log.debug(`Updating player ${player.playerId} history`, 'AccSaberService');
log.debug(
`Updating player ${player.playerId} history`,
"AccSaberService",
);
const accSaberDate = toAccSaberMidnight(new Date());
const playerIdTimestamp = `${player.playerId}_${accSaberDate.getTime()}`;
const existingData = await accSaberPlayersHistoryRepository().get(playerIdTimestamp);
const existingData =
await accSaberPlayersHistoryRepository().get(playerIdTimestamp);
const lastUpdated = dateFromString(existingData?.lastUpdated);
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
log.debug(`Refresh interval for player ${player.playerId} history not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'AccSaberService')
log.debug(
`Refresh interval for player ${
player.playerId
} history not yet expired, skipping. Next refresh on ${formatDate(
addToDate(REFRESH_INTERVAL, lastUpdated),
)}`,
"AccSaberService",
);
return;
}
const categories = (await getCategories())?.map(c => c.name) ?? null;
const categories = (await getCategories())?.map((c) => c.name) ?? null;
if (!categories) {
log.trace(`No categories found, skip updating player ${player.playerId} history.`);
log.trace(
`No categories found, skip updating player ${player.playerId} history.`,
);
return;
}
let accStats = {};
for (const category of categories) {
const playerAccInfo = (await getRanking(category) ?? []).find(p => p.playerId === player.playerId);
const playerAccInfo = ((await getRanking(category)) ?? []).find(
(p) => p.playerId === player.playerId,
);
if (!playerAccInfo) continue;
const {
@ -358,109 +485,175 @@ export default () => {
accSaberDate,
lastUpdated: new Date(),
playerIdTimestamp,
categories: accStats
}
categories: accStats,
};
await accSaberPlayersHistoryRepository().set(stats);
} else {
log.trace(`No Acc Saber data for player ${player.playerId}, skipping history updating.`, 'AccSaberService');
log.trace(
`No Acc Saber data for player ${player.playerId}, skipping history updating.`,
"AccSaberService",
);
return;
}
log.debug(`Player ${player.playerId} history updated`, 'AccSaberService');
}
catch(e) {
log.debug(`Player ${player.playerId} history updating error.`, 'AccSaberService', e);
}
log.debug(`Player ${player.playerId} history updated`, "AccSaberService");
} catch (e) {
log.debug(
`Player ${player.playerId} history updating error.`,
"AccSaberService",
e,
);
}
};
const refreshRanking = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
log.debug(`Starting AccSaber ${category} ranking refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
const refreshRanking = async (
category = "overall",
forceUpdate = false,
priority = queues.PRIORITY.BG_NORMAL,
throwErrors = false,
) => {
log.debug(
`Starting AccSaber ${category} ranking refreshing${
forceUpdate ? " (forced)" : ""
}...`,
"AccSaberService",
);
try {
log.trace(`Fetching ${category} ranking from DB...`, 'AccSaberService');
log.trace(`Fetching ${category} ranking from DB...`, "AccSaberService");
const dbRanking = await getRanking(category);
log.trace(`DB ${category} ranking fetched`, 'AccSaberService', dbRanking);
log.trace(`DB ${category} ranking fetched`, "AccSaberService", dbRanking);
const rankingType = `${category}Ranking`
const rankingType = `${category}Ranking`;
if (!await shouldRefresh(rankingType, forceUpdate)) return dbRanking.sort((a, b) => a.rank - b.rank);
if (!(await shouldRefresh(rankingType, forceUpdate)))
return dbRanking.sort((a, b) => a.rank - b.rank);
log.trace(`Fetching current ${category} ranking from AccSaber...`, 'AccSaberService');
log.trace(
`Fetching current ${category} ranking from AccSaber...`,
"AccSaberService",
);
const ranking = await accSaberRankingApiClient.getProcessed({category, priority});
const ranking = await accSaberRankingApiClient.getProcessed({
category,
priority,
});
if (!ranking || !ranking.length) {
log.warn(`AccSaber returned empty ${category} ranking`, 'AccSaberService')
log.warn(
`AccSaber returned empty ${category} ranking`,
"AccSaberService",
);
return null;
}
log.trace(`${capitalize(category)} ranking fetched`, 'AccSaberService', ranking);
log.trace(
`${capitalize(category)} ranking fetched`,
"AccSaberService",
ranking,
);
log.trace(`Updating ${category} ranking...`, 'AccSaberService');
log.trace(`Updating ${category} ranking...`, "AccSaberService");
await db.runInTransaction(['accsaber-players', 'key-value'], async tx => {
const newPlayerIds = ranking.map(c => c.playerId);
await db.runInTransaction(
["accsaber-players", "key-value"],
async (tx) => {
const newPlayerIds = ranking.map((c) => c.playerId);
const accSaberPlayersStore = tx.objectStore('accsaber-players');
const accSaberPlayersStore = tx.objectStore("accsaber-players");
let cursor = await accSaberPlayersStore.openCursor();
log.trace(`Remove old players from DB for category ${category}`, 'AccSaberService');
log.trace(
`Remove old players from DB for category ${category}`,
"AccSaberService",
);
while (cursor) {
const player = cursor.value;
if (player.category === category && !newPlayerIds.includes(player.playerId)) await cursor.delete();
if (
player.category === category &&
!newPlayerIds.includes(player.playerId)
)
await cursor.delete();
cursor = await cursor.continue();
}
log.trace(`Old players removed from DB`, 'AccSaberService');
log.trace(`Old players removed from DB`, "AccSaberService");
log.trace(`Updating players in DB...`, 'AccSaberService');
log.trace(`Updating players in DB...`, "AccSaberService");
await Promise.all(ranking.map(async p => accSaberPlayersStore.put(p)));
await Promise.all(
ranking.map(async (p) => accSaberPlayersStore.put(p)),
);
log.trace(`Players updated`, 'AccSaberService');
log.trace(`Players updated`, "AccSaberService");
log.trace(`Updating players last update date in DB...`, 'AccSaberService');
log.trace(
`Updating players last update date in DB...`,
"AccSaberService",
);
await tx.objectStore('key-value').put(new Date(), getLastUpdatedKey(rankingType));
await tx
.objectStore("key-value")
.put(new Date(), getLastUpdatedKey(rankingType));
log.debug(`Players last update date updated`, 'AccSaberService');
});
log.debug(`Players last update date updated`, "AccSaberService");
},
);
accSaberPlayersRepository().addToCache(ranking);
keyValueRepository().setCache(getLastUpdatedKey(rankingType), new Date());
log.debug(`${capitalize(category)} ranking refreshing completed`, 'AccSaberService');
log.debug(
`${capitalize(category)} ranking refreshing completed`,
"AccSaberService",
);
return ranking.sort((a, b) => a.rank - b.rank);
}
catch (e) {
} catch (e) {
if (throwErrors) throw e;
log.debug(` ${capitalize(category)} ranking refreshing error`, 'AccSaberService', e)
log.debug(
` ${capitalize(category)} ranking refreshing error`,
"AccSaberService",
e,
);
return null;
}
}
};
const refreshAll = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
log.trace(`Starting AccSaber all data refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
const refreshAll = async (
category = "overall",
forceUpdate = false,
priority = queues.PRIORITY.BG_NORMAL,
throwErrors = false,
) => {
log.trace(
`Starting AccSaber all data refreshing${
forceUpdate ? " (forced)" : ""
}...`,
"AccSaberService",
);
try {
const dbCategories = await refreshCategories();
if (!dbCategories || !dbCategories.all) throw 'Can not refresh categories';
if (!dbCategories || !dbCategories.all)
throw "Can not refresh categories";
const allRankings = await Promise.all(
dbCategories.all.map(c => c.name).map(async category => refreshRanking(category))
)
dbCategories.all
.map((c) => c.name)
.map(async (category) => refreshRanking(category)),
);
log.debug(`All data refreshing completed.`, 'AccSaberService')
log.debug(`All data refreshing completed.`, "AccSaberService");
const rankings = allRankings.reduce((cum, ranking) => {
if (!ranking || !ranking.length) return cum;
@ -470,21 +663,28 @@ export default () => {
return cum;
}, {});
Promise.all((await playerService.getAllActive()).map(async player => updatePlayerHistory(player))).then(_ => _);
Promise.all(
(await playerService.getAllActive()).map(async (player) =>
updatePlayerHistory(player),
),
).then((_) => _);
return dbCategories.all.map(c => ({...c, ranking: rankings?.[c.name] ?? []}));
return dbCategories.all.map((c) => ({
...c,
ranking: rankings?.[c.name] ?? [],
}));
} catch (e) {
if (throwErrors) throw e;
log.debug(`All data refreshing error`, 'AccSaberService', e)
log.debug(`All data refreshing error`, "AccSaberService", e);
return null;
}
}
};
const destroyService = () => {
service = null;
}
};
service = {
isDataForPlayerAvailable,
@ -502,7 +702,7 @@ export default () => {
refreshRanking,
refreshAll,
destroyService,
}
};
return service;
}
};

@ -1,28 +1,29 @@
import hashApiClient from '../network/clients/beatmaps/api-hash';
import keyApiClient from '../network/clients/beatmaps/api-key';
import {PRIORITY} from '../network/queues/http-queue';
import log from '../utils/logger'
import {SsrHttpNotFoundError, SsrNetworkError} from '../network/errors'
import hashApiClient from "../network/clients/beatmaps/api-hash";
import keyApiClient from "../network/clients/beatmaps/api-key";
import { PRIORITY } from "../network/queues/http-queue";
import log from "../utils/logger";
import { SsrHttpNotFoundError, SsrNetworkError } from "../network/errors";
import songsBeatMapsRepository from "../db/repository/songs-beatmaps";
import cacheRepository from "../db/repository/cache";
import {addToDate, dateFromString, HOUR} from '../utils/date'
import {capitalize, opt} from '../utils/js'
import { addToDate, dateFromString, HOUR } from "../utils/date";
import { capitalize, opt } from "../utils/js";
const BM_SUSPENSION_KEY = 'bmSuspension';
const BM_NOT_FOUND_KEY = 'bm404';
const BM_SUSPENSION_KEY = "bmSuspension";
const BM_NOT_FOUND_KEY = "bm404";
const BM_NOT_FOUND_HOURS_BETWEEN_COUNTS = 1;
const INVALID_NOTES_COUNT_FIXES = {
'e738b38b594861745bfb0473c66ca5cca15072ff': [
{type: 'Standard', diff: "ExpertPlus", notes: 942}
]
}
e738b38b594861745bfb0473c66ca5cca15072ff: [
{ type: "Standard", diff: "ExpertPlus", notes: 942 },
],
};
export default () => {
const cacheSongInfo = async (songInfo, originalHash) => {
if (!songInfo) return null;
const hash = originalHash && originalHash.length ? originalHash : songInfo.hash;
const hash =
originalHash && originalHash.length ? originalHash : songInfo.hash;
if (!hash || !songInfo.key) return null;
@ -34,30 +35,46 @@ export default () => {
await songsBeatMapsRepository().set(songInfo);
return songInfo;
}
};
const isSuspended = bsSuspension => !!bsSuspension && bsSuspension.activeTo > new Date() && bsSuspension.started > addToDate(-24 * HOUR);
const getCurrentSuspension = async () => cacheRepository().get(BM_SUSPENSION_KEY);
const prolongSuspension = async bsSuspension => {
const isSuspended = (bsSuspension) =>
!!bsSuspension &&
bsSuspension.activeTo > new Date() &&
bsSuspension.started > addToDate(-24 * HOUR);
const getCurrentSuspension = async () =>
cacheRepository().get(BM_SUSPENSION_KEY);
const prolongSuspension = async (bsSuspension) => {
const current = new Date();
const suspension = isSuspended(bsSuspension) ? bsSuspension : {started: current, activeTo: new Date(), count: 0};
const suspension = isSuspended(bsSuspension)
? bsSuspension
: { started: current, activeTo: new Date(), count: 0 };
suspension.activeTo = addToDate(Math.pow(2, suspension.count) * HOUR, suspension.activeTo);
suspension.activeTo = addToDate(
Math.pow(2, suspension.count) * HOUR,
suspension.activeTo,
);
suspension.count++;
return await cacheRepository().set(suspension, BM_SUSPENSION_KEY);
}
};
const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY);
const set404Hashes = async hashes => cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
const setHashNotFound = async hash => {
const set404Hashes = async (hashes) =>
cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
const setHashNotFound = async (hash) => {
let songs404 = await get404Hashes();
if (!songs404) songs404 = {};
const item = songs404[hash] ? songs404[hash] : {firstTry: new Date(), recentTry: null, count: 0};
const item = songs404[hash]
? songs404[hash]
: { firstTry: new Date(), recentTry: null, count: 0 };
if (!item.recentTry || addToDate(BM_NOT_FOUND_HOURS_BETWEEN_COUNTS * HOUR, item.recentTry) < new Date()) {
if (
!item.recentTry ||
addToDate(BM_NOT_FOUND_HOURS_BETWEEN_COUNTS * HOUR, item.recentTry) <
new Date()
) {
item.recentTry = new Date();
item.count++;
@ -65,31 +82,40 @@ export default () => {
await set404Hashes(songs404);
}
}
const isHashUnavailable = async hash => {
};
const isHashUnavailable = async (hash) => {
const songs404 = await get404Hashes();
return songs404 && songs404[hash] && songs404[hash].count >= 3;
}
};
const fixInvalidNotesCount = (hash, songInfo) => {
if (!hash) return songInfo;
if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions)
songInfo.versions.forEach(si => {
songInfo.versions.forEach((si) => {
if (!si?.diffs) return;
si.diffs.forEach(d => {
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(f => f.type === d?.characteristic && f.diff === d?.difficulty);
si.diffs.forEach((d) => {
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(
(f) => f.type === d?.characteristic && f.diff === d?.difficulty,
);
if (!newNotesCnt) return;
d.notes = newNotesCnt.notes;
})
})
});
});
return songInfo;
}
};
const fetchSong = async (songInfo, fetchFunc, forceUpdate = false, cacheOnly = false, errSongId = '', hash = null) => {
const fetchSong = async (
songInfo,
fetchFunc,
forceUpdate = false,
cacheOnly = false,
errSongId = "",
hash = null,
) => {
if (!forceUpdate && songInfo) return fixInvalidNotesCount(hash, songInfo);
if (cacheOnly) return null;
@ -97,7 +123,11 @@ export default () => {
let bsSuspension = await getCurrentSuspension();
try {
if (isSuspended(bsSuspension) || (hash && await isHashUnavailable(hash))) return null;
if (
isSuspended(bsSuspension) ||
(hash && (await isHashUnavailable(hash)))
)
return null;
const songInfo = await fetchFunc();
if (!songInfo) {
@ -111,36 +141,78 @@ export default () => {
await setHashNotFound(hash);
}
if (err instanceof SsrNetworkError && err.message === 'Network error') {
try {await prolongSuspension(bsSuspension)} catch {}
if (err instanceof SsrNetworkError && err.message === "Network error") {
try {
await prolongSuspension(bsSuspension);
} catch {}
}
log.warn(`Error fetching BeatSaver song "${errSongId}"`);
return null;
}
}
};
const byHash = async (hash, forceUpdate = false, cacheOnly = false, signal = null, priority = PRIORITY.FG_LOW) => {
const byHash = async (
hash,
forceUpdate = false,
cacheOnly = false,
signal = null,
priority = PRIORITY.FG_LOW,
) => {
hash = hash.toLowerCase();
const songInfo = await songsBeatMapsRepository().get(hash);
return fetchSong(songInfo, () => hashApiClient.getProcessed({hash, signal, priority}), forceUpdate, cacheOnly, hash, hash)
}
return fetchSong(
songInfo,
() => hashApiClient.getProcessed({ hash, signal, priority }),
forceUpdate,
cacheOnly,
hash,
hash,
);
};
const byKey = async (key, forceUpdate = false, cacheOnly = false, signal = null, priority = PRIORITY.FG_LOW) => {
const byKey = async (
key,
forceUpdate = false,
cacheOnly = false,
signal = null,
priority = PRIORITY.FG_LOW,
) => {
key = key.toLowerCase();
const songInfo = await songsBeatMapsRepository().getFromIndex('songs-beatmaps-key', key);
const songInfo = await songsBeatMapsRepository().getFromIndex(
"songs-beatmaps-key",
key,
);
return fetchSong(songInfo, () => keyApiClient.getProcessed({key, signal, priority}), forceUpdate, cacheOnly, key)
}
return fetchSong(
songInfo,
() => keyApiClient.getProcessed({ key, signal, priority }),
forceUpdate,
cacheOnly,
key,
);
};
const convertOldBeatSaverToBeatMaps = song => {
let {key, hash, name, metadata: {characteristics}} = song;
const convertOldBeatSaverToBeatMaps = (song) => {
let {
key,
hash,
name,
metadata: { characteristics },
} = song;
if (!key || !hash || !name || !characteristics || !Array.isArray(characteristics)) return null;
if (
!key ||
!hash ||
!name ||
!characteristics ||
!Array.isArray(characteristics)
)
return null;
if (hash.toLowerCase) hash = hash.toLowerCase();
@ -148,25 +220,25 @@ export default () => {
if (!ch.name || !ch.difficulties) return diffs;
const characteristic = ch.name;
return diffs.concat(
Object.entries(ch.difficulties)
.map(([difficulty, obj]) => {
return diffs
.concat(
Object.entries(ch.difficulties).map(([difficulty, obj]) => {
if (!obj) return null;
difficulty = capitalize(difficulty);
const seconds = opt(obj, 'length', null);
const notes = opt(obj, 'notes', null)
const seconds = opt(obj, "length", null);
const notes = opt(obj, "notes", null);
const nps = notes && seconds ? notes / seconds : null;
return {
njs: opt(obj, 'njs', null),
offset: opt(obj, 'njsOffset', null),
njs: opt(obj, "njs", null),
offset: opt(obj, "njsOffset", null),
notes,
bombs: opt(obj, 'bombs', null),
obstacles: opt(obj, 'obstacles', null),
bombs: opt(obj, "bombs", null),
obstacles: opt(obj, "obstacles", null),
nps,
length: opt(obj, 'duration', null),
length: opt(obj, "duration", null),
characteristic,
difficulty,
events: null,
@ -182,41 +254,42 @@ export default () => {
},
stars: null,
};
}))
.filter(diff => diff)
}),
)
.filter((diff) => diff);
}, []);
return {
lastUpdated: dateFromString(opt(song, 'uploaded', new Date())),
oldBeatSaverId: opt(song, '_id', null),
lastUpdated: dateFromString(opt(song, "uploaded", new Date())),
oldBeatSaverId: opt(song, "_id", null),
id: key,
hash,
key,
name,
description: '',
description: "",
uploader: {
id: null,
name: opt(song, 'uploader.username', null),
name: opt(song, "uploader.username", null),
hash: null,
avatar: null
avatar: null,
},
metadata: {
bpm: opt(song, 'metadata.bpm', null),
duration: opt(song, 'metadata.duration', null),
songName: opt(song, 'metadata.songName', ''),
songSubName: opt(song, 'metadata.songSubName', ''),
songAuthorName: opt(song, 'metadata.songAuthorName', ''),
levelAuthorName: opt(song, 'metadata.levelAuthorName', '')
bpm: opt(song, "metadata.bpm", null),
duration: opt(song, "metadata.duration", null),
songName: opt(song, "metadata.songName", ""),
songSubName: opt(song, "metadata.songSubName", ""),
songAuthorName: opt(song, "metadata.songAuthorName", ""),
levelAuthorName: opt(song, "metadata.levelAuthorName", ""),
},
stats: {
plays: opt(song, 'stats.plays', 0),
downloads: opt(song, 'stats.downloads', 0),
upvotes: opt(song, 'stats.upVotes', 0),
downvotes: opt(song, 'stats.downVotes', 0),
score: null
plays: opt(song, "stats.plays", 0),
downloads: opt(song, "stats.downloads", 0),
upvotes: opt(song, "stats.upVotes", 0),
downvotes: opt(song, "stats.downVotes", 0),
score: null,
},
uploaded: opt(song, 'uploaded', null),
automapper: !!opt(song, 'metadata.automapper', false),
uploaded: opt(song, "uploaded", null),
automapper: !!opt(song, "metadata.automapper", false),
ranked: null,
qualified: null,
versions: [
@ -224,20 +297,20 @@ export default () => {
hash,
key,
state: "Published",
createdAt: opt(song, 'uploaded', null),
createdAt: opt(song, "uploaded", null),
sageScore: null,
diffs,
downloadURL: `https://cdn.beatsaver.com/${hash}.zip`,
coverURL: `https://cdn.beatsaver.com/${hash}.jpg`,
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`
}
]
}
}
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`,
},
],
};
};
return {
byHash,
byKey,
convertOldBeatSaverToBeatMaps
}
}
convertOldBeatSaverToBeatMaps,
};
};

@ -1,16 +1,24 @@
import {PRIORITY} from '../network/queues/http-queue';
import createPlayerService from './scoresaber/player'
import createScoresService from './scoresaber/scores'
import beatSaviorApiClient from '../network/clients/beatsavior/api';
import beatSaviorRepository from '../db/repository/beat-savior'
import beatSaviorPlayersRepository from '../db/repository/beat-savior-players'
import {addToDate, DAY, formatDate, HOUR, MINUTE, SECOND, truncateDate} from '../utils/date'
import log from '../utils/logger'
import {opt} from '../utils/js'
import makePendingPromisePool from '../utils/pending-promises'
import {PLAYER_SCORES_PER_PAGE} from '../utils/scoresaber/consts'
import {roundToPrecision} from '../utils/format'
import {serviceFilterFunc} from './utils'
import { PRIORITY } from "../network/queues/http-queue";
import createPlayerService from "./scoresaber/player";
import createScoresService from "./scoresaber/scores";
import beatSaviorApiClient from "../network/clients/beatsavior/api";
import beatSaviorRepository from "../db/repository/beat-savior";
import beatSaviorPlayersRepository from "../db/repository/beat-savior-players";
import {
addToDate,
DAY,
formatDate,
HOUR,
MINUTE,
SECOND,
truncateDate,
} from "../utils/date";
import log from "../utils/logger";
import { opt } from "../utils/js";
import makePendingPromisePool from "../utils/pending-promises";
import { PLAYER_SCORES_PER_PAGE } from "../utils/scoresaber/consts";
import { roundToPrecision } from "../utils/format";
import { serviceFilterFunc } from "./utils";
const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 15;
const CACHED_PLAYER_REFRESH_INTERVAL = HOUR * 3;
@ -30,52 +38,73 @@ export default () => {
const playerService = createPlayerService();
const scoresService = createScoresService();
const getPlayerScores = async playerId => resolvePromiseOrWaitForPending(`getPlayerScores/${playerId}`, () => beatSaviorRepository().getAllFromIndex('beat-savior-playerId', playerId));
const getPlayerScores = async (playerId) =>
resolvePromiseOrWaitForPending(`getPlayerScores/${playerId}`, () =>
beatSaviorRepository().getAllFromIndex("beat-savior-playerId", playerId),
);
const getPlayerScoresWithScoreSaber = async playerId => {
const getPlayerScoresWithScoreSaber = async (playerId) => {
const [beatSaviorData, playerScores] = await Promise.all([
getPlayerScores(playerId),
resolvePromiseOrWaitForPending(`getSsPlayerScores/${playerId}`, () => scoresService.getPlayerScoresAsObject(
resolvePromiseOrWaitForPending(`getSsPlayerScores/${playerId}`, () =>
scoresService.getPlayerScoresAsObject(
playerId,
score => score?.leaderboard?.song?.hash?.toLowerCase() ?? null,
(score) => score?.leaderboard?.song?.hash?.toLowerCase() ?? null,
true,
)),
),
),
]);
return beatSaviorData.map(bsData => {
if (!bsData?.hash || !playerScores?.[bsData?.hash?.toLowerCase()]) return bsData;
return beatSaviorData.map((bsData) => {
if (!bsData?.hash || !playerScores?.[bsData?.hash?.toLowerCase()])
return bsData;
const ssScore = playerScores[bsData.hash.toLowerCase()].find(ssScore => isScoreMatchingBsData(ssScore, bsData, true)) ?? null;
const ssScore =
playerScores[bsData.hash.toLowerCase()].find((ssScore) =>
isScoreMatchingBsData(ssScore, bsData, true),
) ?? null;
return {
...bsData,
ssScore
}
ssScore,
};
});
}
};
const isScoreMatchingBsData = (score, bsData, exact = true) => {
if (!bsData.hash || !bsData.score || !bsData.timeSet || !opt(bsData, 'stats.won')) return false;
if (
!bsData.hash ||
!bsData.score ||
!bsData.timeSet ||
!opt(bsData, "stats.won")
)
return false;
const diff = opt(score, 'leaderboard.diffInfo.diff');
const scoreValue = opt(score, 'score.score');
const timeSet = opt(score, 'score.timeSet')
let hash = opt(score, 'leaderboard.song.hash');
const diff = opt(score, "leaderboard.diffInfo.diff");
const scoreValue = opt(score, "score.score");
const timeSet = opt(score, "score.timeSet");
let hash = opt(score, "leaderboard.song.hash");
if (!diff || !score || !timeSet || !hash) return false;
hash = hash.toLowerCase();
if (bsData.hash === hash && bsData.diff === diff) {
return !exact || (bsData.score === scoreValue && Math.abs(timeSet.getTime() - bsData.timeSet.getTime()) < MINUTE);
return (
!exact ||
(bsData.score === scoreValue &&
Math.abs(timeSet.getTime() - bsData.timeSet.getTime()) < MINUTE)
);
}
return false;
}
};
const getScoresHistogramDefinition = (serviceParams = {sort: 'recent', order: 'desc'}) => {
const sort = serviceParams?.sort ?? 'recent';
const order = serviceParams?.order ?? 'desc';
const getScoresHistogramDefinition = (
serviceParams = { sort: "recent", order: "desc" },
) => {
const sort = serviceParams?.sort ?? "recent";
const order = serviceParams?.order ?? "desc";
let round = 2;
let bucketSize = 1;
@ -83,57 +112,65 @@ export default () => {
let maxBucketSize = null;
let bucketSizeStep = null;
let bucketSizeValues = null;
let type = 'linear';
let valFunc = s => s;
let type = "linear";
let valFunc = (s) => s;
let filterFunc = serviceFilterFunc(serviceParams);
let histogramFilterFunc = s => s;
let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear'
let histogramFilterFunc = (s) => s;
let roundedValFunc = (s, type = type, precision = bucketSize) =>
type === "linear"
? roundToPrecision(valFunc(s), precision)
: truncateDate(valFunc(s), precision);
let prefix = '';
let prefixLong = '';
let suffix = '';
let suffixLong = '';
let prefix = "";
let prefixLong = "";
let suffix = "";
let suffixLong = "";
switch (sort) {
case 'recent':
valFunc = s => s?.timeSet;
type = 'time';
bucketSize = 'day'
case "recent":
valFunc = (s) => s?.timeSet;
type = "time";
bucketSize = "day";
break;
case 'acc':
valFunc = s => (s?.trackers?.scoreTracker?.rawRatio ?? 0) * 100;
histogramFilterFunc = h => h?.x >= HISTOGRAM_ACC_THRESHOLD;
type = 'linear';
case "acc":
valFunc = (s) => (s?.trackers?.scoreTracker?.rawRatio ?? 0) * 100;
histogramFilterFunc = (h) => h?.x >= HISTOGRAM_ACC_THRESHOLD;
type = "linear";
bucketSize = 0.25;
minBucketSize = 0.05;
maxBucketSize = 10;
bucketSizeStep = 0.05;
round = 2;
suffix = '%';
suffixLong = '%';
suffix = "%";
suffixLong = "%";
break;
case 'mistakes':
valFunc = s => (s?.stats?.miss ?? 0) + (s?.stats?.wallHit ?? 0) + (s?.stats?.bombHit ?? 0);
histogramFilterFunc = h => h?.x <= HISTOGRAM_MISTAKES_THRESHOLD;
type = 'linear';
case "mistakes":
valFunc = (s) =>
(s?.stats?.miss ?? 0) +
(s?.stats?.wallHit ?? 0) +
(s?.stats?.bombHit ?? 0);
histogramFilterFunc = (h) => h?.x <= HISTOGRAM_MISTAKES_THRESHOLD;
type = "linear";
bucketSize = 1;
minBucketSize = 1;
maxBucketSize = 50;
bucketSizeStep = 1;
round = 0;
suffixLong = ' mistake(s)';
suffixLong = " mistake(s)";
break;
}
return {
getValue: valFunc,
getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize),
getRoundedValue:
(bucketSize = bucketSize) =>
(s) =>
roundedValFunc(s, type, bucketSize),
filter: filterFunc,
histogramFilter: histogramFilterFunc,
sort: (a, b) => order === 'asc' ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a),
sort: (a, b) =>
order === "asc" ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a),
type,
bucketSize,
minBucketSize,
@ -145,11 +182,14 @@ export default () => {
prefixLong,
suffix,
suffixLong,
order
}
}
order,
};
};
const getPlayerScoresPage = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}) => {
const getPlayerScoresPage = async (
playerId,
serviceParams = { sort: "recent", order: "desc", page: 1 },
) => {
let page = serviceParams?.page ?? 1;
if (page < 1) page = 1;
@ -157,9 +197,10 @@ export default () => {
if (!playerScores || !playerScores.length) return { total: 0, scores: [] };
const {sort: sortFunc, filter: filterFunc} = getScoresHistogramDefinition(serviceParams);
const { sort: sortFunc, filter: filterFunc } =
getScoresHistogramDefinition(serviceParams);
playerScores = playerScores.filter(filterFunc).sort(sortFunc)
playerScores = playerScores.filter(filterFunc).sort(sortFunc);
const startIdx = (page - 1) * PLAYER_SCORES_PER_PAGE;
@ -169,13 +210,14 @@ export default () => {
total: playerScores.length,
scores: playerScores
.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE)
.map(bs => {
.map((bs) => {
const leaderboard = bs.leaderboard;
if (!leaderboard.leaderboardId) leaderboard.leaderboardId = bs.beatSaviorId;
if (!leaderboard.leaderboardId)
leaderboard.leaderboardId = bs.beatSaviorId;
leaderboard.leaderboardId += Math.random(); // ScoresSvelte needs different keys for each scores row
const rawScore = opt(bs, 'trackers.scoreTracker.rawScore', 0);
const rawRatio = opt(bs, 'trackers.scoreTracker.rawRatio', 0);
const rawScore = opt(bs, "trackers.scoreTracker.rawScore", 0);
const rawRatio = opt(bs, "trackers.scoreTracker.rawRatio", 0);
const maxScore = rawRatio & rawScore ? rawScore / rawRatio : 0;
return {
@ -188,51 +230,76 @@ export default () => {
score: {
acc: rawRatio * 100,
maxScore,
mods: opt(bs, 'trackers.scoreTracker.modifiers', null),
percentage: opt(bs, 'trackers.scoreTracker.rawRatio', 0) * 100,
mods: opt(bs, "trackers.scoreTracker.modifiers", null),
percentage: opt(bs, "trackers.scoreTracker.rawRatio", 0) * 100,
pp: 0,
ppWeighted: 0,
rank: null,
score: opt(bs, 'trackers.scoreTracker.score', 0),
score: opt(bs, "trackers.scoreTracker.score", 0),
scoreId: bs.beatSaviorId,
timeSet: bs.timeSet,
unmodifiedScore: rawScore,
weight: 0,
},
timeSet: bs.timeSet,
}
})
};
}
}),
};
};
const updateData = async (playerId, data) => {
log.debug(`Updating Beat Savior data for player "${playerId}"...`, 'BeatSaviorService')
log.debug(
`Updating Beat Savior data for player "${playerId}"...`,
"BeatSaviorService",
);
await Promise.all(data.map(async d => beatSaviorRepository().set(d)));
await Promise.all(data.map(async (d) => beatSaviorRepository().set(d)));
log.debug(`Update player "${playerId}" Beat Savior last refresh date...`, 'BeatSaviorService')
log.debug(
`Update player "${playerId}" Beat Savior last refresh date...`,
"BeatSaviorService",
);
await beatSaviorPlayersRepository().set({playerId, lastRefresh: new Date()})
await beatSaviorPlayersRepository().set({
playerId,
lastRefresh: new Date(),
});
log.debug(`Beat Savior data for player "${playerId}" updated.`, 'BeatSaviorService')
log.debug(
`Beat Savior data for player "${playerId}" updated.`,
"BeatSaviorService",
);
return data;
}
};
const fetchPlayer = async (playerId, priority = PRIORITY.BG_NORMAL) => {
try {
log.debug(`Fetching Beat Savior data for player "${playerId}"...`, 'BeatSaviorService');
log.debug(
`Fetching Beat Savior data for player "${playerId}"...`,
"BeatSaviorService",
);
const data = await beatSaviorApiClient.getProcessed({playerId, priority});
const data = await beatSaviorApiClient.getProcessed({
playerId,
priority,
});
if (!data) {
log.debug(`No Beat Savior data for player "${playerId}"`, 'BeatSaviorService')
log.debug(
`No Beat Savior data for player "${playerId}"`,
"BeatSaviorService",
);
return null;
}
// TODO: check if data already exists in DB
log.trace(`Beat Savior data for player "${playerId}" fetched`, 'BeatSaviorService', data);
log.trace(
`Beat Savior data for player "${playerId}" fetched`,
"BeatSaviorService",
data,
);
return updateData(playerId, data);
} catch (err) {
@ -240,62 +307,121 @@ export default () => {
return null;
}
}
};
const refresh = async (playerId, force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
log.trace(`Starting refreshing BeatSavior for player "${playerId}" ${force ? ' (forced)' : ''}...`, 'BeatSaviorService')
const refresh = async (
playerId,
force = false,
priority = PRIORITY.BG_NORMAL,
throwErrors = false,
) => {
log.trace(
`Starting refreshing BeatSavior for player "${playerId}" ${
force ? " (forced)" : ""
}...`,
"BeatSaviorService",
);
try {
const player = await playerService.get(playerId);
const REFRESH_INTERVAL = playerService.isMainPlayer(playerId) ? MAIN_PLAYER_REFRESH_INTERVAL : (player ? CACHED_PLAYER_REFRESH_INTERVAL : OTHER_PLAYER_REFRESH_INTERVAL);
const REFRESH_INTERVAL = playerService.isMainPlayer(playerId)
? MAIN_PLAYER_REFRESH_INTERVAL
: player
? CACHED_PLAYER_REFRESH_INTERVAL
: OTHER_PLAYER_REFRESH_INTERVAL;
const bsPlayerInfo = await beatSaviorPlayersRepository().get(playerId);
const nextUpdate = bsPlayerInfo && bsPlayerInfo.lastRefresh ? addToDate(REFRESH_INTERVAL, bsPlayerInfo.lastRefresh) : addToDate(-SECOND);
const nextUpdate =
bsPlayerInfo && bsPlayerInfo.lastRefresh
? addToDate(REFRESH_INTERVAL, bsPlayerInfo.lastRefresh)
: addToDate(-SECOND);
if (!force && bsPlayerInfo && nextUpdate > new Date()) {
log.debug(`Beat Savior data is still fresh, skipping. Next refresh on ${formatDate(nextUpdate)}`, 'BeatSaviorService')
log.debug(
`Beat Savior data is still fresh, skipping. Next refresh on ${formatDate(
nextUpdate,
)}`,
"BeatSaviorService",
);
return null;
if (player) {
log.trace(`Player "${playerId}" is a cached one, checking recent play date`, 'BeatSaviorService')
log.trace(
`Player "${playerId}" is a cached one, checking recent play date`,
"BeatSaviorService",
);
if (player.recentPlay && player.recentPlay < bsPlayerInfo.lastRefresh) {
log.debug(`Beat Savior data for player "${playerId}" was refreshed after recent play, skipping`, 'BeatSaviorService')
if (
player.recentPlay &&
player.recentPlay < bsPlayerInfo.lastRefresh
) {
log.debug(
`Beat Savior data for player "${playerId}" was refreshed after recent play, skipping`,
"BeatSaviorService",
);
return null;
}
}
}
return resolvePromiseOrWaitForPending(`refresh/${playerId}`, () => fetchPlayer(playerId, priority));
return resolvePromiseOrWaitForPending(`refresh/${playerId}`, () =>
fetchPlayer(playerId, priority),
);
} catch (e) {
if (throwErrors) throw e;
log.debug(`Beat Savior data refreshing error${e.toString ? `: ${e.toString()}` : ''}`, 'BeatSaviorService', e)
log.debug(
`Beat Savior data refreshing error${
e.toString ? `: ${e.toString()}` : ""
}`,
"BeatSaviorService",
e,
);
return null;
}
}
};
const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
log.trace(`Starting refreshing Beat Savior data for all players${force ? ' (forced)' : ''}...`, 'BeatSaviorService');
const refreshAll = async (
force = false,
priority = PRIORITY.BG_NORMAL,
throwErrors = false,
) => {
log.trace(
`Starting refreshing Beat Savior data for all players${
force ? " (forced)" : ""
}...`,
"BeatSaviorService",
);
const allPlayers = await playerService.getAll();
if (!allPlayers || !allPlayers.length) {
log.trace(`No players in DB, skipping.`, 'BeatSaviorService');
log.trace(`No players in DB, skipping.`, "BeatSaviorService");
return null;
}
const allRefreshed = await Promise.all(allPlayers.map(async player => ({
const allRefreshed = await Promise.all(
allPlayers.map(async (player) => ({
playerId: player.playerId,
beatSavior: await refresh(player.playerId, force, priority, throwErrors),
})));
beatSavior: await refresh(
player.playerId,
force,
priority,
throwErrors,
),
})),
);
log.trace(`Beat Savior data for all players refreshed.`, 'BeatSaviorService', allRefreshed);
log.trace(
`Beat Savior data for all players refreshed.`,
"BeatSaviorService",
allRefreshed,
);
return allRefreshed;
}
};
const get = async (playerId, score) => {
if (score && score.beatSavior) return score.beatSavior;
@ -303,12 +429,18 @@ export default () => {
const playerBsData = await getPlayerScores(playerId);
if (!playerBsData || !playerBsData.length) return null;
const bsData = playerBsData.find(bsData => isScoreMatchingBsData(score, bsData, true));
const bsData = playerBsData.find((bsData) =>
isScoreMatchingBsData(score, bsData, true),
);
return bsData ? bsData : null;
}
};
const isDataForPlayerAvailable = async playerId => await beatSaviorRepository().getFromIndex('beat-savior-playerId', playerId) !== undefined;
const isDataForPlayerAvailable = async (playerId) =>
(await beatSaviorRepository().getFromIndex(
"beat-savior-playerId",
playerId,
)) !== undefined;
const destroyService = () => {
serviceCreationCount--;
@ -319,7 +451,7 @@ export default () => {
service = null;
}
}
};
service = {
fetchPlayer,
@ -332,7 +464,7 @@ export default () => {
isDataForPlayerAvailable,
getScoresHistogramDefinition,
destroyService,
}
};
return service;
}
};

@ -1,7 +1,7 @@
import keyValueRepository from '../db/repository/key-value';
import {opt} from '../utils/js'
import keyValueRepository from "../db/repository/key-value";
import { opt } from "../utils/js";
const STORE_CONFIG_KEY = 'config';
const STORE_CONFIG_KEY = "config";
let service = null;
@ -9,22 +9,23 @@ export default () => {
if (service) return service;
const get = async () => keyValueRepository().get(STORE_CONFIG_KEY);
const set = async config => keyValueRepository().set(config, STORE_CONFIG_KEY);
const set = async (config) =>
keyValueRepository().set(config, STORE_CONFIG_KEY);
const getMainPlayerId = async () => {
const config = await get();
return opt(config, 'users.main');
}
return opt(config, "users.main");
};
const destroyService = () => {}
const destroyService = () => {};
service = {
get,
set,
getMainPlayerId,
destroyService,
}
};
return service;
}
};

@ -1,14 +1,14 @@
import leaderboardPageClient from '../../network/clients/scoresaber/leaderboard/page-leaderboard'
import accSaberLeaderboardApiClient from '../../network/clients/accsaber/api-leaderboard'
import makePendingPromisePool from '../../utils/pending-promises'
import createPlayersService from '../../services/scoresaber/player'
import createScoresService from '../../services/scoresaber/scores'
import {PRIORITY} from '../../network/queues/http-queue'
import {LEADERBOARD_SCORES_PER_PAGE} from '../../utils/scoresaber/consts'
import {LEADERBOARD_SCORES_PER_PAGE as ACCSABER_LEADERBOARD_SCORES_PER_PAGE} from '../../utils/accsaber/consts'
import {formatDateRelative, MINUTE} from '../../utils/date'
import {convertArrayToObjectByKey, opt} from '../../utils/js'
import eventBus from '../../utils/broadcast-channel-pubsub'
import leaderboardPageClient from "../../network/clients/scoresaber/leaderboard/page-leaderboard";
import accSaberLeaderboardApiClient from "../../network/clients/accsaber/api-leaderboard";
import makePendingPromisePool from "../../utils/pending-promises";
import createPlayersService from "../../services/scoresaber/player";
import createScoresService from "../../services/scoresaber/scores";
import { PRIORITY } from "../../network/queues/http-queue";
import { LEADERBOARD_SCORES_PER_PAGE } from "../../utils/scoresaber/consts";
import { LEADERBOARD_SCORES_PER_PAGE as ACCSABER_LEADERBOARD_SCORES_PER_PAGE } from "../../utils/accsaber/consts";
import { formatDateRelative, MINUTE } from "../../utils/date";
import { convertArrayToObjectByKey, opt } from "../../utils/js";
import eventBus from "../../utils/broadcast-channel-pubsub";
const ACCSABER_LEADERBOARD_NETWORK_TTL = MINUTE * 5;
@ -20,79 +20,118 @@ export default () => {
const scoresService = createScoresService();
let friendsPromise = Promise.resolve([]);
const refreshFriends = async () => friendsPromise = playersService.getAll();
eventBus.on('player-profile-removed', playerId => refreshFriends());
eventBus.on('player-profile-added', player => refreshFriends());
eventBus.on('player-profile-changed', player => refreshFriends());
refreshFriends().then(_ => {});
const refreshFriends = async () => (friendsPromise = playersService.getAll());
eventBus.on("player-profile-removed", (playerId) => refreshFriends());
eventBus.on("player-profile-added", (player) => refreshFriends());
eventBus.on("player-profile-changed", (player) => refreshFriends());
refreshFriends().then((_) => {});
const resolvePromiseOrWaitForPending = makePendingPromisePool();
const fetchPage = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, signal = null, force = false) => resolvePromiseOrWaitForPending(
const fetchPage = async (
leaderboardId,
page = 1,
priority = PRIORITY.FG_LOW,
signal = null,
force = false,
) =>
resolvePromiseOrWaitForPending(
`pageClient/leaderboard/${leaderboardId}/${page}`,
() => leaderboardPageClient.getProcessed({
() =>
leaderboardPageClient.getProcessed({
leaderboardId,
page,
signal,
priority,
cacheTtl: MINUTE,
}));
}),
);
const fetchAccSaberPage = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, signal = null, force = false) => {
const fetchAccSaberPage = async (
leaderboardId,
page = 1,
priority = PRIORITY.FG_LOW,
signal = null,
force = false,
) => {
if (page < 1) page = 1;
const data = await resolvePromiseOrWaitForPending(
`accSaberApiClient/leaderboard/${leaderboardId}/${page}`,
() => accSaberLeaderboardApiClient.getProcessed({
() =>
accSaberLeaderboardApiClient.getProcessed({
leaderboardId,
page,
signal,
priority,
cacheTtl: ACCSABER_LEADERBOARD_NETWORK_TTL,
}));
}),
);
if (!data || !data.scores) return data
if (!data || !data.scores) return data;
const startIdx = (page - 1) * ACCSABER_LEADERBOARD_SCORES_PER_PAGE;
if (data.scores.length < startIdx + 1) return data;
return {
...data,
scores: data.scores
.slice(startIdx, startIdx + ACCSABER_LEADERBOARD_SCORES_PER_PAGE)
}
}
scores: data.scores.slice(
startIdx,
startIdx + ACCSABER_LEADERBOARD_SCORES_PER_PAGE,
),
};
};
const getFriendsLeaderboard = async (leaderboardId, priority = PRIORITY.FG_LOW, signal = null) => {
const leaderboard = await resolvePromiseOrWaitForPending(`pageClient/leaderboard/${leaderboardId}/1`, () => leaderboardPageClient.getProcessed({leaderboardId, page: 1, signal, priority, cacheTtl: MINUTE}));
const getFriendsLeaderboard = async (
leaderboardId,
priority = PRIORITY.FG_LOW,
signal = null,
) => {
const leaderboard = await resolvePromiseOrWaitForPending(
`pageClient/leaderboard/${leaderboardId}/1`,
() =>
leaderboardPageClient.getProcessed({
leaderboardId,
page: 1,
signal,
priority,
cacheTtl: MINUTE,
}),
);
const friends = convertArrayToObjectByKey(await friendsPromise, 'playerId');
const friends = convertArrayToObjectByKey(await friendsPromise, "playerId");
const scores = (await scoresService.getLeaderboardScores(leaderboardId))
.map(score => {
.map((score) => {
if (!score || !score.playerId || !friends[score.playerId]) return null;
const player = friends[score.playerId];
return {
player: {playerId: player.playerId, name: player.name, playerInfo: {...player.playerInfo}},
player: {
playerId: player.playerId,
name: player.name,
playerInfo: { ...player.playerInfo },
},
score: { ...score.score },
}
};
})
.filter(s => s)
.sort((a, b) => opt(b, 'score.score', 0) - opt(a, 'score.score', 0))
.filter((s) => s)
.sort((a, b) => opt(b, "score.score", 0) - opt(a, "score.score", 0))
.map((score, idx) => ({
player: score.player,
score: {...score.score, rank: idx + 1, timeSetString: formatDateRelative(score.score.timeSet)},
}))
;
score: {
...score.score,
rank: idx + 1,
timeSetString: formatDateRelative(score.score.timeSet),
},
}));
return { ...leaderboard, scores, pageQty: 1, totalItems: scores.length };
}
};
const destroyService = () => {
service = null;
}
};
service = {
fetchPage,
@ -100,7 +139,7 @@ export default () => {
getFriendsLeaderboard,
LEADERBOARD_SCORES_PER_PAGE,
destroyService,
}
};
return service;
}
};

@ -1,12 +1,12 @@
import eventBus from '../../utils/broadcast-channel-pubsub'
import {configStore} from '../../stores/config'
import playerApiClient from '../../network/clients/scoresaber/player/api'
import playerFindApiClient from '../../network/clients/scoresaber/players/api-player-find'
import playerPageClient from '../../network/clients/scoresaber/player/page'
import {PRIORITY} from '../../network/queues/http-queue'
import playersRepository from '../../db/repository/players'
import playersHistoryRepository from '../../db/repository/players-history'
import log from '../../utils/logger'
import eventBus from "../../utils/broadcast-channel-pubsub";
import { configStore } from "../../stores/config";
import playerApiClient from "../../network/clients/scoresaber/player/api";
import playerFindApiClient from "../../network/clients/scoresaber/players/api-player-find";
import playerPageClient from "../../network/clients/scoresaber/player/page";
import { PRIORITY } from "../../network/queues/http-queue";
import playersRepository from "../../db/repository/players";
import playersHistoryRepository from "../../db/repository/players-history";
import log from "../../utils/logger";
import {
addToDate,
formatDate,
@ -14,12 +14,12 @@ import {
SECOND,
toSsMidnight,
truncateDate,
} from '../../utils/date'
import {opt} from '../../utils/js'
import {db} from '../../db/db'
import makePendingPromisePool from '../../utils/pending-promises'
import {worker} from '../../utils/worker-wrappers'
import {getServicePlayerGain} from '../utils'
} from "../../utils/date";
import { opt } from "../../utils/js";
import { db } from "../../db/db";
import makePendingPromisePool from "../../utils/pending-promises";
import { worker } from "../../utils/worker-wrappers";
import { getServicePlayerGain } from "../utils";
const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 3;
const PLAYER_REFRESH_INTERVAL = MINUTE * 20;
@ -34,91 +34,128 @@ export default () => {
const resolvePromiseOrWaitForPending = makePendingPromisePool();
const configStoreUnsubscribe = configStore.subscribe(config => {
const newMainPlayerId = opt(config, 'users.main')
const configStoreUnsubscribe = configStore.subscribe((config) => {
const newMainPlayerId = opt(config, "users.main");
if (mainPlayerId !== newMainPlayerId) {
mainPlayerId = newMainPlayerId;
log.debug(`Main player changed to ${mainPlayerId}`, 'PlayerService')
log.debug(`Main player changed to ${mainPlayerId}`, "PlayerService");
}
})
});
const isMainPlayer = playerId => mainPlayerId && playerId === mainPlayerId;
const isMainPlayer = (playerId) => mainPlayerId && playerId === mainPlayerId;
const getAll = async (force = false) => playersRepository().getAll(force);
// TODO: just for now
const getFriends = async () => (await getAll()).filter(player => player && player.playerId && !isPlayerMain(player.playerId)).map(p => p.playerId);
const getFriends = async () =>
(await getAll())
.filter(
(player) => player && player.playerId && !isPlayerMain(player.playerId),
)
.map((p) => p.playerId);
const getAllActive = async () => {
const players = await getAll();
if (!players) return [];
return players.filter(player => player && player.playerInfo && !player.playerInfo.inactive && !player.playerInfo.banned);
}
return players.filter(
(player) =>
player &&
player.playerInfo &&
!player.playerInfo.inactive &&
!player.playerInfo.banned,
);
};
const getPlayer = async playerId => await playersRepository().get(playerId);
const getPlayer = async (playerId) => await playersRepository().get(playerId);
const removePlayer = async (playerId, purgeScores = false) => {
await playersRepository().delete(playerId);
// TODO: purge scores if requested
eventBus.publish('player-profile-removed', playerId);
}
eventBus.publish("player-profile-removed", playerId);
};
const addPlayer = async (playerId, priority = PRIORITY.FG_LOW) => {
log.trace(`Starting to add a player "${playerId}"...`, 'PlayerService');
log.trace(`Starting to add a player "${playerId}"...`, "PlayerService");
const player = await refresh(playerId, true, priority, false, true);
if (!player) {
log.warn(`Can not add player "${playerId}"`, 'PlayerService');
log.warn(`Can not add player "${playerId}"`, "PlayerService");
return null;
}
eventBus.publish('player-profile-added', player);
eventBus.publish('player-profile-changed', player);
eventBus.publish("player-profile-added", player);
eventBus.publish("player-profile-changed", player);
log.trace(`Player "${playerId}" added.`, 'PlayerService')
log.trace(`Player "${playerId}" added.`, "PlayerService");
return player;
}
};
const setPlayer = async (player) => {
await playersRepository().set(player);
eventBus.publish('player-profile-changed', player);
eventBus.publish("player-profile-changed", player);
return player;
}
};
const updatePlayer = async (player, waitForSaving = true, forceAdd = false) => {
const updatePlayer = async (
player,
waitForSaving = true,
forceAdd = false,
) => {
if (!player || !player.playerId) {
log.warn(`Can not update player, empty playerId`, 'PlayerService', player)
log.warn(
`Can not update player, empty playerId`,
"PlayerService",
player,
);
}
const dbPlayer = await getPlayer(player.playerId);
if (!dbPlayer && !forceAdd) return player;
const finalPlayer = {...dbPlayer, ...player}
const finalPlayer = { ...dbPlayer, ...player };
if (!waitForSaving) {
setPlayer(finalPlayer).then(_ => _)
setPlayer(finalPlayer).then((_) => _);
return finalPlayer;
}
return await setPlayer(finalPlayer);
}
};
const getPlayerHistory = async playerId => resolvePromiseOrWaitForPending(`playerHistory/${playerId}`, () => playersHistoryRepository().getAllFromIndex('players-history-playerId', playerId))
const getPlayerHistory = async (playerId) =>
resolvePromiseOrWaitForPending(`playerHistory/${playerId}`, () =>
playersHistoryRepository().getAllFromIndex(
"players-history-playerId",
playerId,
),
);
const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) => getServicePlayerGain(playerHistory, toSsMidnight, 'ssDate', daysAgo, maxDaysAgo);
const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) =>
getServicePlayerGain(
playerHistory,
toSsMidnight,
"ssDate",
daysAgo,
maxDaysAgo,
);
const updatePlayerHistory = async player => {
const updatePlayerHistory = async (player) => {
if (!player) return null;
const {playerId, profileLastUpdated, playerInfo: {banned, countries, inactive, pp, rank}, scoreStats} = player;
const {
playerId,
profileLastUpdated,
playerInfo: { banned, countries, inactive, pp, rank },
scoreStats,
} = player;
if (!playerId) return null;
@ -130,8 +167,9 @@ export default () => {
const playerIdLocalTimestamp = `${playerId}_${localDate.getTime()}`;
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
return playersHistoryRepository().getFromIndex('players-history-playerIdSsTimestamp', playerIdSsTimestamp)
.then(async ph => {
return playersHistoryRepository()
.getFromIndex("players-history-playerIdSsTimestamp", playerIdSsTimestamp)
.then(async (ph) => {
if (ph && ph._idbId) {
await playersHistoryRepository().delete(ph._idbId);
@ -142,53 +180,74 @@ export default () => {
return null;
})
.then(async previous => {
.then(async (previous) => {
let accStats = {};
if (worker) {
const stats = await worker.calcPlayerStats(playerId);
const ppBoundary = await worker.calcPpBoundary(playerId) ?? null;
const ppBoundary = (await worker.calcPpBoundary(playerId)) ?? null;
const { badges, totalScore, playCount, ...playerStats } = stats ?? {};
accStats = {...playerStats}
accStats = { ...playerStats };
if (ppBoundary) accStats.ppBoundary = ppBoundary;
if (badges?.length) accStats.accBadges = badges.reduce((cum, b) => ({...cum, [b.label]: b.value}), {});
if (badges?.length)
accStats.accBadges = badges.reduce(
(cum, b) => ({ ...cum, [b.label]: b.value }),
{},
);
}
return playersHistoryRepository().set({
...previous,
...accStats,
playerId, banned, countries, inactive, pp, rank, ...scoreStats,
localDate, ssDate,
playerId,
banned,
countries,
inactive,
pp,
rank,
...scoreStats,
localDate,
ssDate,
playerIdLocalTimestamp,
playerIdSsTimestamp,
});
})
})
.catch(err => {}) // swallow error
}
.catch((err) => {}); // swallow error
};
const isPlayerMain = playerId => playerId === mainPlayerId;
const isPlayerMain = (playerId) => playerId === mainPlayerId;
const getProfileFreshnessDate = (player, refreshInterval = null) => {
const lastUpdated = player && player.profileLastUpdated ? player.profileLastUpdated : null;
const lastUpdated =
player && player.profileLastUpdated ? player.profileLastUpdated : null;
if (!lastUpdated) return addToDate(-SECOND);
const REFRESH_INTERVAL = refreshInterval ? refreshInterval : (isPlayerMain(player.playerId) ? MAIN_PLAYER_REFRESH_INTERVAL : PLAYER_REFRESH_INTERVAL);
const REFRESH_INTERVAL = refreshInterval
? refreshInterval
: isPlayerMain(player.playerId)
? MAIN_PLAYER_REFRESH_INTERVAL
: PLAYER_REFRESH_INTERVAL;
return addToDate(REFRESH_INTERVAL, lastUpdated);
}
};
const isProfileFresh = (player, refreshInterval = null) => getProfileFreshnessDate(player, refreshInterval) > new Date();
const isProfileFresh = (player, refreshInterval = null) =>
getProfileFreshnessDate(player, refreshInterval) > new Date();
const updatePlayerRecentPlay = async (playerId, recentPlay, recentPlayLastUpdated = new Date()) => {
const updatePlayerRecentPlay = async (
playerId,
recentPlay,
recentPlayLastUpdated = new Date(),
) => {
let player;
try {
await db.runInTransaction(['players'], async tx => {
const playersStore = tx.objectStore('players')
await db.runInTransaction(["players"], async (tx) => {
const playersStore = tx.objectStore("players");
player = await playersStore.get(playerId);
if (player) {
player.recentPlayLastUpdated = recentPlayLastUpdated;
@ -200,61 +259,135 @@ export default () => {
if (player) {
playersRepository().addToCache([player]);
eventBus.publish('player-profile-changed', player);
eventBus.publish("player-profile-changed", player);
eventBus.publish('player-recent-play-updated', {playerId, player, recentPlay, recentPlayLastUpdated});
eventBus.publish("player-recent-play-updated", {
playerId,
player,
recentPlay,
recentPlayLastUpdated,
});
}
}
catch(err) {
// swallow error
}
}
const fetchPlayerAndUpdateRecentPlay = async playerId => {
try {
const player = await resolvePromiseOrWaitForPending(`pageClient/${playerId}`, () =>playerPageClient.getProcessed({playerId}));
const recentPlay = opt(player, 'playerInfo.recentPlay');
const recentPlayLastUpdated = opt(player, 'playerInfo.recentPlayLastUpdated');
if (!recentPlay || !recentPlayLastUpdated) return null;
return updatePlayerRecentPlay(playerId, recentPlay, recentPlayLastUpdated);
} catch (err) {
// swallow error
}
};
const fetchPlayerAndUpdateRecentPlay = async (playerId) => {
try {
const player = await resolvePromiseOrWaitForPending(
`pageClient/${playerId}`,
() => playerPageClient.getProcessed({ playerId }),
);
const recentPlay = opt(player, "playerInfo.recentPlay");
const recentPlayLastUpdated = opt(
player,
"playerInfo.recentPlayLastUpdated",
);
if (!recentPlay || !recentPlayLastUpdated) return null;
return updatePlayerRecentPlay(
playerId,
recentPlay,
recentPlayLastUpdated,
);
} catch (err) {
// swallow error
}
};
const isResponseCached = response => playerApiClient.isResponseCached(response);
const getDataFromResponse = response => playerApiClient.getDataFromResponse(response);
const isResponseCached = (response) =>
playerApiClient.isResponseCached(response);
const getDataFromResponse = (response) =>
playerApiClient.getDataFromResponse(response);
const fetchPlayer = async (playerId, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => resolvePromiseOrWaitForPending(`apiClient/${playerId}/${fullResponse}`, () => playerApiClient.getProcessed({...options, playerId, priority, fullResponse}));
const fetchPlayer = async (
playerId,
priority = PRIORITY.FG_LOW,
{ fullResponse = false, ...options } = {},
) =>
resolvePromiseOrWaitForPending(
`apiClient/${playerId}/${fullResponse}`,
() =>
playerApiClient.getProcessed({
...options,
playerId,
priority,
fullResponse,
}),
);
const findPlayer = async (query, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => resolvePromiseOrWaitForPending(`apiClient/find/${query}/${fullResponse}`, () => playerFindApiClient.getProcessed({...options, query, priority, fullResponse}));
const findPlayer = async (
query,
priority = PRIORITY.FG_LOW,
{ fullResponse = false, ...options } = {},
) =>
resolvePromiseOrWaitForPending(
`apiClient/find/${query}/${fullResponse}`,
() =>
playerFindApiClient.getProcessed({
...options,
query,
priority,
fullResponse,
}),
);
const fetchPlayerOrGetFromCache = async (playerId, refreshInterval = MINUTE, priority = PRIORITY.FG_LOW, signal = null, force = false) => {
const fetchPlayerOrGetFromCache = async (
playerId,
refreshInterval = MINUTE,
priority = PRIORITY.FG_LOW,
signal = null,
force = false,
) => {
const player = await getPlayer(playerId);
if (!player || !isProfileFresh(player, refreshInterval)) {
const fetchedPlayerResponse = await fetchPlayer(playerId, priority, {signal, cacheTtl: MINUTE, maxAge: force ? 0 : refreshInterval, fullResponse: true});
if (isResponseCached(fetchedPlayerResponse)) return getDataFromResponse(fetchedPlayerResponse);
const fetchedPlayerResponse = await fetchPlayer(playerId, priority, {
signal,
cacheTtl: MINUTE,
maxAge: force ? 0 : refreshInterval,
fullResponse: true,
});
if (isResponseCached(fetchedPlayerResponse))
return getDataFromResponse(fetchedPlayerResponse);
return updatePlayer({...player, ...getDataFromResponse(fetchedPlayerResponse), profileLastUpdated: new Date()}, false)
.then(player => {
return updatePlayer(
{
...player,
...getDataFromResponse(fetchedPlayerResponse),
profileLastUpdated: new Date(),
},
false,
).then((player) => {
fetchPlayerAndUpdateRecentPlay(player.playerId);
updatePlayerHistory(player);
return player;
})
});
}
return player;
}
};
const refresh = async (playerId, force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false, addIfNotExists = false) => {
log.trace(`Starting refreshing player "${playerId}" ${force ? ' (forced)' : ''}...`, 'PlayerService')
const refresh = async (
playerId,
force = false,
priority = PRIORITY.BG_NORMAL,
throwErrors = false,
addIfNotExists = false,
) => {
log.trace(
`Starting refreshing player "${playerId}" ${force ? " (forced)" : ""}...`,
"PlayerService",
);
if (!playerId) {
log.warn(`Can not refresh player if an empty playerId is given`, 'PlayerService');
log.warn(
`Can not refresh player if an empty playerId is given`,
"PlayerService",
);
return null;
}
@ -262,66 +395,101 @@ export default () => {
try {
let player = await getPlayer(playerId);
if (!player && !addIfNotExists) {
log.debug(`Profile is not added to DB, skipping.`, 'PlayerService')
log.debug(`Profile is not added to DB, skipping.`, "PlayerService");
return null;
}
log.trace(`Player fetched from DB`, 'PlayerService', player);
log.trace(`Player fetched from DB`, "PlayerService", player);
if (!force) {
const profileFreshnessDate = getProfileFreshnessDate(player);
if (profileFreshnessDate > new Date()) {
log.debug(`Profile is still fresh, skipping. Next refresh on ${formatDate(profileFreshnessDate)}`, 'PlayerService')
log.debug(
`Profile is still fresh, skipping. Next refresh on ${formatDate(
profileFreshnessDate,
)}`,
"PlayerService",
);
return player;
}
}
log.trace(`Fetching player ${playerId} from ScoreSaber...`, 'PlayerService')
log.trace(
`Fetching player ${playerId} from ScoreSaber...`,
"PlayerService",
);
const fetchedPlayer = await fetchPlayer(playerId, priority);
if (!fetchedPlayer || !fetchedPlayer.playerId || !fetchedPlayer.name || !fetchedPlayer.playerInfo || !fetchedPlayer.scoreStats) {
log.warn(`ScoreSaber returned empty info for player ${playerId}`, 'PlayerService')
if (
!fetchedPlayer ||
!fetchedPlayer.playerId ||
!fetchedPlayer.name ||
!fetchedPlayer.playerInfo ||
!fetchedPlayer.scoreStats
) {
log.warn(
`ScoreSaber returned empty info for player ${playerId}`,
"PlayerService",
);
return null;
}
log.trace(`Player fetched`, 'PlayerService', fetchedPlayer);
log.trace(`Player fetched`, "PlayerService", fetchedPlayer);
player = await updatePlayer({...fetchedPlayer, profileLastUpdated: new Date()}, true, addIfNotExists);
player = await updatePlayer(
{ ...fetchedPlayer, profileLastUpdated: new Date() },
true,
addIfNotExists,
);
updatePlayerHistory(player).then(_ => _);
updatePlayerHistory(player).then((_) => _);
log.debug(`Player refreshed.`, 'PlayerService', player);
log.debug(`Player refreshed.`, "PlayerService", player);
return player;
} catch (e) {
if (throwErrors) throw e;
log.debug(`Player refreshing error${e.toString ? `: ${e.toString()}` : ''}`, 'PlayerService', e)
log.debug(
`Player refreshing error${e.toString ? `: ${e.toString()}` : ""}`,
"PlayerService",
e,
);
return null;
}
}
};
const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
log.trace(`Starting refreshing all players${force ? ' (forced)' : ''}...`, 'PlayerService');
const refreshAll = async (
force = false,
priority = PRIORITY.BG_NORMAL,
throwErrors = false,
) => {
log.trace(
`Starting refreshing all players${force ? " (forced)" : ""}...`,
"PlayerService",
);
const allPlayers = await getAll();
if (!allPlayers || !allPlayers.length) {
log.trace(`No players in DB, skipping.`, 'PlayerService');
log.trace(`No players in DB, skipping.`, "PlayerService");
return null;
}
const allRefreshed = await Promise.all(allPlayers.map(player => refresh(player.playerId, force, priority, throwErrors)));
const allRefreshed = await Promise.all(
allPlayers.map((player) =>
refresh(player.playerId, force, priority, throwErrors),
),
);
log.trace(`All players refreshed.`, 'PlayerService', allRefreshed);
log.trace(`All players refreshed.`, "PlayerService", allRefreshed);
return allRefreshed;
}
};
const destroyService = () => {
serviceCreationCount--;
@ -331,7 +499,7 @@ export default () => {
service = null;
}
}
};
service = {
isMainPlayer,
@ -356,7 +524,7 @@ export default () => {
destroyService,
isResponseCached,
getDataFromResponse,
}
};
return service;
}
};

@ -1,6 +1,6 @@
import createScoresService from './scores'
import makePendingPromisePool from '../../utils/pending-promises'
import {getTotalPpFromSortedPps} from '../../utils/scoresaber/pp'
import createScoresService from "./scores";
import makePendingPromisePool from "../../utils/pending-promises";
import { getTotalPpFromSortedPps } from "../../utils/scoresaber/pp";
let service = null;
let serviceCreationCount = 0;
@ -12,18 +12,22 @@ export default () => {
const resolvePromiseOrWaitForPending = makePendingPromisePool();
const getTotalPp = scores => scores && Array.isArray(scores)
const getTotalPp = (scores) =>
scores && Array.isArray(scores)
? getTotalPpFromSortedPps(
scores
.filter(s => s.pp > 0)
.map(s => s.pp)
.filter((s) => s.pp > 0)
.map((s) => s.pp)
.sort((a, b) => b - a),
)
: null;
const getTotalPlayerPp = async (playerId, modifiedScores = {}) => getTotalPp(
const getTotalPlayerPp = async (playerId, modifiedScores = {}) =>
getTotalPp(
Object.values({
...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () => scoresService.getPlayerScoresAsObject(playerId))),
...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () =>
scoresService.getPlayerScoresAsObject(playerId),
)),
...modifiedScores,
}),
);
@ -67,7 +71,7 @@ export default () => {
if (!acc || acc <= 0) {
return 0;
}
let index = ppCurve.findIndex(o => o.at >= acc);
let index = ppCurve.findIndex((o) => o.at >= acc);
if (index === -1) {
return ppCurve[ppCurve.length - 1].value;
}
@ -83,7 +87,7 @@ export default () => {
function accFromPpFactor(ppFactor) {
if (!ppFactor || ppFactor <= 0) return 0;
const idx = ppCurve.findIndex(o => o.value >= ppFactor);
const idx = ppCurve.findIndex((o) => o.value >= ppFactor);
if (idx < 0) return ppCurve[ppCurve.length - 1].at;
const from = ppCurve[idx - 1];
@ -101,7 +105,7 @@ export default () => {
service = null;
}
}
};
service = {
getWhatIfScore,
@ -111,7 +115,7 @@ export default () => {
accFromPpFactor,
PP_PER_STAR,
destroyService,
}
};
return service;
}
};

@ -1,13 +1,17 @@
import {db} from '../../db/db'
import queues from '../../network/queues/queues';
import rankedsPageClient from '../../network/clients/scoresaber/rankeds/page';
import eventBus from '../../utils/broadcast-channel-pubsub'
import {arrayDifference, convertArrayToObjectByKey, opt} from '../../utils/js'
import rankedsRepository from '../../db/repository/rankeds'
import rankedsChangesRepository from '../../db/repository/rankeds-changes'
import keyValueRepository from '../../db/repository/key-value'
import log from '../../utils/logger'
import {addToDate, formatDate, HOUR} from '../../utils/date'
import { db } from "../../db/db";
import queues from "../../network/queues/queues";
import rankedsPageClient from "../../network/clients/scoresaber/rankeds/page";
import eventBus from "../../utils/broadcast-channel-pubsub";
import {
arrayDifference,
convertArrayToObjectByKey,
opt,
} from "../../utils/js";
import rankedsRepository from "../../db/repository/rankeds";
import rankedsChangesRepository from "../../db/repository/rankeds-changes";
import keyValueRepository from "../../db/repository/key-value";
import log from "../../utils/logger";
import { addToDate, formatDate, HOUR } from "../../utils/date";
const REFRESH_INTERVAL = HOUR;
@ -16,16 +20,27 @@ export default () => {
if (service) return service;
const getRankeds = async () => {
const dbRankeds = await rankedsRepository().getAll()
const dbRankeds = await rankedsRepository().getAll();
return dbRankeds ? convertArrayToObjectByKey(dbRankeds, 'leaderboardId') : {}
}
return dbRankeds
? convertArrayToObjectByKey(dbRankeds, "leaderboardId")
: {};
};
const getLastUpdated = async () => keyValueRepository().get('rankedsLastUpdated');
const setLastUpdated = async date => keyValueRepository().set(date, 'rankedsLastUpdated');
const getLastUpdated = async () =>
keyValueRepository().get("rankedsLastUpdated");
const setLastUpdated = async (date) =>
keyValueRepository().set(date, "rankedsLastUpdated");
const refreshRankeds = async (forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
log.trace(`Starting rankeds refreshing${forceUpdate ? ' (forced)' : ''}...`, 'RankedsService')
const refreshRankeds = async (
forceUpdate = false,
priority = queues.PRIORITY.BG_NORMAL,
throwErrors = false,
) => {
log.trace(
`Starting rankeds refreshing${forceUpdate ? " (forced)" : ""}...`,
"RankedsService",
);
try {
let fetchedRankedSongs;
@ -33,40 +48,50 @@ export default () => {
if (!forceUpdate) {
const lastUpdated = await getLastUpdated();
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
log.debug(`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'RankedsService')
log.debug(
`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(
addToDate(REFRESH_INTERVAL, lastUpdated),
)}`,
"RankedsService",
);
return null;
}
}
log.trace(`Fetching current rankeds from ScoreSaber...`, 'RankedsService')
log.trace(
`Fetching current rankeds from ScoreSaber...`,
"RankedsService",
);
fetchedRankedSongs = await rankedsPageClient.getProcessed({ priority });
if (!fetchedRankedSongs || !fetchedRankedSongs.length) {
log.warn(`ScoreSaber returned empty rankeds list`, 'RankedsService')
log.warn(`ScoreSaber returned empty rankeds list`, "RankedsService");
return null;
}
log.trace('Fetching rankeds from DB', 'RankedsService');
log.trace("Fetching rankeds from DB", "RankedsService");
const oldRankedSongs = await getRankeds();
// add firstSeen & oldStars properties
fetchedRankedSongs = convertArrayToObjectByKey(
fetchedRankedSongs.map(s => {
const firstSeen = oldRankedSongs[s.leaderboardId] && oldRankedSongs[s.leaderboardId].firstSeen
fetchedRankedSongs.map((s) => {
const firstSeen =
oldRankedSongs[s.leaderboardId] &&
oldRankedSongs[s.leaderboardId].firstSeen
? oldRankedSongs[s.leaderboardId].firstSeen
: new Date();
return {...s, firstSeen, oldStars: null}
return { ...s, firstSeen, oldStars: null };
}),
'leaderboardId',
"leaderboardId",
);
// find differences between old and new ranked songs
const newRankeds = arrayDifference(
Object.keys(fetchedRankedSongs),
Object.keys(oldRankedSongs),
).map(leaderboardId => ({
).map((leaderboardId) => ({
leaderboardId: parseInt(leaderboardId, 10),
oldStars: null,
stars: fetchedRankedSongs[leaderboardId].stars,
@ -74,77 +99,99 @@ export default () => {
}));
if (newRankeds && newRankeds.length)
log.debug(`${newRankeds.length} ranked(s) found`, 'RankedsService');
log.debug(`${newRankeds.length} ranked(s) found`, "RankedsService");
const changed =
// concat new rankeds with changed rankeds
newRankeds
.concat(
newRankeds.concat(
Object.values(oldRankedSongs)
.filter(s => s.stars !== (fetchedRankedSongs[s.leaderboardId] ? opt(fetchedRankedSongs[s.leaderboardId], 'stars', null) : null))
.map(s => ({
.filter(
(s) =>
s.stars !==
(fetchedRankedSongs[s.leaderboardId]
? opt(fetchedRankedSongs[s.leaderboardId], "stars", null)
: null),
)
.map((s) => ({
leaderboardId: s.leaderboardId,
oldStars: s.stars,
stars: opt(fetchedRankedSongs[s.leaderboardId], 'stars', null),
stars: opt(fetchedRankedSongs[s.leaderboardId], "stars", null),
timestamp: Date.now(),
}),
)
})),
);
if (newRankeds && changed && changed.length - newRankeds.length > 0)
log.debug(`${changed.length - newRankeds.length} changed ranked(s) found`, 'RankedsService');
log.debug(
`${changed.length - newRankeds.length} changed ranked(s) found`,
"RankedsService",
);
const changedLeaderboards = changed
.map(s => {
const ranked = fetchedRankedSongs[s.leaderboardId] ? fetchedRankedSongs[s.leaderboardId] : oldRankedSongs[s.leaderboardId];
.map((s) => {
const ranked = fetchedRankedSongs[s.leaderboardId]
? fetchedRankedSongs[s.leaderboardId]
: oldRankedSongs[s.leaderboardId];
return {
...ranked,
...s,
}
},
)
.filter(s => s && s.hash)
.map(l => {
};
})
.filter((s) => s && s.hash)
.map((l) => {
const { oldStars, timestamp, ...leaderboard } = l;
return leaderboard;
});
log.trace('Saving rankeds to DB...', 'RankedsService');
log.trace("Saving rankeds to DB...", "RankedsService");
await db.runInTransaction(['rankeds', 'rankeds-changes', 'key-value'], async tx => {
await Promise.all(changedLeaderboards.map(async ranked => rankedsRepository().set(ranked, undefined, tx)));
await Promise.all(changed.map(async rc => rankedsChangesRepository().set(rc, undefined, tx)));
await setLastUpdated(new Date())
});
await db.runInTransaction(
["rankeds", "rankeds-changes", "key-value"],
async (tx) => {
await Promise.all(
changedLeaderboards.map(async (ranked) =>
rankedsRepository().set(ranked, undefined, tx),
),
);
await Promise.all(
changed.map(async (rc) =>
rankedsChangesRepository().set(rc, undefined, tx),
),
);
await setLastUpdated(new Date());
},
);
log.trace('Rankeds saved', 'RankedsService');
log.trace("Rankeds saved", "RankedsService");
if (changed.length) {
eventBus.publish('rankeds-changed', {changed, allRankeds: fetchedRankedSongs});
eventBus.publish("rankeds-changed", {
changed,
allRankeds: fetchedRankedSongs,
});
}
log.debug(`Rankeds refreshing complete.`, 'RankedsService')
log.debug(`Rankeds refreshing complete.`, "RankedsService");
return changed;
} catch (e) {
if (throwErrors) throw e;
log.debug(`Rankeds refreshing error`, 'RankedsService', e)
log.debug(`Rankeds refreshing error`, "RankedsService", e);
return null;
}
}
};
const destroyService = () => {
service = null;
}
};
service = {
get: getRankeds,
refresh: refreshRankeds,
destroyService,
}
};
return service;
}
};

@ -1,10 +1,10 @@
import playersGlobalRankingApiClient from '../../network/clients/scoresaber/players/api-ranking-global'
import playersGlobalRankingPagesApiClient from '../../network/clients/scoresaber/players/api-ranking-global-pages'
import playersCountryRankingPageClient from '../../network/clients/scoresaber/players/page-ranking-country'
import makePendingPromisePool from '../../utils/pending-promises'
import {PRIORITY} from '../../network/queues/http-queue'
import {PLAYERS_PER_PAGE} from '../../utils/scoresaber/consts'
import {opt} from '../../utils/js'
import playersGlobalRankingApiClient from "../../network/clients/scoresaber/players/api-ranking-global";
import playersGlobalRankingPagesApiClient from "../../network/clients/scoresaber/players/api-ranking-global-pages";
import playersCountryRankingPageClient from "../../network/clients/scoresaber/players/page-ranking-country";
import makePendingPromisePool from "../../utils/pending-promises";
import { PRIORITY } from "../../network/queues/http-queue";
import { PLAYERS_PER_PAGE } from "../../utils/scoresaber/consts";
import { opt } from "../../utils/js";
let service = null;
export default () => {
@ -12,24 +12,52 @@ export default () => {
const resolvePromiseOrWaitForPending = makePendingPromisePool();
const fetchGlobal = async (page = 1, priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`apiClient/ranking/global/${page}`, () => playersGlobalRankingApiClient.getProcessed({page, signal, priority}));
const fetchGlobal = async (
page = 1,
priority = PRIORITY.FG_LOW,
signal = null,
) =>
resolvePromiseOrWaitForPending(`apiClient/ranking/global/${page}`, () =>
playersGlobalRankingApiClient.getProcessed({ page, signal, priority }),
);
const fetchCountry = async (country, page = 1, priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`pageClient/ranking/${country}/${page}`, () => playersCountryRankingPageClient.getProcessed({country, page, signal, priority}));
const fetchCountry = async (
country,
page = 1,
priority = PRIORITY.FG_LOW,
signal = null,
) =>
resolvePromiseOrWaitForPending(
`pageClient/ranking/${country}/${page}`,
() =>
playersCountryRankingPageClient.getProcessed({
country,
page,
signal,
priority,
}),
);
const fetchGlobalPages = async (priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`apiClient/rankingGlobalPages`, () => playersGlobalRankingPagesApiClient.getProcessed({signal, priority}));
const fetchGlobalPages = async (priority = PRIORITY.FG_LOW, signal = null) =>
resolvePromiseOrWaitForPending(`apiClient/rankingGlobalPages`, () =>
playersGlobalRankingPagesApiClient.getProcessed({ signal, priority }),
);
const fetchGlobalCount = async (priority = PRIORITY.FG_LOW, signal = null) => {
const fetchGlobalCount = async (
priority = PRIORITY.FG_LOW,
signal = null,
) => {
const pages = await fetchGlobalPages(priority, signal);
if (!pages || !Number.isFinite(pages)) return 0;
return pages * PLAYERS_PER_PAGE;
}
};
async function fetchMiniRanking(rank, country = null, numOfPlayers = 5) {
try {
if (!Number.isFinite(numOfPlayers)) numOfPlayers = 5;
const getPage = rank => Math.floor((rank - 1) / PLAYERS_PER_PAGE) + 1;
const getPage = (rank) => Math.floor((rank - 1) / PLAYERS_PER_PAGE) + 1;
const playerPage = getPage(rank);
let firstPlayerRank = rank - (numOfPlayers - (numOfPlayers > 2 ? 2 : 1));
@ -38,15 +66,23 @@ export default () => {
const lastPlayerRank = firstPlayerRank + numOfPlayers - 1;
const lastPlayerRankPage = getPage(lastPlayerRank);
const pages = [...new Set([playerPage, firstPlayerRankPage, lastPlayerRankPage])].filter(p => p);
const pages = [
...new Set([playerPage, firstPlayerRankPage, lastPlayerRankPage]),
].filter((p) => p);
const ranking = (await Promise.all(pages.map(async page => (country ? fetchCountry(country, page) : fetchGlobal(page)))))
const ranking = (
await Promise.all(
pages.map(async (page) =>
country ? fetchCountry(country, page) : fetchGlobal(page),
),
)
)
.reduce((cum, arr) => cum.concat(arr), [])
.filter(player => {
const rank = opt(player, 'playerInfo.rank')
.filter((player) => {
const rank = opt(player, "playerInfo.rank");
return rank >= firstPlayerRank && rank <= lastPlayerRank;
})
.sort((a,b) => opt(a, 'playerInfo.rank') - opt(b, 'playerInfo.rank'))
.sort((a, b) => opt(a, "playerInfo.rank") - opt(b, "playerInfo.rank"));
return ranking;
} catch (err) {
@ -56,7 +92,7 @@ export default () => {
const destroyService = () => {
service = null;
}
};
service = {
getGlobal: fetchGlobal,
@ -66,7 +102,7 @@ export default () => {
getMiniRanking: fetchMiniRanking,
PLAYERS_PER_PAGE,
destroyService,
}
};
return service;
}
};

File diff suppressed because it is too large Load Diff

@ -1,16 +1,23 @@
import queues from '../network/queues/queues';
import keyValueRepository from '../db/repository/key-value'
import twitchRepository from '../db/repository/twitch'
import createPlayerService from '../services/scoresaber/player'
import profileApiClient from '../network/clients/twitch/api-profile'
import videosApiClient from '../network/clients/twitch/api-videos'
import eventBus from '../utils/broadcast-channel-pubsub'
import log from '../utils/logger'
import {addToDate, dateFromString, durationToMillis, formatDate, millisToDuration, MINUTE} from '../utils/date'
import {PRIORITY} from '../network/queues/http-queue'
import makePendingPromisePool from '../utils/pending-promises'
import queues from "../network/queues/queues";
import keyValueRepository from "../db/repository/key-value";
import twitchRepository from "../db/repository/twitch";
import createPlayerService from "../services/scoresaber/player";
import profileApiClient from "../network/clients/twitch/api-profile";
import videosApiClient from "../network/clients/twitch/api-videos";
import eventBus from "../utils/broadcast-channel-pubsub";
import log from "../utils/logger";
import {
addToDate,
dateFromString,
durationToMillis,
formatDate,
millisToDuration,
MINUTE,
} from "../utils/date";
import { PRIORITY } from "../network/queues/http-queue";
import makePendingPromisePool from "../utils/pending-promises";
const TWITCH_TOKEN_KEY = 'twitchToken';
const TWITCH_TOKEN_KEY = "twitchToken";
const REFRESH_INTERVAL = 5 * MINUTE;
@ -24,31 +31,38 @@ export default () => {
const playerService = createPlayerService();
const getAuthUrl = (state = '', scopes = '') => queues.TWITCH.getAuthUrl(state, scopes)
const getAuthUrl = (state = "", scopes = "") =>
queues.TWITCH.getAuthUrl(state, scopes);
const getTwitchTokenFromUrl = () => {
const url = (new URL(document.location));
const url = new URL(document.location);
const error = url.searchParams.get('error')
const error = url.searchParams.get("error");
if (error) {
const errorMsg = url.searchParams.get('error_description');
const errorMsg = url.searchParams.get("error_description");
throw new Error(errorMsg ? errorMsg : error);
}
const hash = url.hash;
if (!hash || !hash.length) throw new Error("Twitch did not return access token")
if (!hash || !hash.length)
throw new Error("Twitch did not return access token");
const accessTokenMatch = /access_token=(.*?)(&|$)/.exec(hash);
if (!accessTokenMatch) throw new Error("Twitch did not return access token")
if (!accessTokenMatch)
throw new Error("Twitch did not return access token");
const stateMatch = /state=(.*?)(&|$)/.exec(hash);
return {accessToken: accessTokenMatch[1], url: stateMatch ? decodeURIComponent(stateMatch[1]) : ''};
}
return {
accessToken: accessTokenMatch[1],
url: stateMatch ? decodeURIComponent(stateMatch[1]) : "",
};
};
const processToken = async accessToken => {
const processToken = async (accessToken) => {
// validate token
const tokenValidation = (await queues.TWITCH.validateToken(accessToken)).body;
const tokenValidation = (await queues.TWITCH.validateToken(accessToken))
.body;
const expiresIn = tokenValidation.expires_in * 1000;
@ -58,39 +72,77 @@ export default () => {
obtained: new Date(),
expires: new Date(Date.now() + expiresIn),
expires_in: expiresIn,
}
};
await keyValueRepository().set(twitchToken, TWITCH_TOKEN_KEY);
eventBus.publish('twitch-token-refreshed', twitchToken)
eventBus.publish("twitch-token-refreshed", twitchToken);
return twitchToken
}
return twitchToken;
};
const getCurrentToken = async () => keyValueRepository().get(TWITCH_TOKEN_KEY, true);
const getCurrentToken = async () =>
keyValueRepository().get(TWITCH_TOKEN_KEY, true);
const fetchProfile = async (login, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => {
const fetchProfile = async (
login,
priority = PRIORITY.FG_LOW,
{ fullResponse = false, ...options } = {},
) => {
const token = await getCurrentToken();
if (!token || !token.expires || token.expires <= new Date()) return null;
return resolvePromiseOrWaitForPending(`profileApiClient/${login}/${fullResponse}`, () => profileApiClient.getProcessed({...options, accessToken: token.accessToken, login, priority, fullResponse}));
}
return resolvePromiseOrWaitForPending(
`profileApiClient/${login}/${fullResponse}`,
() =>
profileApiClient.getProcessed({
...options,
accessToken: token.accessToken,
login,
priority,
fullResponse,
}),
);
};
const fetchVideos = async (userId, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => {
const fetchVideos = async (
userId,
priority = PRIORITY.FG_LOW,
{ fullResponse = false, ...options } = {},
) => {
const token = await getCurrentToken();
if (!token || !token.expires || token.expires <= new Date()) return null;
return resolvePromiseOrWaitForPending(`videosApiClient/${userId}/${fullResponse}`, () => videosApiClient.getProcessed({...options, accessToken: token.accessToken, userId, priority, fullResponse}));
}
return resolvePromiseOrWaitForPending(
`videosApiClient/${userId}/${fullResponse}`,
() =>
videosApiClient.getProcessed({
...options,
accessToken: token.accessToken,
userId,
priority,
fullResponse,
}),
);
};
const getPlayerProfile = async playerId => twitchRepository().get(playerId);
const updatePlayerProfile = async twitchProfile => twitchRepository().set(twitchProfile);
const getPlayerProfile = async (playerId) => twitchRepository().get(playerId);
const updatePlayerProfile = async (twitchProfile) =>
twitchRepository().set(twitchProfile);
const refresh = async (playerId, forceUpdate = false, priority = queues.PRIORITY.FG_LOW, throwErrors = false) => {
log.trace(`Starting Twitch videos refreshing${forceUpdate ? ' (forced)' : ''}...`, 'TwitchService')
const refresh = async (
playerId,
forceUpdate = false,
priority = queues.PRIORITY.FG_LOW,
throwErrors = false,
) => {
log.trace(
`Starting Twitch videos refreshing${forceUpdate ? " (forced)" : ""}...`,
"TwitchService",
);
if (!playerId) {
log.debug(`No playerId provided, skipping`, 'TwitchService')
log.debug(`No playerId provided, skipping`, "TwitchService");
return null;
}
@ -98,7 +150,10 @@ export default () => {
try {
let twitchProfile = await twitchRepository().get(playerId);
if (!twitchProfile || !twitchProfile.login) {
log.debug(`Twitch profile for player ${playerId} is not set, skipping`, 'TwitchService')
log.debug(
`Twitch profile for player ${playerId} is not set, skipping`,
"TwitchService",
);
return null;
}
@ -106,7 +161,12 @@ export default () => {
const lastUpdated = twitchProfile.lastUpdated;
if (!forceUpdate) {
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
log.debug(`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'TwitchService')
log.debug(
`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(
addToDate(REFRESH_INTERVAL, lastUpdated),
)}`,
"TwitchService",
);
return twitchProfile;
}
@ -115,7 +175,10 @@ export default () => {
const player = playerService.get(playerId);
if (player && player.recentPlay) {
if (lastUpdated && lastUpdated > player.recentPlay) {
log.debug(`Twitch updated after recent player play, skipping`, 'TwitchService')
log.debug(
`Twitch updated after recent player play, skipping`,
"TwitchService",
);
return twitchProfile;
}
@ -124,7 +187,10 @@ export default () => {
if (!twitchProfile.id) {
const fetchedProfile = await fetchProfile(twitchProfile.login);
if (!fetchedProfile) {
log.debug(`Can not fetch Twitch profile for player ${playerId}, skipping`, 'TwitchService')
log.debug(
`Can not fetch Twitch profile for player ${playerId}, skipping`,
"TwitchService",
);
return twitchProfile;
}
@ -141,7 +207,7 @@ export default () => {
await updatePlayerProfile(twitchProfile);
if (videos && videos.length) {
eventBus.publish('player-twitch-videos-updated', {
eventBus.publish("player-twitch-videos-updated", {
playerId,
twitchProfile,
});
@ -151,25 +217,46 @@ export default () => {
} catch (e) {
if (throwErrors) throw e;
log.debug(`Twitch player ${playerId} refreshing error`, 'TwitchService', e)
log.debug(
`Twitch player ${playerId} refreshing error`,
"TwitchService",
e,
);
return null;
}
}
};
async function findTwitchVideo(playerTwitchProfile, timeset, songLength) {
if (!playerTwitchProfile || !playerTwitchProfile.videos || !timeset || !songLength) return null;
if (
!playerTwitchProfile ||
!playerTwitchProfile.videos ||
!timeset ||
!songLength
)
return null;
const songStarted = addToDate(-songLength * 1000, timeset)
const songStarted = addToDate(-songLength * 1000, timeset);
const video = playerTwitchProfile.videos
.map(v => ({
.map((v) => ({
...v,
created_at: dateFromString(v.created_at),
ended_at: addToDate(durationToMillis(v.duration), dateFromString(v.created_at)),
ended_at: addToDate(
durationToMillis(v.duration),
dateFromString(v.created_at),
),
}))
.find(v => v.created_at <= songStarted && songStarted < v.ended_at);
.find((v) => v.created_at <= songStarted && songStarted < v.ended_at);
return video ? {...video, url: video.url + '?t=' + millisToDuration(songStarted - video.created_at)} : null;
return video
? {
...video,
url:
video.url +
"?t=" +
millisToDuration(songStarted - video.created_at),
}
: null;
}
const destroyService = () => {
@ -179,7 +266,7 @@ export default () => {
service = null;
playerService.destroyService();
}
}
};
service = {
getAuthUrl,
@ -192,7 +279,7 @@ export default () => {
findTwitchVideo,
refresh,
destroyService,
}
};
return service;
}
};

@ -1,43 +1,67 @@
import {DateTime} from 'luxon';
import { DateTime } from "luxon";
export const getServicePlayerGain = (playerHistory, dateTruncFunc, dateKey, daysAgo = 1, maxDaysAgo = 7) => {
export const getServicePlayerGain = (
playerHistory,
dateTruncFunc,
dateKey,
daysAgo = 1,
maxDaysAgo = 7,
) => {
if (!playerHistory?.length) return null;
let todayServiceMidnightDate = dateTruncFunc(new Date());
const compareToDate = DateTime.fromJSDate(todayServiceMidnightDate).minus({days: daysAgo}).toJSDate();
const maxServiceDate = DateTime.fromJSDate(todayServiceMidnightDate).minus({days: maxDaysAgo}).toJSDate();
const compareToDate = DateTime.fromJSDate(todayServiceMidnightDate)
.minus({ days: daysAgo })
.toJSDate();
const maxServiceDate = DateTime.fromJSDate(todayServiceMidnightDate)
.minus({ days: maxDaysAgo })
.toJSDate();
return playerHistory
.sort((a, b) => b?.[dateKey]?.getTime() - a?.[dateKey]?.getTime())
.find(h => h?.[dateKey] <= compareToDate && h?.[dateKey] >= maxServiceDate);
}
.find(
(h) => h?.[dateKey] <= compareToDate && h?.[dateKey] >= maxServiceDate,
);
};
export const serviceFilterFunc = serviceParams => s => {
export const serviceFilterFunc = (serviceParams) => (s) => {
// accept score if there is no non-empty filter
if (!Object.entries(serviceParams?.filters ?? {})?.filter(([key, val]) => val)?.length) return true;
if (
!Object.entries(serviceParams?.filters ?? {})?.filter(([key, val]) => val)
?.length
)
return true;
let filterVal = true;
if (serviceParams?.filters?.search?.length) {
const song = s?.leaderboard?.song ?? null;
if (song) {
const name = `${song?.name?.toLowerCase() ?? ''} ${song?.subName?.toLowerCase() ?? ''} ${song?.authorName?.toLowerCase() ?? ''} ${song?.levelAuthorName?.toLowerCase() ?? ''}`
const name = `${song?.name?.toLowerCase() ?? ""} ${
song?.subName?.toLowerCase() ?? ""
} ${song?.authorName?.toLowerCase() ?? ""} ${
song?.levelAuthorName?.toLowerCase() ?? ""
}`;
filterVal &= name.indexOf(serviceParams.filters.search.toLowerCase()) >= 0;
filterVal &=
name.indexOf(serviceParams.filters.search.toLowerCase()) >= 0;
} else {
filterVal &= false;
}
}
if (serviceParams?.filters.diff?.length) {
filterVal &= s?.leaderboard?.diffInfo?.diff?.toLowerCase() === serviceParams.filters.diff?.toLowerCase()
filterVal &=
s?.leaderboard?.diffInfo?.diff?.toLowerCase() ===
serviceParams.filters.diff?.toLowerCase();
}
if (serviceParams?.filters?.songType?.length) {
filterVal &= (serviceParams.filters.songType === 'ranked' && s?.pp > 0) ||
(serviceParams.filters.songType === 'unranked' && (s?.pp ?? 0) === 0)
filterVal &=
(serviceParams.filters.songType === "ranked" && s?.pp > 0) ||
(serviceParams.filters.songType === "unranked" && (s?.pp ?? 0) === 0);
}
return filterVal;
}
};

@ -1,21 +1,22 @@
import {writable} from 'svelte/store'
import keyValueRepository from '../db/repository/key-value';
import {opt} from '../utils/js'
import { writable } from "svelte/store";
import keyValueRepository from "../db/repository/key-value";
import { opt } from "../utils/js";
const STORE_CONFIG_KEY = 'config';
const STORE_CONFIG_KEY = "config";
export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_LOCALE = "en-US";
export let configStore = null;
const locales = {
'de-DE': {id: 'de-DE', name: 'Deutschland'},
'es-ES': {id: 'es-ES', name: 'España'},
'pl-PL': {id: 'pl-PL', name: 'Polska'},
'en-GB': {id: 'en-GB', name: 'United Kingdom'},
'en-US': {id: 'en-US', name: 'United States'},
"de-DE": { id: "de-DE", name: "Deutschland" },
"es-ES": { id: "es-ES", name: "España" },
"pl-PL": { id: "pl-PL", name: "Polska" },
"en-GB": { id: "en-GB", name: "United Kingdom" },
"en-US": { id: "en-US", name: "United States" },
};
export const getCurrentLocale = () => configStore ? configStore.getLocale() : DEFAULT_LOCALE;
export const getCurrentLocale = () =>
configStore ? configStore.getLocale() : DEFAULT_LOCALE;
export const getSupportedLocales = () => Object.values(locales);
const DEFAULT_CONFIG = {
@ -24,21 +25,21 @@ const DEFAULT_CONFIG = {
country: null,
},
scoreComparison: {
method: 'in-place',
method: "in-place",
},
preferences: {
secondaryPp: 'attribution',
avatarIcons: 'only-if-needed',
secondaryPp: "attribution",
avatarIcons: "only-if-needed",
},
locale: DEFAULT_LOCALE,
}
};
const newSettingsAvailableDefinition = {
'scoreComparison.method': 'Method of displaying the comparison of scores',
'preferences.secondaryPp': 'Setting the second PP metric',
'preferences.avatarIcons': 'Showing icons on avatars',
'locale': 'Locale selection',
}
"scoreComparison.method": "Method of displaying the comparison of scores",
"preferences.secondaryPp": "Setting the second PP metric",
"preferences.avatarIcons": "Showing icons on avatars",
locale: "Locale selection",
};
export default async () => {
if (configStore) return configStore;
@ -49,16 +50,17 @@ export default async () => {
const { subscribe, set: storeSet } = writable(currentConfig);
const get = key => key ? (currentConfig[key] ? currentConfig[key] : null) : currentConfig;
const get = (key) =>
key ? (currentConfig[key] ? currentConfig[key] : null) : currentConfig;
const set = async (config, persist = true) => {
const newConfig = { ...DEFAULT_CONFIG };
Object.keys(config).forEach(key => {
if (key === 'locale') {
Object.keys(config).forEach((key) => {
if (key === "locale") {
newConfig[key] = config?.[key] ?? newConfig?.[key] ?? DEFAULT_LOCALE;
return;
}
newConfig[key] = {...newConfig?.[key], ...config?.[key]}
newConfig[key] = { ...newConfig?.[key], ...config?.[key] };
});
if (persist) await keyValueRepository().set(newConfig, STORE_CONFIG_KEY);
@ -69,27 +71,31 @@ export default async () => {
storeSet(newConfig);
return newConfig;
}
};
const getLocale = () => opt(currentConfig, 'locale', DEFAULT_LOCALE);
const getLocale = () => opt(currentConfig, "locale", DEFAULT_LOCALE);
const determineNewSettingsAvailable = dbConfig => Object.entries(newSettingsAvailableDefinition)
.map(([key, description]) => opt(dbConfig, key) === undefined ? description : null)
.filter(d => d)
const determineNewSettingsAvailable = (dbConfig) =>
Object.entries(newSettingsAvailableDefinition)
.map(([key, description]) =>
opt(dbConfig, key) === undefined ? description : null,
)
.filter((d) => d);
const dbConfig = await keyValueRepository().get(STORE_CONFIG_KEY);
const newSettings = determineNewSettingsAvailable(dbConfig);
if (dbConfig) await set(dbConfig, false);
newSettingsAvailable = newSettings && newSettings.length ? newSettings : undefined;
newSettingsAvailable =
newSettings && newSettings.length ? newSettings : undefined;
configStore = {
subscribe,
set,
get,
getMainPlayerId: () => opt(currentConfig, 'users.main'),
getMainPlayerId: () => opt(currentConfig, "users.main"),
getLocale,
getNewSettingsAvailable: () => newSettingsAvailable,
}
};
return configStore;
}
};

@ -1,7 +1,9 @@
import {writable} from 'svelte/store'
import { writable } from "svelte/store";
export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => {
const defaultValue = {name: null, width: null, nodeWidth: null, rect: null}
export default (
sizes = { phone: 0, tablet: 768, desktop: 1024, xxl: 1749 },
) => {
const defaultValue = { name: null, width: null, nodeWidth: null, rect: null };
const { subscribe, unsubscribe, set } = writable(defaultValue);
let ro = null;
@ -10,12 +12,12 @@ export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => {
const unobserve = () => {
if (!node) return;
ro.unobserve(node)
ro.unobserve(node);
node = null;
}
};
const observe = nodeToObserve => {
const observe = (nodeToObserve) => {
if (!nodeToObserve) return null;
if (node) unobserve();
@ -34,19 +36,25 @@ export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => {
set(
Object.entries(sizes)
.sort((a, b) => a[1] - b[1])
.reduce((cum, item) => item[1] <= nodeWidth ? {name: item[0], width: item[1], nodeWidth, rect} : cum, defaultValue),
)
.reduce(
(cum, item) =>
item[1] <= nodeWidth
? { name: item[0], width: item[1], nodeWidth, rect }
: cum,
defaultValue,
),
);
});
ro.observe(node)
ro.observe(node);
return node;
}
};
return {
subscribe,
unsubscribe,
observe,
unobserve,
}
};
};

@ -1,12 +1,18 @@
import {getFixedLeaderboardMaxScore, getMaxScore} from '../../../../utils/scoresaber/song'
import {
getFixedLeaderboardMaxScore,
getMaxScore,
} from "../../../../utils/scoresaber/song";
export default (score, bmStats, leaderboardId) => {
let maxScore;
if (bmStats && bmStats.notes) {
maxScore = getMaxScore(bmStats.notes)
maxScore = getMaxScore(bmStats.notes);
} else if (leaderboardId) {
maxScore = getFixedLeaderboardMaxScore(leaderboardId, score?.maxScore ?? null)
maxScore = getFixedLeaderboardMaxScore(
leaderboardId,
score?.maxScore ?? null,
);
}
if (maxScore) {
@ -17,14 +23,14 @@ export default (score, bmStats, leaderboardId) => {
if (!unmodifiedScore) unmodifiedScore = score?.score ?? null;
if (unmodifiedScore && score.maxScore) {
score.acc = unmodifiedScore ? unmodifiedScore / maxScore * 100 : null;
score.acc = unmodifiedScore ? (unmodifiedScore / maxScore) * 100 : null;
if (score.score) score.percentage = score.score / score.maxScore * 100;
if (score.score) score.percentage = (score.score / score.maxScore) * 100;
}
if (score?.score && score?.maxScore) {
score.percentage = score.score / score.maxScore * 100;
score.percentage = (score.score / score.maxScore) * 100;
}
return score;
}
};

@ -1,10 +1,14 @@
import createBeatMapsService from '../../../../services/beatmaps'
import {opt} from '../../../../utils/js'
import createBeatMapsService from "../../../../services/beatmaps";
import { opt } from "../../../../utils/js";
const beatMaps = createBeatMapsService();
export default async (data, cachedOnly = false) => {
if (!opt(data, 'leaderboard.song.hash.length')) return;
if (!opt(data, "leaderboard.song.hash.length")) return;
data.leaderboard.beatMaps = await beatMaps.byHash(data.leaderboard.song.hash, false, cachedOnly);
}
data.leaderboard.beatMaps = await beatMaps.byHash(
data.leaderboard.song.hash,
false,
cachedOnly,
);
};

@ -1,5 +1,5 @@
import createRankedsStore from '../../../../stores/scoresaber/rankeds'
import {opt} from '../../../../utils/js'
import createRankedsStore from "../../../../stores/scoresaber/rankeds";
import { opt } from "../../../../utils/js";
let rankeds;
@ -10,8 +10,11 @@ export default async (data) => {
if (!rankeds) return;
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
const leaderboardId = opt(data, "leaderboard.leaderboardId");
if (!leaderboardId) return;
data.leaderboard.stars = rankeds[leaderboardId] && rankeds[leaderboardId].stars ? rankeds[leaderboardId].stars : null;
}
data.leaderboard.stars =
rankeds[leaderboardId] && rankeds[leaderboardId].stars
? rankeds[leaderboardId].stars
: null;
};

@ -1,16 +1,22 @@
import {opt} from '../../../../utils/js'
import calculateAcc from '../common/acc-calc'
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
import { opt } from "../../../../utils/js";
import calculateAcc from "../common/acc-calc";
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
export default async (data) => {
if (!data || !data.score) return;
const leaderboardId = opt(data, 'leaderboard.leaderboardId')
const diffInfo = opt(data, 'leaderboard.diffInfo');
const leaderboardId = opt(data, "leaderboard.leaderboardId");
const diffInfo = opt(data, "leaderboard.diffInfo");
const versions = opt(data, 'leaderboard.beatMaps.versions')
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), diffInfo);
const versions = opt(data, "leaderboard.beatMaps.versions");
const versionsLastIdx =
versions && Array.isArray(versions) && versions.length
? versions.length - 1
: 0;
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(
opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`),
diffInfo,
);
data.score = calculateAcc(data.score, bmStats, leaderboardId);
}
};

@ -1,6 +1,6 @@
import createBeatSaviorService from '../../../../services/beatsavior'
import {opt} from '../../../../utils/js'
import {PRIORITY} from '../../../../network/queues/http-queue'
import createBeatSaviorService from "../../../../services/beatsavior";
import { opt } from "../../../../utils/js";
import { PRIORITY } from "../../../../network/queues/http-queue";
let beatSaviorService;
@ -15,19 +15,19 @@ export default async (data, playerId = null) => {
if (!bsData) return;
if (bsData?.stats)
['left', 'right'].forEach(hand => {
['Preswing', 'Postswing'].forEach(stat => {
["left", "right"].forEach((hand) => {
["Preswing", "Postswing"].forEach((stat) => {
const key = `${hand}${stat}`;
if (!bsData?.stats?.[key])
bsData.stats[key] = bsData?.trackers?.accuracyTracker?.[key] ?? null;
})
})
});
});
const acc = opt(bsData, 'trackers.scoreTracker.rawRatio');
const acc = opt(bsData, "trackers.scoreTracker.rawRatio");
if (acc) data.score.acc = acc * 100;
const percentage = opt(bsData, 'trackers.scoreTracker.modifiedRatio');
const percentage = opt(bsData, "trackers.scoreTracker.modifiedRatio");
if (percentage) data.score.percentage = percentage * 100;
data.beatSavior = bsData;
}
};

@ -1,10 +1,10 @@
import {configStore} from '../../../config'
import createScoresService from '../../../../services/scoresaber/scores'
import accEnhancer from './acc'
import beatSaviorEnhancer from './beatsavior'
import beatMapsEnhancer from '../common/beatmaps'
import {opt} from '../../../../utils/js'
import produce from 'immer'
import { configStore } from "../../../config";
import createScoresService from "../../../../services/scoresaber/scores";
import accEnhancer from "./acc";
import beatSaviorEnhancer from "./beatsavior";
import beatMapsEnhancer from "../common/beatmaps";
import { opt } from "../../../../utils/js";
import produce from "immer";
let scoresService = null;
let mainPlayerId = null;
@ -17,20 +17,29 @@ export const initCompareEnhancer = async () => {
scoresService = createScoresService();
configStoreUnsubscribe = configStore.subscribe(async config => {
const newMainPlayerId = opt(config, 'users.main')
configStoreUnsubscribe = configStore.subscribe(async (config) => {
const newMainPlayerId = opt(config, "users.main");
if (mainPlayerId !== newMainPlayerId) {
mainPlayerId = newMainPlayerId;
if (!playerScores[mainPlayerId]) playerScores[mainPlayerId] = await scoresService.getPlayerScoresAsObject(mainPlayerId);
}
})
if (!playerScores[mainPlayerId])
playerScores[mainPlayerId] =
await scoresService.getPlayerScoresAsObject(mainPlayerId);
}
});
};
export default async (data, playerId = null) => {
if (!data || !data.score || data.comparePlayers || !mainPlayerId || mainPlayerId === playerId) return;
if (
!data ||
!data.score ||
data.comparePlayers ||
!mainPlayerId ||
mainPlayerId === playerId
)
return;
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
const leaderboardId = opt(data, "leaderboard.leaderboardId");
if (!leaderboardId) return;
const comparePlayerScores = await playerScores[mainPlayerId];
@ -38,14 +47,15 @@ export default async (data, playerId = null) => {
const mainPlayerScore = await produce(
await produce(
await produce(
comparePlayerScores[leaderboardId],
draft => beatMapsEnhancer(draft),
await produce(comparePlayerScores[leaderboardId], (draft) =>
beatMapsEnhancer(draft),
),
draft => accEnhancer(draft, true),
(draft) => accEnhancer(draft, true),
),
draft => beatSaviorEnhancer(draft, mainPlayerId),
(draft) => beatSaviorEnhancer(draft, mainPlayerId),
);
data.comparePlayers = [{...mainPlayerScore, playerId: mainPlayerId, playerName: 'Me'}];
}
data.comparePlayers = [
{ ...mainPlayerScore, playerId: mainPlayerId, playerName: "Me" },
];
};

@ -1,7 +1,7 @@
import createScoresService from '../../../../services/scoresaber/scores';
import calculateAcc from '../common/acc-calc'
import {opt} from '../../../../utils/js'
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
import createScoresService from "../../../../services/scoresaber/scores";
import calculateAcc from "../common/acc-calc";
import { opt } from "../../../../utils/js";
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
let scoresService;
@ -10,27 +10,39 @@ export default async (data, playerId = null) => {
if (data.prevScore) delete data.prevScore;
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
const leaderboardId = opt(data, "leaderboard.leaderboardId");
if (!scoresService) scoresService = createScoresService();
const playerScores = scoresService.convertScoresToObject(await scoresService.getPlayerScores(playerId));
const playerScores = scoresService.convertScoresToObject(
await scoresService.getPlayerScores(playerId),
);
// skip if no cached score
if (!playerScores[leaderboardId]) return;
// compare to cached score if cached is equal to current or to cached history score otherwise
let prevScore = playerScores[leaderboardId].score.score === data.score.score
? (playerScores[leaderboardId].history && playerScores[leaderboardId].history.length ? playerScores[leaderboardId].history[0] : null)
let prevScore =
playerScores[leaderboardId].score.score === data.score.score
? playerScores[leaderboardId].history &&
playerScores[leaderboardId].history.length
? playerScores[leaderboardId].history[0]
: null
: playerScores[leaderboardId].score;
// skip if no score to compare
if (!prevScore) return;
const diffInfo = opt(data, 'leaderboard.diffInfo');
const versions = opt(data, 'leaderboard.beatMaps.versions')
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), diffInfo);
const diffInfo = opt(data, "leaderboard.diffInfo");
const versions = opt(data, "leaderboard.beatMaps.versions");
const versionsLastIdx =
versions && Array.isArray(versions) && versions.length
? versions.length - 1
: 0;
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(
opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`),
diffInfo,
);
data.prevScore = calculateAcc(prevScore, bmStats, leaderboardId);
}
};

@ -1,24 +1,28 @@
import createPpService from '../../../../services/scoresaber/pp'
import {configStore} from '../../../config'
import {opt} from '../../../../utils/js'
import createPpService from "../../../../services/scoresaber/pp";
import { configStore } from "../../../config";
import { opt } from "../../../../utils/js";
let ppService;
export default async (data, playerId = null, whatIfOnly = false) => {
if (!playerId) return;
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
const leaderboardId = opt(data, "leaderboard.leaderboardId");
if (!leaderboardId) return;
const pp = opt(data, 'score.pp');
const pp = opt(data, "score.pp");
if (!pp) return;
if (!ppService) ppService = createPpService();
const mainPlayerId = configStore.getMainPlayerId();
if (mainPlayerId && mainPlayerId !== playerId) {
const whatIfPp = await ppService.getWhatIfScore(mainPlayerId, leaderboardId, pp)
if (whatIfPp && whatIfPp.diff >= 0.01) data.score.whatIfPp = whatIfPp
const whatIfPp = await ppService.getWhatIfScore(
mainPlayerId,
leaderboardId,
pp,
);
if (whatIfPp && whatIfPp.diff >= 0.01) data.score.whatIfPp = whatIfPp;
}
if (whatIfOnly) return;
@ -27,4 +31,4 @@ export default async (data, playerId = null, whatIfOnly = false) => {
if (!ppAttribution) return;
data.score.ppAttribution = -ppAttribution.diff;
}
};

@ -1,15 +1,22 @@
import createTwitchService from '../../../../services/twitch'
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
import {opt} from '../../../../utils/js'
import createTwitchService from "../../../../services/twitch";
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
import { opt } from "../../../../utils/js";
let twitchService;
export default async (data, playerId = null) => {
if (!data || !data.score || !data.leaderboard || !data.leaderboard.beatMaps) return;
if (!data || !data.score || !data.leaderboard || !data.leaderboard.beatMaps)
return;
const versions = opt(data, 'leaderboard.beatMaps.versions')
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), data.leaderboard.diffInfo);
const versions = opt(data, "leaderboard.beatMaps.versions");
const versionsLastIdx =
versions && Array.isArray(versions) && versions.length
? versions.length - 1
: 0;
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(
opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`),
data.leaderboard.diffInfo,
);
if (!bmStats || !bmStats.seconds) return;
if (!twitchService) twitchService = createTwitchService();
@ -17,8 +24,12 @@ export default async (data, playerId = null) => {
const twitchProfile = await twitchService.refresh(playerId);
if (!twitchProfile) return;
const video = await twitchService.findTwitchVideo(twitchProfile, data.score.timeSet, bmStats.seconds);
const video = await twitchService.findTwitchVideo(
twitchProfile,
data.score.timeSet,
bmStats.seconds,
);
if (!video || !video.url) return;
data.twitchVideo = video;
}
};

@ -1,44 +1,53 @@
import createHttpStore from './http-store';
import beatMapsEnhancer from './enhancers/common/beatmaps'
import accEnhancer from './enhancers/scores/acc'
import createLeaderboardPageProvider from './providers/page-leaderboard'
import {writable} from 'svelte/store'
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../utils/scoresaber/song'
import {debounce} from '../../utils/debounce'
import produce, {applyPatches} from 'immer'
import ppAttributionEnhancer from './enhancers/scores/pp-attribution'
import createHttpStore from "./http-store";
import beatMapsEnhancer from "./enhancers/common/beatmaps";
import accEnhancer from "./enhancers/scores/acc";
import createLeaderboardPageProvider from "./providers/page-leaderboard";
import { writable } from "svelte/store";
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../utils/scoresaber/song";
import { debounce } from "../../utils/debounce";
import produce, { applyPatches } from "immer";
import ppAttributionEnhancer from "./enhancers/scores/pp-attribution";
export default (leaderboardId, type = 'global', page = 1, initialState = null, initialStateType = 'initial') => {
export default (
leaderboardId,
type = "global",
page = 1,
initialState = null,
initialStateType = "initial",
) => {
let currentLeaderboardId = leaderboardId ? leaderboardId : null;
let currentType = type ? type : 'global';
let currentType = type ? type : "global";
let currentPage = page ? page : 1;
const { subscribe: subscribeEnhanced, set: setEnhanced } = writable(null);
const getCurrentEnhanceTaskId = () => `${currentLeaderboardId}/${currentPage}/${currentType}`;
const getPatchId = (leaderboardId, scoreRow) => `${leaderboardId}/${scoreRow?.player?.playerId}`
const getCurrentEnhanceTaskId = () =>
`${currentLeaderboardId}/${currentPage}/${currentType}`;
const getPatchId = (leaderboardId, scoreRow) =>
`${leaderboardId}/${scoreRow?.player?.playerId}`;
let enhancePatches = {};
let currentEnhanceTaskId = null;
const onNewData = ({ fetchParams, state, set }) => {
currentLeaderboardId = fetchParams?.leaderboardId ?? null;
currentType = fetchParams?.type ?? 'global';
currentType = fetchParams?.type ?? "global";
currentPage = fetchParams?.page ?? 1;
if (!state) return;
const enhanceTaskId = getCurrentEnhanceTaskId();
if (currentEnhanceTaskId !== enhanceTaskId) {
enhancePatches = {}
enhancePatches = {};
currentEnhanceTaskId = enhanceTaskId;
}
const stateProduce = (state, patchId, producer) => produce(state, producer, patches => {
const stateProduce = (state, patchId, producer) =>
produce(state, producer, (patches) => {
if (!enhancePatches[patchId]) enhancePatches[patchId] = [];
enhancePatches[patchId].push(...patches)
})
enhancePatches[patchId].push(...patches);
});
const debouncedSetState = debounce((enhanceTaskId, state) => {
if (currentEnhanceTaskId !== enhanceTaskId) return;
@ -51,49 +60,78 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
const setStateRow = (enhanceTaskId, scoreRow) => {
if (currentEnhanceTaskId !== enhanceTaskId) return null;
const patchId = getPatchId(currentLeaderboardId, scoreRow)
const stateRowIdx = newState.scores.findIndex(s => getPatchId(currentLeaderboardId, s) === patchId)
const patchId = getPatchId(currentLeaderboardId, scoreRow);
const stateRowIdx = newState.scores.findIndex(
(s) => getPatchId(currentLeaderboardId, s) === patchId,
);
if (stateRowIdx < 0) return;
newState.scores[stateRowIdx] = applyPatches(newState.scores[stateRowIdx], enhancePatches[patchId]);
newState.scores[stateRowIdx] = applyPatches(
newState.scores[stateRowIdx],
enhancePatches[patchId],
);
debouncedSetState(enhanceTaskId, newState);
return newState.scores[stateRowIdx];
}
};
if (newState.leaderboard)
beatMapsEnhancer(newState)
.then(_ => {
const versions = newState?.leaderboard?.beatMaps?.versions ?? null
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
.then((_) => {
const versions = newState?.leaderboard?.beatMaps?.versions ?? null;
const versionsLastIdx =
versions && Array.isArray(versions) && versions.length
? versions.length - 1
: 0;
const bpm = newState?.leaderboard?.beatMaps?.metadata?.bpm ?? null;
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(newState?.leaderboard?.beatMaps?.versions?.[versionsLastIdx]?.diffs, newState?.leaderboard?.diffInfo);
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(
newState?.leaderboard?.beatMaps?.versions?.[versionsLastIdx]?.diffs,
newState?.leaderboard?.diffInfo,
);
if (!bmStats) return null;
newState.leaderboard.stats = {...newState.leaderboard.stats, ...bmStats, bpm};
newState.leaderboard.stats = {
...newState.leaderboard.stats,
...bmStats,
bpm,
};
setEnhanced({leaderboardId, type, page, enhancedAt: new Date()})
setEnhanced({ leaderboardId, type, page, enhancedAt: new Date() });
debouncedSetState(enhanceTaskId, newState);
return newState.leaderboard.beatMaps;
})
.then(_ => {
.then((_) => {
if (!newState.scores || !newState.scores.length) return;
for (const scoreRow of newState.scores) {
stateProduce({
stateProduce(
{
...scoreRow,
leaderboard: newState.leaderboard
}, getPatchId(currentLeaderboardId, scoreRow), draft => accEnhancer(draft))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
.then(scoreRow => stateProduce({...scoreRow, leaderboard: newState.leaderboard}, getPatchId(currentLeaderboardId, scoreRow), draft => ppAttributionEnhancer(draft, scoreRow?.player?.playerId, true))
leaderboard: newState.leaderboard,
},
getPatchId(currentLeaderboardId, scoreRow),
(draft) => accEnhancer(draft),
)
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
}
})
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
.then((scoreRow) =>
stateProduce(
{ ...scoreRow, leaderboard: newState.leaderboard },
getPatchId(currentLeaderboardId, scoreRow),
(draft) =>
ppAttributionEnhancer(
draft,
scoreRow?.player?.playerId,
true,
),
),
)
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
}
});
};
const provider = createLeaderboardPageProvider();
@ -106,18 +144,30 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
onAfterStateChange: onNewData,
onSetPending: ({ fetchParams }) => ({ ...fetchParams }),
},
initialStateType
initialStateType,
);
const fetch = async (leaderboardId = currentLeaderboardId, type = currentType, page = currentPage, force = false) => {
const fetch = async (
leaderboardId = currentLeaderboardId,
type = currentType,
page = currentPage,
force = false,
) => {
if (!leaderboardId) return false;
if (leaderboardId === currentLeaderboardId && (!type || type === currentType) && (!page || page === currentPage) && !force) return false;
if (
leaderboardId === currentLeaderboardId &&
(!type || type === currentType) &&
(!page || page === currentPage) &&
!force
)
return false;
return httpStore.fetch({ leaderboardId, type, page }, force, provider);
}
};
const refresh = async () => fetch(currentLeaderboardId, currentType, currentPage, true);
const refresh = async () =>
fetch(currentLeaderboardId, currentType, currentPage, true);
return {
...httpStore,
@ -127,6 +177,5 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
getType: () => currentType,
getPage: () => currentPage,
enhanced: { subscribe: subscribeEnhanced },
}
}
};
};

@ -1,12 +1,16 @@
import createHttpStore from './http-store';
import playerApiClient from '../../network/clients/scoresaber/player/api'
import createHttpStore from "./http-store";
import playerApiClient from "../../network/clients/scoresaber/player/api";
export default (playerId = null, initialState = null, initialStateType = 'initial') => {
export default (
playerId = null,
initialState = null,
initialStateType = "initial",
) => {
let currentPlayerId = playerId;
const onNewData = ({ fetchParams }) => {
currentPlayerId = fetchParams?.playerId ?? null;
}
};
const httpStore = createHttpStore(
playerApiClient,
@ -23,12 +27,11 @@ export default (playerId = null, initialState = null, initialStateType = 'initia
if (!playerId || (playerId === currentPlayerId && !force)) return false;
return httpStore.fetch({ playerId }, force, playerApiClient);
}
};
return {
...httpStore,
fetch,
getPlayerId: () => currentPlayerId,
}
}
};
};

@ -1,12 +1,18 @@
import stringify from 'json-stable-stringify';
import eventBus from '../../utils/broadcast-channel-pubsub'
import createHttpStore from './http-store';
import createApiPlayerWithScoresProvider from './providers/api-player-with-scores'
import createPlayerService from '../../services/scoresaber/player'
import {addToDate, MINUTE} from '../../utils/date'
import {writable} from 'svelte/store'
import stringify from "json-stable-stringify";
import eventBus from "../../utils/broadcast-channel-pubsub";
import createHttpStore from "./http-store";
import createApiPlayerWithScoresProvider from "./providers/api-player-with-scores";
import createPlayerService from "../../services/scoresaber/player";
import { addToDate, MINUTE } from "../../utils/date";
import { writable } from "svelte/store";
export default (playerId = null, service = 'scoresaber', serviceParams = {type: 'recent', page: 1}, initialState = null, initialStateType = 'initial') => {
export default (
playerId = null,
service = "scoresaber",
serviceParams = { type: "recent", page: 1 },
initialState = null,
initialStateType = "initial",
) => {
let currentPlayerId = playerId;
let currentService = service;
let currentServiceParams = serviceParams;
@ -23,8 +29,8 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
currentService = fetchParams?.service ?? null;
currentServiceParams = fetchParams?.serviceParams ?? null;
setParams({currentPlayerId, currentService, currentServiceParams})
}
setParams({ currentPlayerId, currentService, currentServiceParams });
};
const provider = createApiPlayerWithScoresProvider();
@ -36,14 +42,20 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
onInitialized: onNewData,
onAfterStateChange: onNewData,
},
initialStateType
initialStateType,
);
const fetch = async (playerId = currentPlayerId, service = currentService, serviceParams = currentServiceParams, force = false) => {
const fetch = async (
playerId = currentPlayerId,
service = currentService,
serviceParams = currentServiceParams,
force = false,
) => {
if (
(!playerId || playerId === currentPlayerId) &&
(!service || stringify(service) === stringify(currentService)) &&
(!serviceParams || stringify(serviceParams) === stringify(currentServiceParams)) &&
(!serviceParams ||
stringify(serviceParams) === stringify(currentServiceParams)) &&
!force
)
return false;
@ -54,12 +66,20 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
playerForLastRecentPlay = playerId;
}
return httpStore.fetch({playerId, service, serviceParams}, force, provider, !playerId || playerId !== currentPlayerId || force);
}
return httpStore.fetch(
{ playerId, service, serviceParams },
force,
provider,
!playerId || playerId !== currentPlayerId || force,
);
};
const refresh = async () => fetch(currentPlayerId, currentService, currentServiceParams, true);
const refresh = async () =>
fetch(currentPlayerId, currentService, currentServiceParams, true);
const playerRecentPlayUpdatedUnsubscribe = eventBus.on('player-recent-play-updated', async ({playerId, recentPlay}) => {
const playerRecentPlayUpdatedUnsubscribe = eventBus.on(
"player-recent-play-updated",
async ({ playerId, recentPlay }) => {
if (!playerId || !currentPlayerId || playerId !== currentPlayerId) return;
if (!recentPlay || !lastRecentPlay || recentPlay <= lastRecentPlay) {
@ -74,37 +94,44 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
playerForLastRecentPlay = playerId;
await refresh();
});
},
);
const subscribe = fn => {
const subscribe = (fn) => {
const storeUnsubscribe = httpStore.subscribe(fn);
return () => {
storeUnsubscribe();
playerRecentPlayUpdatedUnsubscribe();
}
}
};
};
const DEFAULT_RECENT_PLAY_REFRESH_INTERVAL = MINUTE;
const enqueueRecentPlayRefresh = async () => {
if (!currentPlayerId) {
setTimeout(() => enqueueRecentPlayRefresh(), DEFAULT_RECENT_PLAY_REFRESH_INTERVAL);
setTimeout(
() => enqueueRecentPlayRefresh(),
DEFAULT_RECENT_PLAY_REFRESH_INTERVAL,
);
return;
}
await playerService.fetchPlayerAndUpdateRecentPlay(currentPlayerId);
const refreshInterval = !lastRecentPlay || lastRecentPlay >= addToDate(-30 * MINUTE, new Date())
const refreshInterval =
!lastRecentPlay || lastRecentPlay >= addToDate(-30 * MINUTE, new Date())
? DEFAULT_RECENT_PLAY_REFRESH_INTERVAL
: 15 * MINUTE;
setTimeout(() => enqueueRecentPlayRefresh(), refreshInterval);
};
}
setTimeout(() => enqueueRecentPlayRefresh(), DEFAULT_RECENT_PLAY_REFRESH_INTERVAL);
setTimeout(
() => enqueueRecentPlayRefresh(),
DEFAULT_RECENT_PLAY_REFRESH_INTERVAL,
);
return {
...httpStore,
@ -114,15 +141,14 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
params: { subscribe: subscribeParams },
getPlayerId: () => currentPlayerId,
getService: () => currentService,
setService: type => {
setService: (type) => {
currentService = type;
setParams({currentPlayerId, currentService, currentServiceParams})
setParams({ currentPlayerId, currentService, currentServiceParams });
},
getServiceParams: () => currentServiceParams,
setServiceParams: page => {
currentServiceParams = page
setParams({currentPlayerId, currentService, currentServiceParams})
setServiceParams: (page) => {
currentServiceParams = page;
setParams({ currentPlayerId, currentService, currentServiceParams });
},
}
}
};
};

@ -1,14 +1,19 @@
import createHttpStore from './http-store';
import createApiRankingProvider from './providers/api-ranking'
import createHttpStore from "./http-store";
import createApiRankingProvider from "./providers/api-ranking";
export default (type = 'global', page = 1, initialState = null, initialStateType = 'initial') => {
let currentType = type ? type : 'global';
export default (
type = "global",
page = 1,
initialState = null,
initialStateType = "initial",
) => {
let currentType = type ? type : "global";
let currentPage = page ? page : 1;
const onNewData = ({ fetchParams }) => {
currentType = fetchParams?.type ?? 'global';
currentType = fetchParams?.type ?? "global";
currentPage = fetchParams?.page ?? 1;
}
};
const provider = createApiRankingProvider();
@ -21,14 +26,23 @@ export default (type = 'global', page = 1, initialState = null, initialStateType
onAfterStateChange: onNewData,
onSetPending: ({ fetchParams }) => ({ ...fetchParams }),
},
initialStateType
initialStateType,
);
const fetch = async (type = currentType, page = currentPage, force = false) => {
if ((!type || type === currentType) && (!page || page === currentPage) && !force) return false;
const fetch = async (
type = currentType,
page = currentPage,
force = false,
) => {
if (
(!type || type === currentType) &&
(!page || page === currentPage) &&
!force
)
return false;
return httpStore.fetch({ type, page }, force, provider);
}
};
const refresh = async () => fetch(currentType, currentPage, true);
@ -38,6 +52,5 @@ export default (type = 'global', page = 1, initialState = null, initialStateType
refresh,
getType: () => currentType,
getPage: () => currentPage,
}
}
};
};

@ -1,26 +1,34 @@
import createHttpStore from './http-store';
import beatMapsEnhancer from './enhancers/common/beatmaps'
import accEnhancer from './enhancers/scores/acc'
import beatSaviorEnhancer from './enhancers/scores/beatsavior'
import rankedsEnhancer from './enhancers/leaderboard/rankeds'
import compareEnhancer from './enhancers/scores/compare'
import diffEnhancer from './enhancers/scores/diff'
import twitchEnhancer from './enhancers/scores/twitch'
import ppAttributionEnhancer from './enhancers/scores/pp-attribution'
import {debounce} from '../../utils/debounce'
import createApiScoresProvider from './providers/api-scores'
import produce, {applyPatches} from 'immer'
import stringify from 'json-stable-stringify'
import createHttpStore from "./http-store";
import beatMapsEnhancer from "./enhancers/common/beatmaps";
import accEnhancer from "./enhancers/scores/acc";
import beatSaviorEnhancer from "./enhancers/scores/beatsavior";
import rankedsEnhancer from "./enhancers/leaderboard/rankeds";
import compareEnhancer from "./enhancers/scores/compare";
import diffEnhancer from "./enhancers/scores/diff";
import twitchEnhancer from "./enhancers/scores/twitch";
import ppAttributionEnhancer from "./enhancers/scores/pp-attribution";
import { debounce } from "../../utils/debounce";
import createApiScoresProvider from "./providers/api-scores";
import produce, { applyPatches } from "immer";
import stringify from "json-stable-stringify";
export default (playerId = null, service = 'scoresaber', serviceParams = {type: 'recent', page: 1}, initialState = null, initialStateType = 'initial') => {
export default (
playerId = null,
service = "scoresaber",
serviceParams = { type: "recent", page: 1 },
initialState = null,
initialStateType = "initial",
) => {
let currentPlayerId = playerId;
let currentService = service;
let currentServiceParams = serviceParams;
let totalScores = null;
const getCurrentEnhanceTaskId = () => `${currentPlayerId}/${currentService}/${stringify(currentServiceParams)}`;
const getPatchId = (playerId, scoreRow) => `${playerId}/${scoreRow?.leaderboard?.leaderboardId}`
const getCurrentEnhanceTaskId = () =>
`${currentPlayerId}/${currentService}/${stringify(currentServiceParams)}`;
const getPatchId = (playerId, scoreRow) =>
`${playerId}/${scoreRow?.leaderboard?.leaderboardId}`;
let enhancePatches = {};
let currentEnhanceTaskId = null;
@ -34,7 +42,7 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
totalScores = state !== null ? null : 0;
return state;
}
};
const onNewData = ({ fetchParams, state, stateType, set }) => {
currentPlayerId = fetchParams?.playerId ?? null;
@ -50,15 +58,16 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
const enhanceTaskId = getCurrentEnhanceTaskId();
if (currentEnhanceTaskId !== enhanceTaskId) {
enhancePatches = {}
enhancePatches = {};
currentEnhanceTaskId = enhanceTaskId;
}
const stateProduce = (state, patchId, producer) => produce(state, producer, patches => {
const stateProduce = (state, patchId, producer) =>
produce(state, producer, (patches) => {
if (!enhancePatches[patchId]) enhancePatches[patchId] = [];
enhancePatches[patchId].push(...patches)
})
enhancePatches[patchId].push(...patches);
});
const debouncedSetState = debounce((enhanceTaskId, state) => {
if (currentEnhanceTaskId !== enhanceTaskId) return;
@ -71,48 +80,98 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
const setStateRow = (enhanceTaskId, scoreRow) => {
if (currentEnhanceTaskId !== enhanceTaskId) return null;
const patchId = getPatchId(currentPlayerId, scoreRow)
const stateRowIdx = newState.findIndex(s => getPatchId(currentPlayerId, s) === patchId)
const patchId = getPatchId(currentPlayerId, scoreRow);
const stateRowIdx = newState.findIndex(
(s) => getPatchId(currentPlayerId, s) === patchId,
);
if (stateRowIdx < 0) return;
newState[stateRowIdx] = applyPatches(newState[stateRowIdx], enhancePatches[patchId]);
newState[stateRowIdx] = applyPatches(
newState[stateRowIdx],
enhancePatches[patchId],
);
debouncedSetState(enhanceTaskId, newState);
return newState[stateRowIdx];
}
};
for (const scoreRow of newState) {
if (currentService !== 'accsaber') {
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatMapsEnhancer(draft))
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => accEnhancer(draft)))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => diffEnhancer(draft, currentPlayerId)))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => compareEnhancer(draft, currentPlayerId)))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => twitchEnhancer(draft, currentPlayerId)))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
if (currentService !== "accsaber") {
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) =>
beatMapsEnhancer(draft),
)
.then((scoreRow) =>
stateProduce(
scoreRow,
getPatchId(currentPlayerId, scoreRow),
(draft) => accEnhancer(draft),
),
)
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
.then((scoreRow) =>
stateProduce(
scoreRow,
getPatchId(currentPlayerId, scoreRow),
(draft) => diffEnhancer(draft, currentPlayerId),
),
)
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
.then((scoreRow) =>
stateProduce(
scoreRow,
getPatchId(currentPlayerId, scoreRow),
(draft) => compareEnhancer(draft, currentPlayerId),
),
)
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
.then((scoreRow) =>
stateProduce(
scoreRow,
getPatchId(currentPlayerId, scoreRow),
(draft) => twitchEnhancer(draft, currentPlayerId),
),
)
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => rankedsEnhancer(draft))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) =>
rankedsEnhancer(draft),
).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => ppAttributionEnhancer(draft, currentPlayerId))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) =>
ppAttributionEnhancer(draft, currentPlayerId),
).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
if (stateType && stateType === 'live')
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatSaviorEnhancer(draft, currentPlayerId))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
if (stateType && stateType === "live")
stateProduce(
scoreRow,
getPatchId(currentPlayerId, scoreRow),
(draft) => beatSaviorEnhancer(draft, currentPlayerId),
).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
} else {
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatMapsEnhancer(draft))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => twitchEnhancer(draft, currentPlayerId)))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatSaviorEnhancer(draft, currentPlayerId)))
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
}
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) =>
beatMapsEnhancer(draft),
)
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
.then((scoreRow) =>
stateProduce(
scoreRow,
getPatchId(currentPlayerId, scoreRow),
(draft) => twitchEnhancer(draft, currentPlayerId),
),
)
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
.then((scoreRow) =>
stateProduce(
scoreRow,
getPatchId(currentPlayerId, scoreRow),
(draft) => beatSaviorEnhancer(draft, currentPlayerId),
),
)
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
}
}
};
const provider = createApiScoresProvider();
@ -126,22 +185,34 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
onAfterStateChange: onNewData,
onSetPending: ({ fetchParams }) => ({ ...fetchParams }),
},
initialStateType
initialStateType,
);
const fetch = async (serviceParams = currentServiceParams, service = currentService, playerId = currentPlayerId, force = false) => {
const fetch = async (
serviceParams = currentServiceParams,
service = currentService,
playerId = currentPlayerId,
force = false,
) => {
if (
(!playerId || playerId === currentPlayerId) &&
(!service || stringify(service) === stringify(currentService)) &&
(!serviceParams || stringify(serviceParams) === stringify(currentServiceParams)) &&
(!serviceParams ||
stringify(serviceParams) === stringify(currentServiceParams)) &&
!force
)
return false;
return httpStore.fetch({playerId, service, serviceParams}, force, provider, !playerId || playerId !== currentPlayerId || force);
}
return httpStore.fetch(
{ playerId, service, serviceParams },
force,
provider,
!playerId || playerId !== currentPlayerId || force,
);
};
const refresh = async () => fetch(currentServiceParams, currentService, currentPlayerId, true);
const refresh = async () =>
fetch(currentServiceParams, currentService, currentPlayerId, true);
return {
...httpStore,
@ -151,6 +222,5 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
getService: () => currentService,
getServiceParams: () => currentServiceParams,
getTotalScores: () => totalScores,
}
}
};
};

Some files were not shown because too many files have changed in this diff Show More