add and run prettier
This commit is contained in:
parent
47a23f0484
commit
f6f56aa09c
@ -24,6 +24,7 @@
|
|||||||
"json-stable-stringify": "^1.0.1",
|
"json-stable-stringify": "^1.0.1",
|
||||||
"luxon": "^2.0.2",
|
"luxon": "^2.0.2",
|
||||||
"p-queue": "^7.1.0",
|
"p-queue": "^7.1.0",
|
||||||
|
"prettier": "3.0.3",
|
||||||
"rollup": "^2.3.4",
|
"rollup": "^2.3.4",
|
||||||
"rollup-plugin-css-only": "^3.1.0",
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
"rollup-plugin-livereload": "^2.0.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);
|
color: var(--decrease);
|
||||||
}
|
}
|
||||||
|
|
||||||
*[title]:not([title=""]):not(.clickable) {cursor: help;}
|
*[title]:not([title=""]):not(.clickable) {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
.scoresaber-icon {
|
.scoresaber-icon {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -69,8 +71,7 @@ select {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
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 {
|
.accsaber-icon {
|
||||||
|
87
public/assets/swiped-events.min.js
vendored
87
public/assets/swiped-events.min.js
vendored
@ -6,4 +6,89 @@
|
|||||||
* @author John Doherty <www.johndoherty.info>
|
* @author John Doherty <www.johndoherty.info>
|
||||||
* @license MIT
|
* @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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'>
|
<meta charset="utf-8" />
|
||||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
|
||||||
<title>ScoreSaber Reloaded</title>
|
<title>ScoreSaber Reloaded</title>
|
||||||
|
|
||||||
<link rel='icon' type='image/png' href='/assets/favicon.png' />
|
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||||
<link rel='stylesheet' href='/assets/ss-bulma.css' />
|
<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="https://use.fontawesome.com/releases/v5.15.4/css/all.css"
|
||||||
|
/>
|
||||||
<link rel="stylesheet" href="/assets/ssr.css?20210925" />
|
<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 src="/assets/swiped-events.min.js"></script>
|
||||||
<script defer src='/build/bundle.js'></script>
|
<script defer src="/build/bundle.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body></body>
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
import svelte from 'rollup-plugin-svelte';
|
import svelte from "rollup-plugin-svelte";
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
import livereload from 'rollup-plugin-livereload';
|
import livereload from "rollup-plugin-livereload";
|
||||||
import { terser } from 'rollup-plugin-terser';
|
import { terser } from "rollup-plugin-terser";
|
||||||
import sveltePreprocess from 'svelte-preprocess';
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
import css from 'rollup-plugin-css-only';
|
import css from "rollup-plugin-css-only";
|
||||||
import svg from 'rollup-plugin-svg';
|
import svg from "rollup-plugin-svg";
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH;
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
const buildVersion = execSync("git rev-parse --short HEAD").toString();
|
const buildVersion = execSync("git rev-parse --short HEAD").toString();
|
||||||
fs.writeFileSync('build-info.js', 'export default ' + JSON.stringify({
|
fs.writeFileSync(
|
||||||
buildDate: (new Date()).toISOString().substr(0, 19).replace('T', ' ') + ' UTC',
|
"build-info.js",
|
||||||
buildVersion
|
"export default " +
|
||||||
}))
|
JSON.stringify({
|
||||||
|
buildDate:
|
||||||
|
new Date().toISOString().substr(0, 19).replace("T", " ") + " UTC",
|
||||||
|
buildVersion,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
function serve() {
|
function serve() {
|
||||||
let server;
|
let server;
|
||||||
@ -28,25 +33,29 @@ function serve() {
|
|||||||
return {
|
return {
|
||||||
writeBundle() {
|
writeBundle() {
|
||||||
if (server) return;
|
if (server) return;
|
||||||
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
|
server = require("child_process").spawn(
|
||||||
stdio: ['ignore', 'inherit', 'inherit'],
|
"npm",
|
||||||
shell: true
|
["run", "start", "--", "--dev"],
|
||||||
});
|
{
|
||||||
|
stdio: ["ignore", "inherit", "inherit"],
|
||||||
|
shell: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
process.on('SIGTERM', toExit);
|
process.on("SIGTERM", toExit);
|
||||||
process.on('exit', toExit);
|
process.on("exit", toExit);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
input: 'src/main.js',
|
input: "src/main.js",
|
||||||
output: {
|
output: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
format: 'iife',
|
format: "iife",
|
||||||
name: 'app',
|
name: "app",
|
||||||
file: 'public/build/bundle.js',
|
file: "public/build/bundle.js",
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
svelte({
|
svelte({
|
||||||
@ -58,7 +67,7 @@ export default [
|
|||||||
}),
|
}),
|
||||||
// we'll extract any component CSS out into
|
// we'll extract any component CSS out into
|
||||||
// a separate file - better for performance
|
// a separate file - better for performance
|
||||||
css({output: 'bundle.css'}),
|
css({ output: "bundle.css" }),
|
||||||
|
|
||||||
svg(),
|
svg(),
|
||||||
|
|
||||||
@ -69,7 +78,7 @@ export default [
|
|||||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||||
resolve({
|
resolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
dedupe: ['svelte'],
|
dedupe: ["svelte"],
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
|
||||||
@ -79,23 +88,23 @@ export default [
|
|||||||
|
|
||||||
// Watch the `public` directory and refresh the
|
// Watch the `public` directory and refresh the
|
||||||
// browser on changes when not in production
|
// browser on changes when not in production
|
||||||
!production && livereload('public'),
|
!production && livereload("public"),
|
||||||
|
|
||||||
// If we're building for production (npm run build
|
// If we're building for production (npm run build
|
||||||
// instead of npm run dev), minify
|
// instead of npm run dev), minify
|
||||||
production && terser(),
|
production && terser(),
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'copy-comlink',
|
name: "copy-comlink",
|
||||||
generateBundle() {
|
generateBundle() {
|
||||||
const buildDir = './public/build'
|
const buildDir = "./public/build";
|
||||||
if (!fs.existsSync(buildDir)) {
|
if (!fs.existsSync(buildDir)) {
|
||||||
fs.mkdirSync(buildDir);
|
fs.mkdirSync(buildDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
path.resolve('./node_modules/comlink/dist/umd/comlink.min.js'),
|
path.resolve("./node_modules/comlink/dist/umd/comlink.min.js"),
|
||||||
path.resolve('./public/build/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: {
|
output: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
format: 'iife',
|
format: "iife",
|
||||||
name: 'app',
|
name: "app",
|
||||||
file: 'public/build/stats-worker.js',
|
file: "public/build/stats-worker.js",
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// If you have external dependencies installed from
|
// If you have external dependencies installed from
|
||||||
@ -121,7 +130,7 @@ export default [
|
|||||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||||
resolve({
|
resolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
dedupe: ['svelte'],
|
dedupe: ["svelte"],
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
|
||||||
@ -129,19 +138,19 @@ export default [
|
|||||||
// instead of npm run dev), minify
|
// instead of npm run dev), minify
|
||||||
production && terser(),
|
production && terser(),
|
||||||
{
|
{
|
||||||
name: 'copy-test-worker',
|
name: "copy-test-worker",
|
||||||
load() {
|
load() {
|
||||||
this.addWatchFile(path.resolve('./src/workers/stats-worker.js'));
|
this.addWatchFile(path.resolve("./src/workers/stats-worker.js"));
|
||||||
},
|
},
|
||||||
generateBundle() {
|
generateBundle() {
|
||||||
const buildDir = './public/build'
|
const buildDir = "./public/build";
|
||||||
if (!fs.existsSync(buildDir)) {
|
if (!fs.existsSync(buildDir)) {
|
||||||
fs.mkdirSync(buildDir);
|
fs.mkdirSync(buildDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
path.resolve('./src/workers/stats-worker.js'),
|
path.resolve("./src/workers/stats-worker.js"),
|
||||||
path.resolve('./public/build/stats-worker.js'),
|
path.resolve("./public/build/stats-worker.js"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -17,15 +17,23 @@ export const onLegendClick = (event, legendItem, legend) => {
|
|||||||
|
|
||||||
if (legend?.chart) {
|
if (legend?.chart) {
|
||||||
const yAxisIdsToShow = (legend?.legendItems ?? [])
|
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))
|
.sort(
|
||||||
.reduce((cum, legendItem) => {
|
(a, b) =>
|
||||||
|
(ci?.config?.data?.datasets?.[a?.datasetIndex]?.axisOrder ??
|
||||||
|
a?.datasetIndex) -
|
||||||
|
(ci?.config?.data?.datasets?.[b?.datasetIndex]?.axisOrder ??
|
||||||
|
b?.datasetIndex),
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
(cum, legendItem) => {
|
||||||
// done
|
// done
|
||||||
if (cum.second) return cum;
|
if (cum.second) return cum;
|
||||||
|
|
||||||
// skip hidden legend items
|
// skip hidden legend items
|
||||||
if (legendItem?.hidden) return cum;
|
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 (!yAxisId) return cum;
|
||||||
|
|
||||||
if (!cum.first) {
|
if (!cum.first) {
|
||||||
@ -35,20 +43,26 @@ export const onLegendClick = (event, legendItem, legend) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return cum;
|
return cum;
|
||||||
}, {first: null, second: null});
|
},
|
||||||
|
{ first: null, second: null },
|
||||||
|
);
|
||||||
|
|
||||||
Object.keys(yAxes).forEach(currentAxisKey => {
|
Object.keys(yAxes).forEach((currentAxisKey) => {
|
||||||
if (![yAxisIdsToShow.first, yAxisIdsToShow.second].includes(currentAxisKey)) {
|
if (
|
||||||
|
![yAxisIdsToShow.first, yAxisIdsToShow.second].includes(currentAxisKey)
|
||||||
|
) {
|
||||||
yAxes[currentAxisKey].display = false;
|
yAxes[currentAxisKey].display = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
yAxes[currentAxisKey].display = true;
|
yAxes[currentAxisKey].display = true;
|
||||||
if (yAxisIdsToShow.first === currentAxisKey) yAxes[currentAxisKey].position = 'left';
|
if (yAxisIdsToShow.first === currentAxisKey)
|
||||||
if (yAxisIdsToShow.second === currentAxisKey) yAxes[currentAxisKey].position = 'right';
|
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();
|
legend.chart.update();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'regions',
|
id: "regions",
|
||||||
beforeDraw(chart, args, options) {
|
beforeDraw(chart, args, options) {
|
||||||
if (!options?.regions || !Array.isArray(options.regions)) return;
|
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;
|
const width = right - left;
|
||||||
|
|
||||||
let fontSize = parseInt(ctx.font, 10);
|
let fontSize = parseInt(ctx.font, 10);
|
||||||
@ -11,12 +15,15 @@ export default {
|
|||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
options.regions.forEach(region => {
|
options.regions.forEach((region) => {
|
||||||
if (y.min <= region.max && y.max >= region.min) {
|
if (y.min <= region.max && y.max >= region.min) {
|
||||||
const minY = Math.max(region.min, y.min);
|
const minY = Math.max(region.min, y.min);
|
||||||
const maxY = Math.min(region.max, y.max);
|
const maxY = Math.min(region.max, y.max);
|
||||||
const top = y.getPixelForValue(maxY);
|
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.fillStyle = region.color;
|
||||||
ctx.fillRect(left, top, width, height);
|
ctx.fillRect(left, top, width, height);
|
||||||
@ -24,16 +31,20 @@ export default {
|
|||||||
if (region.label) {
|
if (region.label) {
|
||||||
const labelWidth = ctx.measureText(region.label)?.width ?? 0;
|
const labelWidth = ctx.measureText(region.label)?.width ?? 0;
|
||||||
|
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = "top";
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
region.label,
|
region.label,
|
||||||
region?.position?.horizontal === 'right' ? right - labelWidth - 3 : left + 3,
|
region?.position?.horizontal === "right"
|
||||||
region?.position?.vertical === 'bottom' ? top + 2 : top - fontSize - 1
|
? right - labelWidth - 3
|
||||||
|
: left + 3,
|
||||||
|
region?.position?.vertical === "bottom"
|
||||||
|
? top + 2
|
||||||
|
: top - fontSize - 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
@ -70,7 +70,7 @@ function updateScoresStats(playerData, playerStats) {
|
|||||||
bgColor: "var(--ppColour)",
|
bgColor: "var(--ppColour)",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []
|
: [],
|
||||||
)
|
)
|
||||||
.filter((s) => s && (!playerStats || s.label !== "Average"));
|
.filter((s) => s && (!playerStats || s.label !== "Average"));
|
||||||
}
|
}
|
||||||
|
@ -2,27 +2,34 @@ export default () => {
|
|||||||
let currentService = null;
|
let currentService = null;
|
||||||
let currentServiceParams = {};
|
let currentServiceParams = {};
|
||||||
|
|
||||||
const getAllServices = () => ['scoresaber', 'beatsavior', 'accsaber'];
|
const getAllServices = () => ["scoresaber", "beatsavior", "accsaber"];
|
||||||
|
|
||||||
const get = () => ({ service: currentService, params: currentServiceParams });
|
const get = () => ({ service: currentService, params: currentServiceParams });
|
||||||
|
|
||||||
const getDefaultParams = service => {
|
const getDefaultParams = (service) => {
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case 'beatsavior':
|
case "beatsavior":
|
||||||
return {sort: 'recent', order: 'desc', page: 1, filters: {}};
|
return { sort: "recent", order: "desc", page: 1, filters: {} };
|
||||||
|
|
||||||
case 'accsaber':
|
case "accsaber":
|
||||||
return {type: 'overall', order: 'desc', sort: 'ap', page: 1, filters: {}}
|
return {
|
||||||
|
type: "overall",
|
||||||
|
order: "desc",
|
||||||
|
sort: "ap",
|
||||||
|
page: 1,
|
||||||
|
filters: {},
|
||||||
|
};
|
||||||
|
|
||||||
case 'scoresaber':
|
case "scoresaber":
|
||||||
default:
|
default:
|
||||||
return {sort: 'recent', order: 'desc', page: 1, filters: {}}
|
return { sort: "recent", order: "desc", page: 1, filters: {} };
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const update = (serviceParams = {}, service = currentService) => {
|
const update = (serviceParams = {}, service = currentService) => {
|
||||||
const availableServices = getAllServices();
|
const availableServices = getAllServices();
|
||||||
if (!availableServices.includes(service)) service = availableServices?.[0] ?? 'scoresaber';
|
if (!availableServices.includes(service))
|
||||||
|
service = availableServices?.[0] ?? "scoresaber";
|
||||||
|
|
||||||
const defaultServiceParams = getDefaultParams(service);
|
const defaultServiceParams = getDefaultParams(service);
|
||||||
|
|
||||||
@ -32,84 +39,103 @@ export default () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// preserve old filters
|
// preserve old filters
|
||||||
serviceParams = {...serviceParams}
|
serviceParams = { ...serviceParams };
|
||||||
serviceParams.filters = {
|
serviceParams.filters = {
|
||||||
...(currentServiceParams?.filters ?? {}),
|
...(currentServiceParams?.filters ?? {}),
|
||||||
...(serviceParams?.filters ?? {}),
|
...(serviceParams?.filters ?? {}),
|
||||||
}
|
};
|
||||||
|
|
||||||
currentService = service;
|
currentService = service;
|
||||||
currentServiceParams = {...defaultServiceParams, ...currentServiceParams, ...serviceParams}
|
currentServiceParams = {
|
||||||
|
...defaultServiceParams,
|
||||||
|
...currentServiceParams,
|
||||||
|
...serviceParams,
|
||||||
|
};
|
||||||
|
|
||||||
return get();
|
return get();
|
||||||
}
|
};
|
||||||
|
|
||||||
const clearServiceParams = () => currentServiceParams = {}
|
const clearServiceParams = () => (currentServiceParams = {});
|
||||||
|
|
||||||
const initFromUrl = (url = null) => {
|
const initFromUrl = (url = null) => {
|
||||||
const availableServices = getAllServices();
|
const availableServices = getAllServices();
|
||||||
const defaultService = availableServices?.[0] ?? 'scoresaber';
|
const defaultService = availableServices?.[0] ?? "scoresaber";
|
||||||
const paramsArr = url ? url.split('/') : [defaultService];
|
const paramsArr = url ? url.split("/") : [defaultService];
|
||||||
|
|
||||||
const service = paramsArr[0] ?? 'scoresaber';
|
const service = paramsArr[0] ?? "scoresaber";
|
||||||
|
|
||||||
const serviceDefaultParams = getDefaultParams(service);
|
const serviceDefaultParams = getDefaultParams(service);
|
||||||
|
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case 'beatsavior':
|
case "beatsavior":
|
||||||
return update(
|
return update(
|
||||||
{
|
{
|
||||||
sort: paramsArr[1] ?? serviceDefaultParams?.sort,
|
sort: paramsArr[1] ?? serviceDefaultParams?.sort,
|
||||||
order: 'desc',
|
order: "desc",
|
||||||
page: paramsArr[2] ?? serviceDefaultParams?.page,
|
page: paramsArr[2] ?? serviceDefaultParams?.page,
|
||||||
},
|
},
|
||||||
service,
|
service,
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'accsaber':
|
case "accsaber":
|
||||||
return update(
|
return update(
|
||||||
{
|
{
|
||||||
type: paramsArr[1] ?? serviceDefaultParams?.type,
|
type: paramsArr[1] ?? serviceDefaultParams?.type,
|
||||||
sort: paramsArr[2] ?? serviceDefaultParams?.sort,
|
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,
|
page: paramsArr[3] ?? serviceDefaultParams?.page,
|
||||||
},
|
},
|
||||||
service,
|
service,
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'scoresaber':
|
case "scoresaber":
|
||||||
default:
|
default:
|
||||||
return update(
|
return update(
|
||||||
{
|
{
|
||||||
sort: paramsArr[1] ?? serviceDefaultParams?.sort,
|
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,
|
page: paramsArr[2] ?? serviceDefaultParams?.page,
|
||||||
},
|
},
|
||||||
service,
|
service,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const getUrl = (service, params = {}, noPage = false) => {
|
const getUrl = (service, params = {}, noPage = false) => {
|
||||||
if (!service) return '';
|
if (!service) return "";
|
||||||
|
|
||||||
const serviceDefaultParams = getDefaultParams(service);
|
const serviceDefaultParams = getDefaultParams(service);
|
||||||
|
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case 'beatsavior':
|
case "beatsavior":
|
||||||
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
|
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${
|
||||||
|
noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`
|
||||||
|
}`;
|
||||||
|
|
||||||
case 'accsaber':
|
case "accsaber":
|
||||||
return `${service}/${params?.type ?? serviceDefaultParams?.type}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
|
return `${service}/${params?.type ?? serviceDefaultParams?.type}/${
|
||||||
|
params?.sort ?? serviceDefaultParams?.sort
|
||||||
|
}${noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`}`;
|
||||||
|
|
||||||
case 'scoresaber':
|
case "scoresaber":
|
||||||
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
|
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${
|
||||||
}
|
noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getCurrentServiceUrl = () => getUrl(currentService, currentServiceParams);
|
const getCurrentServiceUrl = () =>
|
||||||
const getCurrentServiceUrlWithoutPage = () => getUrl(currentService, currentServiceParams, true);
|
getUrl(currentService, currentServiceParams);
|
||||||
const getDefaultServiceUrl = (service = currentService) => getUrl(service, {});
|
const getCurrentServiceUrlWithoutPage = () =>
|
||||||
|
getUrl(currentService, currentServiceParams, true);
|
||||||
|
const getDefaultServiceUrl = (service = currentService) =>
|
||||||
|
getUrl(service, {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getAvailableServices: getAllServices,
|
getAvailableServices: getAllServices,
|
||||||
@ -122,5 +148,5 @@ export default () => {
|
|||||||
getParams: () => currentServiceParams,
|
getParams: () => currentServiceParams,
|
||||||
update,
|
update,
|
||||||
clearServiceParams,
|
clearServiceParams,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,19 +1,29 @@
|
|||||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||||
|
|
||||||
export default (name, getObjKey) => {
|
export default (name, getObjKey) => {
|
||||||
let cache = {};
|
let cache = {};
|
||||||
|
|
||||||
// update data cached on another node
|
// update data cached on another node
|
||||||
eventBus.on('cache-key-set-' + name, ({key, value}, isLocal) => !isLocal ? set(key, value, false) : null);
|
eventBus.on("cache-key-set-" + name, ({ key, value }, isLocal) =>
|
||||||
eventBus.on('cache-all-set' + name, ({data}, isLocal) => !isLocal ? setAll(data, false) : null);
|
!isLocal ? set(key, value, 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-all-set" + name, ({ data }, isLocal) =>
|
||||||
eventBus.on('cache-flush-' + name, (_, isLocal) => !isLocal ? flush(false) : null);
|
!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) => {
|
const set = (key, value, emitEvent = true) => {
|
||||||
cache[key] = value;
|
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;
|
return value;
|
||||||
};
|
};
|
||||||
@ -21,17 +31,17 @@ export default (name, getObjKey) => {
|
|||||||
const setAll = (data, emitEvent = true) => {
|
const setAll = (data, emitEvent = true) => {
|
||||||
cache = data;
|
cache = data;
|
||||||
|
|
||||||
if (emitEvent) eventBus.publish('cache-all-set-' + name, {data});
|
if (emitEvent) eventBus.publish("cache-all-set-" + name, { data });
|
||||||
|
|
||||||
return cache;
|
return cache;
|
||||||
}
|
};
|
||||||
const merge = (data, emitEvent = true) => {
|
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;
|
return cache;
|
||||||
}
|
};
|
||||||
|
|
||||||
const get = async (key, fetchFunc) => {
|
const get = async (key, fetchFunc) => {
|
||||||
if (cache.hasOwnProperty(key)) return cache[key];
|
if (cache.hasOwnProperty(key)) return cache[key];
|
||||||
@ -53,42 +63,43 @@ export default (name, getObjKey) => {
|
|||||||
const key = getObjKey(value);
|
const key = getObjKey(value);
|
||||||
|
|
||||||
return set(key, value);
|
return set(key, value);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getAll = () => cache;
|
const getAll = () => cache;
|
||||||
|
|
||||||
const has = key => cache[key] !== undefined;
|
const has = (key) => cache[key] !== undefined;
|
||||||
|
|
||||||
const getKeys = () => Object.keys(cache);
|
const getKeys = () => Object.keys(cache);
|
||||||
|
|
||||||
const forget = (key, emitEvent = true) => {
|
const forget = (key, emitEvent = true) => {
|
||||||
delete cache[key];
|
delete cache[key];
|
||||||
|
|
||||||
if (emitEvent) eventBus.publish('cache-key-forget-' + name, {key});
|
if (emitEvent) eventBus.publish("cache-key-forget-" + name, { key });
|
||||||
|
|
||||||
return cache;
|
return cache;
|
||||||
}
|
};
|
||||||
|
|
||||||
const forgetByFilter = (filterFunc, emitEvent = true) => {
|
const forgetByFilter = (filterFunc, emitEvent = true) => {
|
||||||
if (!filterFunc) return false;
|
if (!filterFunc) return false;
|
||||||
|
|
||||||
Object.keys(cache).filter(key => filterFunc(cache[key]))
|
Object.keys(cache)
|
||||||
.forEach(key => {
|
.filter((key) => filterFunc(cache[key]))
|
||||||
delete 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;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const flush = (emitEvent = true) => {
|
const flush = (emitEvent = true) => {
|
||||||
cache = {};
|
cache = {};
|
||||||
|
|
||||||
if (emitEvent) eventBus.publish('cache-flush-' + name, {});
|
if (emitEvent) eventBus.publish("cache-flush-" + name, {});
|
||||||
|
|
||||||
return cache;
|
return cache;
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
has,
|
has,
|
||||||
@ -102,5 +113,5 @@ export default (name, getObjKey) => {
|
|||||||
forget,
|
forget,
|
||||||
forgetByFilter,
|
forgetByFilter,
|
||||||
flush,
|
flush,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
282
src/db/db.js
282
src/db/db.js
@ -1,195 +1,279 @@
|
|||||||
import {openDB} from 'idb'
|
import { openDB } from "idb";
|
||||||
import log from '../utils/logger'
|
import log from "../utils/logger";
|
||||||
import {isDateObject} from '../utils/js'
|
import { isDateObject } from "../utils/js";
|
||||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||||
|
|
||||||
const SSR_DB_VERSION = 12;
|
const SSR_DB_VERSION = 12;
|
||||||
export let db = null;
|
export let db = null;
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
IDBKeyRange.prototype.toString = function () {
|
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();
|
return await openDatabase();
|
||||||
}
|
};
|
||||||
|
|
||||||
async function openDatabase() {
|
async function openDatabase() {
|
||||||
try {
|
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) {
|
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;
|
dbNewVersion = newVersion;
|
||||||
dbOldVersion = oldVersion;
|
dbOldVersion = oldVersion;
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case newVersion >= 1 && oldVersion <= 0:
|
case newVersion >= 1 && oldVersion <= 0:
|
||||||
db.createObjectStore('players', {
|
db.createObjectStore("players", {
|
||||||
keyPath: 'id',
|
keyPath: "id",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const playersHistory = db.createObjectStore('players-history', {
|
const playersHistory = db.createObjectStore("players-history", {
|
||||||
keyPath: '_idbId',
|
keyPath: "_idbId",
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
});
|
});
|
||||||
playersHistory.createIndex('players-history-playerId', 'playerId', {unique: false});
|
playersHistory.createIndex("players-history-playerId", "playerId", {
|
||||||
playersHistory.createIndex('players-history-timestamp', 'timestamp', {unique: false});
|
unique: false,
|
||||||
|
});
|
||||||
|
playersHistory.createIndex(
|
||||||
|
"players-history-timestamp",
|
||||||
|
"timestamp",
|
||||||
|
{ unique: false },
|
||||||
|
);
|
||||||
|
|
||||||
const scoresStore = db.createObjectStore('scores', {
|
const scoresStore = db.createObjectStore("scores", {
|
||||||
keyPath: 'id',
|
keyPath: "id",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
scoresStore.createIndex('scores-leaderboardId', 'leaderboardId', {unique: false});
|
scoresStore.createIndex("scores-leaderboardId", "leaderboardId", {
|
||||||
scoresStore.createIndex('scores-playerId', 'playerId', {unique: false});
|
unique: false,
|
||||||
scoresStore.createIndex('scores-timeset', 'timeset', {unique: false});
|
});
|
||||||
scoresStore.createIndex('scores-pp', 'pp', {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', {
|
db.createObjectStore("rankeds", {
|
||||||
keyPath: 'leaderboardId',
|
keyPath: "leaderboardId",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const songsStore = db.createObjectStore('songs', {
|
const songsStore = db.createObjectStore("songs", {
|
||||||
keyPath: 'hash',
|
keyPath: "hash",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
songsStore.createIndex('songs-key', 'key', {unique: true});
|
songsStore.createIndex("songs-key", "key", { unique: true });
|
||||||
|
|
||||||
db.createObjectStore('twitch', {
|
db.createObjectStore("twitch", {
|
||||||
keyPath: 'playerId',
|
keyPath: "playerId",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rankedsChangesStore = db.createObjectStore('rankeds-changes', {
|
const rankedsChangesStore = db.createObjectStore(
|
||||||
keyPath: '_idbId',
|
"rankeds-changes",
|
||||||
|
{
|
||||||
|
keyPath: "_idbId",
|
||||||
autoIncrement: true,
|
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
|
// 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});
|
const groups = db.createObjectStore("groups", {
|
||||||
groups.createIndex('groups-name', 'name', {unique: false});
|
keyPath: "_idbId",
|
||||||
groups.createIndex('groups-playerId', 'playerId', {unique: false});
|
autoIncrement: true,
|
||||||
|
});
|
||||||
|
groups.createIndex("groups-name", "name", { unique: false });
|
||||||
|
groups.createIndex("groups-playerId", "playerId", {
|
||||||
|
unique: false,
|
||||||
|
});
|
||||||
|
|
||||||
const beatSaviorFiles = db.createObjectStore('beat-savior-files', {
|
const beatSaviorFiles = db.createObjectStore("beat-savior-files", {
|
||||||
keyPath: 'fileId',
|
keyPath: "fileId",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const beatSavior = db.createObjectStore('beat-savior', {
|
const beatSavior = db.createObjectStore("beat-savior", {
|
||||||
keyPath: 'beatSaviorId',
|
keyPath: "beatSaviorId",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
beatSavior.createIndex('beat-savior-playerId', 'playerId', {unique: false});
|
beatSavior.createIndex("beat-savior-playerId", "playerId", {
|
||||||
beatSavior.createIndex('beat-savior-songId', 'songId', {unique: false});
|
unique: false,
|
||||||
beatSavior.createIndex('beat-savior-fileId', 'fileId', {unique: false});
|
});
|
||||||
|
beatSavior.createIndex("beat-savior-songId", "songId", {
|
||||||
|
unique: false,
|
||||||
|
});
|
||||||
|
beatSavior.createIndex("beat-savior-fileId", "fileId", {
|
||||||
|
unique: false,
|
||||||
|
});
|
||||||
|
|
||||||
// NO break here!
|
// NO break here!
|
||||||
|
|
||||||
case newVersion >= 2 && oldVersion <= 1:
|
case newVersion >= 2 && oldVersion <= 1:
|
||||||
db.createObjectStore('beat-savior-players', {
|
db.createObjectStore("beat-savior-players", {
|
||||||
keyPath: 'playerId',
|
keyPath: "playerId",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// NO break here!
|
// NO break here!
|
||||||
|
|
||||||
case newVersion >= 3 && oldVersion <= 2:
|
case newVersion >= 3 && oldVersion <= 2:
|
||||||
db.deleteObjectStore('players');
|
db.deleteObjectStore("players");
|
||||||
|
|
||||||
db.createObjectStore('players', {
|
db.createObjectStore("players", {
|
||||||
keyPath: 'playerId',
|
keyPath: "playerId",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const scoresStore4 = transaction.objectStore('scores');
|
const scoresStore4 = transaction.objectStore("scores");
|
||||||
scoresStore4.deleteIndex('scores-timeset');
|
scoresStore4.deleteIndex("scores-timeset");
|
||||||
scoresStore4.createIndex('scores-timeSet', 'timeSet', {unique: false});
|
scoresStore4.createIndex("scores-timeSet", "timeSet", {
|
||||||
|
unique: false,
|
||||||
|
});
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 4 && oldVersion <= 3:
|
case newVersion >= 4 && oldVersion <= 3:
|
||||||
db.deleteObjectStore('beat-savior-files');
|
db.deleteObjectStore("beat-savior-files");
|
||||||
|
|
||||||
const beatSaviorStore = transaction.objectStore('beat-savior');
|
const beatSaviorStore = transaction.objectStore("beat-savior");
|
||||||
beatSaviorStore.deleteIndex('beat-savior-fileId');
|
beatSaviorStore.deleteIndex("beat-savior-fileId");
|
||||||
beatSaviorStore.deleteIndex('beat-savior-songId');
|
beatSaviorStore.deleteIndex("beat-savior-songId");
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 5 && oldVersion <= 4:
|
case newVersion >= 5 && oldVersion <= 4:
|
||||||
const songsBeatMapsStore = db.createObjectStore('songs-beatmaps', {
|
const songsBeatMapsStore = db.createObjectStore("songs-beatmaps", {
|
||||||
keyPath: 'hash',
|
keyPath: "hash",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
songsBeatMapsStore.createIndex('songs-beatmaps--key', 'key', {unique: true});
|
songsBeatMapsStore.createIndex("songs-beatmaps--key", "key", {
|
||||||
|
unique: true,
|
||||||
|
});
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 6 && oldVersion <= 5:
|
case newVersion >= 6 && oldVersion <= 5:
|
||||||
const songsBeatMapsStorev6 = transaction.objectStore('songs-beatmaps');
|
const songsBeatMapsStorev6 =
|
||||||
songsBeatMapsStorev6.deleteIndex('songs-beatmaps--key');
|
transaction.objectStore("songs-beatmaps");
|
||||||
songsBeatMapsStorev6.createIndex('songs-beatmaps-key', 'key', {unique: true});
|
songsBeatMapsStorev6.deleteIndex("songs-beatmaps--key");
|
||||||
|
songsBeatMapsStorev6.createIndex("songs-beatmaps-key", "key", {
|
||||||
|
unique: true,
|
||||||
|
});
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 7 && oldVersion <= 6:
|
case newVersion >= 7 && oldVersion <= 6:
|
||||||
const scoresUpdateQueue = db.createObjectStore('scores-update-queue', {
|
const scoresUpdateQueue = db.createObjectStore(
|
||||||
keyPath: 'id',
|
"scores-update-queue",
|
||||||
|
{
|
||||||
|
keyPath: "id",
|
||||||
autoIncrement: false,
|
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:
|
case newVersion >= 8 && oldVersion <= 7:
|
||||||
const beatSaviorStorev8 = transaction.objectStore('beat-savior');
|
const beatSaviorStorev8 = transaction.objectStore("beat-savior");
|
||||||
beatSaviorStorev8.createIndex('beat-savior-hash', 'hash', {unique: false});
|
beatSaviorStorev8.createIndex("beat-savior-hash", "hash", {
|
||||||
|
unique: false,
|
||||||
|
});
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 9 && oldVersion <= 8:
|
case newVersion >= 9 && oldVersion <= 8:
|
||||||
const playersHistoryStorev9 = transaction.objectStore('players-history');
|
const playersHistoryStorev9 =
|
||||||
playersHistoryStorev9.deleteIndex('players-history-timestamp');
|
transaction.objectStore("players-history");
|
||||||
playersHistoryStorev9.createIndex('players-history-playerIdSsTimestamp', 'playerIdSsTimestamp', {unique: true});
|
playersHistoryStorev9.deleteIndex("players-history-timestamp");
|
||||||
|
playersHistoryStorev9.createIndex(
|
||||||
|
"players-history-playerIdSsTimestamp",
|
||||||
|
"playerIdSsTimestamp",
|
||||||
|
{ unique: true },
|
||||||
|
);
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 10 && oldVersion <= 9:
|
case newVersion >= 10 && oldVersion <= 9:
|
||||||
const songsBeatMapsStoreV10 = transaction.objectStore('songs-beatmaps');
|
const songsBeatMapsStoreV10 =
|
||||||
songsBeatMapsStoreV10.deleteIndex('songs-beatmaps-key');
|
transaction.objectStore("songs-beatmaps");
|
||||||
songsBeatMapsStoreV10.createIndex('songs-beatmaps-key', 'key', {unique: false});
|
songsBeatMapsStoreV10.deleteIndex("songs-beatmaps-key");
|
||||||
|
songsBeatMapsStoreV10.createIndex("songs-beatmaps-key", "key", {
|
||||||
|
unique: false,
|
||||||
|
});
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 11 && oldVersion <= 10:
|
case newVersion >= 11 && oldVersion <= 10:
|
||||||
db.createObjectStore('accsaber-categories', {
|
db.createObjectStore("accsaber-categories", {
|
||||||
keyPath: 'name',
|
keyPath: "name",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const accSaberPlayersStore = db.createObjectStore('accsaber-players', {
|
const accSaberPlayersStore = db.createObjectStore(
|
||||||
keyPath: 'id',
|
"accsaber-players",
|
||||||
|
{
|
||||||
|
keyPath: "id",
|
||||||
autoIncrement: false,
|
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
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 12 && oldVersion <= 11:
|
case newVersion >= 12 && oldVersion <= 11:
|
||||||
const accSaberPlayersHistoryStore = db.createObjectStore('accsaber-players-history', {
|
const accSaberPlayersHistoryStore = db.createObjectStore(
|
||||||
keyPath: 'playerIdTimestamp',
|
"accsaber-players-history",
|
||||||
|
{
|
||||||
|
keyPath: "playerIdTimestamp",
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
});
|
},
|
||||||
accSaberPlayersHistoryStore.createIndex('accsaber-players-history-playerId', 'playerId', {unique: false});
|
);
|
||||||
|
accSaberPlayersHistoryStore.createIndex(
|
||||||
|
"accsaber-players-history-playerId",
|
||||||
|
"playerId",
|
||||||
|
{ unique: false },
|
||||||
|
);
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
}
|
}
|
||||||
@ -198,28 +282,33 @@ async function openDatabase() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
blocked() {
|
blocked() {
|
||||||
console.warn('DB blocked')
|
console.warn("DB blocked");
|
||||||
},
|
},
|
||||||
blocking() {
|
blocking() {
|
||||||
// other tab tries to open newer db version - close connection
|
// 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();
|
db.close();
|
||||||
|
|
||||||
eventBus.publish('dl-manager-pause-cmd');
|
eventBus.publish("dl-manager-pause-cmd");
|
||||||
|
|
||||||
// TODO: should be reopened with new version: event.newVersion
|
// TODO: should be reopened with new version: event.newVersion
|
||||||
// TODO: or rather notify user / auto reload page
|
// TODO: or rather notify user / auto reload page
|
||||||
},
|
},
|
||||||
terminated() {
|
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
|
// Closure code should awaits DB operations ONLY or fail
|
||||||
// https://github.com/jakearchibald/idb#user-content-transaction-lifetime
|
// 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 {
|
try {
|
||||||
const tx = db.transaction(objectStores, mode, options);
|
const tx = db.transaction(objectStores, mode, options);
|
||||||
|
|
||||||
@ -231,12 +320,11 @@ async function openDatabase() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
log.error("Can not open DB.");
|
||||||
log.error('Can not open DB.');
|
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,61 @@
|
|||||||
import keyValueRepository from './repository/key-value';
|
import keyValueRepository from "./repository/key-value";
|
||||||
import createBeatMapsService from '../services/beatmaps'
|
import createBeatMapsService from "../services/beatmaps";
|
||||||
import log from '../utils/logger';
|
import log from "../utils/logger";
|
||||||
import {db} from './db'
|
import { db } from "./db";
|
||||||
import {isDateObject} from '../utils/js'
|
import { isDateObject } from "../utils/js";
|
||||||
import twitchRepository from './repository/twitch'
|
import twitchRepository from "./repository/twitch";
|
||||||
import {correctOldSsDate} from '../utils/date'
|
import { correctOldSsDate } from "../utils/date";
|
||||||
|
|
||||||
const FIXES_KEY = 'data-fix';
|
const FIXES_KEY = "data-fix";
|
||||||
|
|
||||||
const getAppliedFixes = async () => keyValueRepository().get(FIXES_KEY, true);
|
const getAppliedFixes = async () => keyValueRepository().get(FIXES_KEY, true);
|
||||||
const setAppliedFixes = async fixes => keyValueRepository().set(fixes, FIXES_KEY);
|
const setAppliedFixes = async (fixes) =>
|
||||||
const addAppliedFix = async fixName => {
|
keyValueRepository().set(fixes, FIXES_KEY);
|
||||||
|
const addAppliedFix = async (fixName) => {
|
||||||
let allAppliedFixes = await getAppliedFixes();
|
let allAppliedFixes = await getAppliedFixes();
|
||||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
allAppliedFixes =
|
||||||
|
allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
||||||
allAppliedFixes.push(fixName);
|
allAppliedFixes.push(fixName);
|
||||||
await setAppliedFixes(allAppliedFixes);
|
await setAppliedFixes(allAppliedFixes);
|
||||||
}
|
};
|
||||||
|
|
||||||
const allFixes = {
|
const allFixes = {
|
||||||
'rankeds-20210725': {
|
"rankeds-20210725": {
|
||||||
apply: async fixName => {
|
apply: async (fixName) => {
|
||||||
log.info('Apply rankeds refresh fix (20210725)')
|
log.info("Apply rankeds refresh fix (20210725)");
|
||||||
|
|
||||||
return db.runInTransaction(['rankeds-changes', 'rankeds', 'key-value'], async tx => {
|
return db.runInTransaction(
|
||||||
await tx.objectStore('rankeds-changes').clear();
|
["rankeds-changes", "rankeds", "key-value"],
|
||||||
await tx.objectStore('rankeds').clear();
|
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);
|
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
||||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
allAppliedFixes =
|
||||||
|
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||||
|
? allAppliedFixes
|
||||||
|
: [];
|
||||||
allAppliedFixes.push(fixName);
|
allAppliedFixes.push(fixName);
|
||||||
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'beatsaver-20210804': {
|
"beatsaver-20210804": {
|
||||||
apply: async fixName => {
|
apply: async (fixName) => {
|
||||||
log.info('Converting BeatSaver songs to a new format...', 'DBFix')
|
log.info("Converting BeatSaver songs to a new format...", "DBFix");
|
||||||
|
|
||||||
return db.runInTransaction(['songs', 'songs-beatmaps', 'key-value'], async tx => {
|
return db.runInTransaction(
|
||||||
const songsBeatMapsStore = tx.objectStore('songs-beatmaps');
|
["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;
|
let songCount = 0;
|
||||||
|
|
||||||
@ -55,90 +65,109 @@ const allFixes = {
|
|||||||
const beatSaverSong = cursor.value;
|
const beatSaverSong = cursor.value;
|
||||||
|
|
||||||
if (beatSaverSong?.metadata?.characteristics) {
|
if (beatSaverSong?.metadata?.characteristics) {
|
||||||
const beatMapsSong = beatmapsService.convertOldBeatSaverToBeatMaps(beatSaverSong);
|
const beatMapsSong =
|
||||||
|
beatmapsService.convertOldBeatSaverToBeatMaps(beatSaverSong);
|
||||||
if (beatMapsSong) {
|
if (beatMapsSong) {
|
||||||
songsBeatMapsStore.put(beatMapsSong)
|
songsBeatMapsStore.put(beatMapsSong);
|
||||||
|
|
||||||
songCount++;
|
songCount++;
|
||||||
} else {
|
} else {
|
||||||
log.info(`Unable to convert, deleting a song`, 'DBFix', beatSaverSong);
|
log.info(
|
||||||
|
`Unable to convert, deleting a song`,
|
||||||
|
"DBFix",
|
||||||
|
beatSaverSong,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
cursor = await cursor.continue();
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyValueStore = tx.objectStore('key-value')
|
const keyValueStore = tx.objectStore("key-value");
|
||||||
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
||||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
allAppliedFixes =
|
||||||
|
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||||
|
? allAppliedFixes
|
||||||
|
: [];
|
||||||
allAppliedFixes.push(fixName);
|
allAppliedFixes.push(fixName);
|
||||||
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
||||||
|
|
||||||
log.info(`${songCount} BeatSaver song(s) converted`, 'DBFix')
|
log.info(`${songCount} BeatSaver song(s) converted`, "DBFix");
|
||||||
});
|
},
|
||||||
}
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'twitch-20210808': {
|
"twitch-20210808": {
|
||||||
apply: async fixName => {
|
apply: async (fixName) => {
|
||||||
const predefinedProfiles = {
|
const predefinedProfiles = {
|
||||||
'76561198059659922': 'patian25',
|
"76561198059659922": "patian25",
|
||||||
'1994101560659098': 'xoxobluff',
|
1994101560659098: "xoxobluff",
|
||||||
'76561198138327464': 'altrowilddog',
|
"76561198138327464": "altrowilddog",
|
||||||
'76561198855288628': 'inbourne',
|
"76561198855288628": "inbourne",
|
||||||
'76561198136177445': 'riviengt',
|
"76561198136177445": "riviengt",
|
||||||
'76561199004224834': 'nyaanos',
|
"76561199004224834": "nyaanos",
|
||||||
'76561198023909718': 'danielduel',
|
"76561198023909718": "danielduel",
|
||||||
'76561198212019365': 'fnyt',
|
"76561198212019365": "fnyt",
|
||||||
'76561197966674102': 'maciekvr',
|
"76561197966674102": "maciekvr",
|
||||||
'76561198025451538': 'drakonno',
|
"76561198025451538": "drakonno",
|
||||||
'76561197994110158': 'sanorek',
|
"76561197994110158": "sanorek",
|
||||||
'76561198034203862': 'vr_agent',
|
"76561198034203862": "vr_agent",
|
||||||
'3702342373170767': 'xjedam',
|
3702342373170767: "xjedam",
|
||||||
'76561197995161445': 'mediekore',
|
"76561197995161445": "mediekore",
|
||||||
'76561198087710981': 'shreddyfreddy',
|
"76561198087710981": "shreddyfreddy",
|
||||||
'76561198999385463': 'woltixo',
|
"76561198999385463": "woltixo",
|
||||||
'76561198035381239': 'motzel',
|
"76561198035381239": "motzel",
|
||||||
'76561198178407566' : 'acetari',
|
"76561198178407566": "acetari",
|
||||||
'76561198045386379': 'duhhello',
|
"76561198045386379": "duhhello",
|
||||||
'76561198835772160': 'tornadoef6',
|
76561198835772160: "tornadoef6",
|
||||||
'76561198187936410': 'garsh_',
|
"76561198187936410": "garsh_",
|
||||||
'76561198362923485': 'tseska_',
|
"76561198362923485": "tseska_",
|
||||||
'76561198154190170': 'tieeli',
|
"76561198154190170": "tieeli",
|
||||||
'76561198333869741': 'cerret07',
|
"76561198333869741": "cerret07",
|
||||||
'76561197995162898': 'electrostats',
|
"76561197995162898": "electrostats",
|
||||||
'76561198166289091': 'rocker1904',
|
"76561198166289091": "rocker1904",
|
||||||
'2538637699496776': 'astrella_',
|
2538637699496776: "astrella_",
|
||||||
'76561198171842815': 'coolpickb',
|
"76561198171842815": "coolpickb",
|
||||||
'76561198145281261': 'harbgy'
|
"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,
|
lastUpdated: null,
|
||||||
login: twitchLogin,
|
login: twitchLogin,
|
||||||
playerId
|
playerId,
|
||||||
}
|
}),
|
||||||
)))
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await addAppliedFix(fixName);
|
await addAppliedFix(fixName);
|
||||||
|
|
||||||
log.info('Twitch profiles added.', 'DBFix')
|
log.info("Twitch profiles added.", "DBFix");
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'player-history-20211022': {
|
"player-history-20211022": {
|
||||||
apply: async fixName => {
|
apply: async (fixName) => {
|
||||||
log.info('Apply player ss history fix (20211022)')
|
log.info("Apply player ss history fix (20211022)");
|
||||||
|
|
||||||
return db.runInTransaction(['players-history', 'key-value'], async tx => {
|
return db.runInTransaction(
|
||||||
const playersHistoryStore = tx.objectStore('players-history');
|
["players-history", "key-value"],
|
||||||
|
async (tx) => {
|
||||||
|
const playersHistoryStore = tx.objectStore("players-history");
|
||||||
|
|
||||||
let cursor = await playersHistoryStore.openCursor();
|
let cursor = await playersHistoryStore.openCursor();
|
||||||
|
|
||||||
@ -157,33 +186,46 @@ const allFixes = {
|
|||||||
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
|
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
|
||||||
|
|
||||||
await cursor.delete();
|
await cursor.delete();
|
||||||
playersHistoryStore.put({...history, ssDate, playerIdSsTimestamp});
|
playersHistoryStore.put({
|
||||||
|
...history,
|
||||||
|
ssDate,
|
||||||
|
playerIdSsTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
cursor = await cursor.continue();
|
cursor = await cursor.continue();
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyValueStore = tx.objectStore('key-value')
|
const keyValueStore = tx.objectStore("key-value");
|
||||||
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
||||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
allAppliedFixes =
|
||||||
|
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||||
|
? allAppliedFixes
|
||||||
|
: [];
|
||||||
allAppliedFixes.push(fixName);
|
allAppliedFixes.push(fixName);
|
||||||
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
let appliedDbFixes = await getAppliedFixes();
|
let appliedDbFixes = await getAppliedFixes();
|
||||||
const appliedFixes = appliedDbFixes && Array.isArray(appliedDbFixes) ? appliedDbFixes : [];
|
const appliedFixes =
|
||||||
const neededFixes = Object.keys(allFixes).filter(f => !appliedFixes.includes(f) && (!allFixes[f].validTo || allFixes[f].validTo > new Date()));
|
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;
|
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) {
|
for (let key of neededFixes) {
|
||||||
await allFixes[key].apply(key);
|
await allFixes[key].apply(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = "";
|
||||||
}
|
};
|
||||||
|
@ -1,19 +1,29 @@
|
|||||||
import cacheRepository from './repository/cache';
|
import cacheRepository from "./repository/cache";
|
||||||
import groupsRepository from './repository/groups';
|
import groupsRepository from "./repository/groups";
|
||||||
import keyValueRepository from './repository/key-value';
|
import keyValueRepository from "./repository/key-value";
|
||||||
import playersRepository from './repository/players';
|
import playersRepository from "./repository/players";
|
||||||
import playersHistoryRepository from './repository/players-history';
|
import playersHistoryRepository from "./repository/players-history";
|
||||||
import rankedsRepository from './repository/rankeds';
|
import rankedsRepository from "./repository/rankeds";
|
||||||
import rankedsChangesRepository from './repository/rankeds-changes';
|
import rankedsChangesRepository from "./repository/rankeds-changes";
|
||||||
import scoresRepository from './repository/scores';
|
import scoresRepository from "./repository/scores";
|
||||||
import songsRepository from './repository/songs';
|
import songsRepository from "./repository/songs";
|
||||||
import twitchRepository from './repository/twitch';
|
import twitchRepository from "./repository/twitch";
|
||||||
import log from '../utils/logger';
|
import log from "../utils/logger";
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
log.debug('Initialize DB repositories');
|
log.debug("Initialize DB repositories");
|
||||||
|
|
||||||
// initialize all repositories in order to create cache to sync
|
// 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', {
|
export default () =>
|
||||||
'accsaber-players-history-playerId': 'playerId',
|
createRepository("accsaber-players-history", "playerIdTimestamp", {
|
||||||
'accsaber-players-history-playerIdTimestamp': '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(
|
export default () =>
|
||||||
'accsaber-players',
|
createRepository("accsaber-players", "id", {
|
||||||
'id',
|
"accsaber-players-playerId": "playerId",
|
||||||
{
|
"accsaber-players-category": "category",
|
||||||
'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', {
|
export default () =>
|
||||||
'beat-savior-playerId': 'playerId',
|
createRepository("beat-savior", "beatSaviorId", {
|
||||||
'beat-savior-hash': 'hash',
|
"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 cache from "../cache";
|
||||||
import {db} from '../db';
|
import { db } from "../db";
|
||||||
import {convertArrayToObjectByKey} from '../../utils/js'
|
import { convertArrayToObjectByKey } from "../../utils/js";
|
||||||
import makePendingPromisePool from '../../utils/pending-promises'
|
import makePendingPromisePool from "../../utils/pending-promises";
|
||||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||||
|
|
||||||
export const ALL_KEY = '__ALL';
|
export const ALL_KEY = "__ALL";
|
||||||
const NONE_KEY = '__NONE';
|
const NONE_KEY = "__NONE";
|
||||||
|
|
||||||
let repositories = {};
|
let repositories = {};
|
||||||
|
|
||||||
@ -20,46 +20,52 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
|||||||
const getKeyName = () => inlineKeyName;
|
const getKeyName = () => inlineKeyName;
|
||||||
const hasOutOfLineKey = () => getKeyName() === undefined;
|
const hasOutOfLineKey = () => getKeyName() === undefined;
|
||||||
const getObjKey = (obj, outOfLineKey = undefined) => {
|
const getObjKey = (obj, outOfLineKey = undefined) => {
|
||||||
const key = hasOutOfLineKey() ? outOfLineKey : obj[inlineKeyName]
|
const key = hasOutOfLineKey() ? outOfLineKey : obj[inlineKeyName];
|
||||||
return key ? key : outOfLineKey;
|
return key ? key : outOfLineKey;
|
||||||
}
|
};
|
||||||
|
|
||||||
let repositoryCache = cache(repositoryName, getObjKey);
|
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 getFieldForIndexName = (indexName) => indexesKeyNames[indexName];
|
||||||
const isFieldForIndexDefined = indexName => !!getFieldForIndexName(indexName);
|
const isFieldForIndexDefined = (indexName) =>
|
||||||
|
!!getFieldForIndexName(indexName);
|
||||||
|
|
||||||
const setDataAvailabilityStatus = cacheKey => dataAvailableFor[cacheKey] = true;
|
const setDataAvailabilityStatus = (cacheKey) =>
|
||||||
const setAllDataAvailabilityStatus = () => setDataAvailabilityStatus(getCacheKeyFor());
|
(dataAvailableFor[cacheKey] = true);
|
||||||
const removeDataAvailabilityStatus = cacheKey => {
|
const setAllDataAvailabilityStatus = () =>
|
||||||
|
setDataAvailabilityStatus(getCacheKeyFor());
|
||||||
|
const removeDataAvailabilityStatus = (cacheKey) => {
|
||||||
delete dataAvailableFor[cacheKey];
|
delete dataAvailableFor[cacheKey];
|
||||||
delete dataAvailableFor[getCacheKeyFor()];
|
delete dataAvailableFor[getCacheKeyFor()];
|
||||||
}
|
};
|
||||||
const flushDataAvailabilityStatus = () => dataAvailableFor = {};
|
const flushDataAvailabilityStatus = () => (dataAvailableFor = {});
|
||||||
const isIndexDataAvailable = cacheKey => !!dataAvailableFor[cacheKey];
|
const isIndexDataAvailable = (cacheKey) => !!dataAvailableFor[cacheKey];
|
||||||
const isAllDataAvailable = () => isIndexDataAvailable(getCacheKeyFor());
|
const isAllDataAvailable = () => isIndexDataAvailable(getCacheKeyFor());
|
||||||
|
|
||||||
const flushCache = () => {
|
const flushCache = () => {
|
||||||
repositoryCache.flush();
|
repositoryCache.flush();
|
||||||
flushDataAvailabilityStatus();
|
flushDataAvailabilityStatus();
|
||||||
}
|
};
|
||||||
|
|
||||||
const forgetCacheKey = key => repositoryCache.forget(key);
|
const forgetCacheKey = (key) => repositoryCache.forget(key);
|
||||||
|
|
||||||
const forgetObject = async obj => {
|
const forgetObject = async (obj) => {
|
||||||
if (hasOutOfLineKey()) throw 'forgetObject function is not available in repositories with out-of-line keys';
|
if (hasOutOfLineKey())
|
||||||
|
throw "forgetObject function is not available in repositories with out-of-line keys";
|
||||||
|
|
||||||
const key = getObjKey(obj);
|
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);
|
forgetCacheKey(key);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getStoreName = () => storeName;
|
const getStoreName = () => storeName;
|
||||||
|
|
||||||
const getCachedKeys = _ => repositoryCache.getKeys();
|
const getCachedKeys = (_) => repositoryCache.getKeys();
|
||||||
|
|
||||||
const getAllKeys = async () => db.getAllKeys(storeName);
|
const getAllKeys = async () => db.getAllKeys(storeName);
|
||||||
|
|
||||||
@ -68,16 +74,23 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
|||||||
|
|
||||||
const cacheKey = getCacheKeyFor(key);
|
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) => {
|
const getFromIndex = async (indexName, query, refreshCache = false) => {
|
||||||
if (hasOutOfLineKey()) throw `getFromIndex() is not available for stores with out-of-line key`;
|
if (hasOutOfLineKey())
|
||||||
if (!isFieldForIndexDefined(indexName)) throw `Index ${indexName} has no field set`;
|
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();
|
if (query && query instanceof IDBKeyRange) return getFromDb();
|
||||||
|
|
||||||
@ -85,7 +98,8 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
|||||||
|
|
||||||
const fullIndexCacheKey = getCacheKeyFor(query, indexName);
|
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) {
|
if (refreshCache) {
|
||||||
removeDataAvailabilityStatus(cacheKey);
|
removeDataAvailabilityStatus(cacheKey);
|
||||||
@ -94,24 +108,34 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
|||||||
repositoryCache.forgetByFilter(filterItems);
|
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 getAll = async (refreshCache = false) => {
|
||||||
const cacheKey = getCacheKeyFor();
|
const cacheKey = getCacheKeyFor();
|
||||||
|
|
||||||
const getFromDb = () => resolvePromiseOrWaitForPending(cacheKey, () => db.getAll(storeName))
|
const getFromDb = () =>
|
||||||
|
resolvePromiseOrWaitForPending(cacheKey, () => db.getAll(storeName));
|
||||||
|
|
||||||
if (hasOutOfLineKey()) return getFromDb();
|
if (hasOutOfLineKey()) return getFromDb();
|
||||||
|
|
||||||
if (refreshCache) flushCache();
|
if (refreshCache) flushCache();
|
||||||
|
|
||||||
const filterUndefined = item => item !== undefined;
|
const filterUndefined = (item) => item !== undefined;
|
||||||
|
|
||||||
if (!isAllDataAvailable()) {
|
if (!isAllDataAvailable()) {
|
||||||
const data = convertArrayToObjectByKey(await getFromDb(), inlineKeyName);
|
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();
|
setAllDataAvailabilityStatus();
|
||||||
|
|
||||||
@ -119,28 +143,39 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(repositoryCache.getAll()).filter(filterUndefined);
|
return Object.values(repositoryCache.getAll()).filter(filterUndefined);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getAllFromIndex = async(indexName, query = undefined, refreshCache = false) => {
|
const getAllFromIndex = async (
|
||||||
if (hasOutOfLineKey()) throw `getAllFromIndex() is not available for stores with out-of-line key`;
|
indexName,
|
||||||
if (!isFieldForIndexDefined(indexName)) throw `Index ${indexName} has no field set`;
|
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 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();
|
if (query && query instanceof IDBKeyRange) return getFromDb();
|
||||||
|
|
||||||
const field = getFieldForIndexName(indexName);
|
const field = getFieldForIndexName(indexName);
|
||||||
|
|
||||||
const filterItems = item => item !== undefined && (!query || item[field] === query);
|
const filterItems = (item) =>
|
||||||
|
item !== undefined && (!query || item[field] === query);
|
||||||
|
|
||||||
if (refreshCache) {
|
if (refreshCache) {
|
||||||
removeDataAvailabilityStatus(cacheKey);
|
removeDataAvailabilityStatus(cacheKey);
|
||||||
repositoryCache.forgetByFilter(filterItems);
|
repositoryCache.forgetByFilter(filterItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFromDbAndUpdateCache = async () => resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => {
|
const getFromDbAndUpdateCache = async () =>
|
||||||
|
resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => {
|
||||||
const data = await getFromDb();
|
const data = await getFromDb();
|
||||||
|
|
||||||
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
|
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
|
||||||
@ -148,63 +183,71 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
|||||||
setDataAvailabilityStatus(cacheKey);
|
setDataAvailabilityStatus(cacheKey);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!isAllDataAvailable() && !isIndexDataAvailable(cacheKey)) return await getFromDbAndUpdateCache();
|
if (!isAllDataAvailable() && !isIndexDataAvailable(cacheKey))
|
||||||
|
return await getFromDbAndUpdateCache();
|
||||||
|
|
||||||
return Object.values(repositoryCache.getAll()).filter(filterItems);
|
return Object.values(repositoryCache.getAll()).filter(filterItems);
|
||||||
}
|
};
|
||||||
|
|
||||||
const set = async (value, key = undefined, tx = null) => {
|
const set = async (value, key = undefined, tx = null) => {
|
||||||
const txStores = tx ? [...tx.objectStoreNames] : null;
|
const txStores = tx ? [...tx.objectStoreNames] : null;
|
||||||
|
|
||||||
let putKey;
|
let putKey;
|
||||||
if (tx && txStores.includes(storeName)) {
|
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 {
|
} 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;
|
if (!hasOutOfLineKey() && !getObjKey(value)) value[inlineKeyName] = putKey;
|
||||||
|
|
||||||
return repositoryCache.set(getObjKey(value, key), value);
|
return repositoryCache.set(getObjKey(value, key), value);
|
||||||
}
|
};
|
||||||
|
|
||||||
const del = async key => {
|
const del = async (key) => {
|
||||||
await db.delete(storeName, key);
|
await db.delete(storeName, key);
|
||||||
|
|
||||||
return repositoryCache.forget(key);
|
return repositoryCache.forget(key);
|
||||||
}
|
};
|
||||||
|
|
||||||
const deleteObject = async obj => {
|
const deleteObject = async (obj) => {
|
||||||
if (hasOutOfLineKey()) throw 'deleteObject function is not available in repositories with out-of-line keys';
|
if (hasOutOfLineKey())
|
||||||
|
throw "deleteObject function is not available in repositories with out-of-line keys";
|
||||||
|
|
||||||
const key = getObjKey(obj);
|
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);
|
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) => {
|
const setCache = (value, key) => {
|
||||||
if (hasOutOfLineKey()) {
|
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 {
|
} else {
|
||||||
key = getObjKey(value, key);
|
key = getObjKey(value, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
repositoryCache.set(key, value);
|
repositoryCache.set(key, value);
|
||||||
}
|
};
|
||||||
const addToCache = data => {
|
const addToCache = (data) => {
|
||||||
if (hasOutOfLineKey()) throw `addToCache() is not available for stores (${storeName}) with out-of-line key`;
|
if (hasOutOfLineKey())
|
||||||
|
throw `addToCache() is not available for stores (${storeName}) with out-of-line key`;
|
||||||
|
|
||||||
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
|
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
|
||||||
}
|
};
|
||||||
|
|
||||||
const getCache = () => repositoryCache;
|
const getCache = () => repositoryCache;
|
||||||
|
|
||||||
return repositories[repositoryName] = {
|
return (repositories[repositoryName] = {
|
||||||
getStoreName,
|
getStoreName,
|
||||||
hasOutOfLineKey,
|
hasOutOfLineKey,
|
||||||
getAllKeys,
|
getAllKeys,
|
||||||
@ -224,5 +267,5 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
|||||||
setCache,
|
setCache,
|
||||||
addToCache,
|
addToCache,
|
||||||
getCache,
|
getCache,
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import createRepository from './generic';
|
import createRepository from "./generic";
|
||||||
|
|
||||||
let repository;
|
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', {
|
export default () =>
|
||||||
'players-history-playerId': 'playerId',
|
createRepository("players-history", "_idbId", {
|
||||||
'players-history-playerIdSsTimestamp': 'playerIdSsTimestamp'
|
"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(
|
export default () =>
|
||||||
'scores-update-queue',
|
createRepository("scores-update-queue", "id", {
|
||||||
'id',
|
"scores-update-queue-fetchedAt": "fetchedAt",
|
||||||
{
|
});
|
||||||
'scores-update-queue-fetchedAt': 'fetchedAt',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import createRepository from './generic';
|
import createRepository from "./generic";
|
||||||
|
|
||||||
export default () => createRepository(
|
export default () =>
|
||||||
'scores',
|
createRepository("scores", "id", {
|
||||||
'id',
|
"scores-timeset": "timeset",
|
||||||
{
|
"scores-leaderboardId": "leaderboardId",
|
||||||
'scores-timeset': 'timeset',
|
"scores-playerId": "playerId",
|
||||||
'scores-leaderboardId': 'leaderboardId',
|
"scores-pp": "pp",
|
||||||
'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");
|
||||||
|
45
src/main.js
45
src/main.js
@ -1,18 +1,18 @@
|
|||||||
import App from './App.svelte';
|
import App from "./App.svelte";
|
||||||
import log from './utils/logger'
|
import log from "./utils/logger";
|
||||||
import initDb from './db/db'
|
import initDb from "./db/db";
|
||||||
import initializeRepositories from './db/repositories-init';
|
import initializeRepositories from "./db/repositories-init";
|
||||||
import setupDataFixes from './db/fix-data'
|
import setupDataFixes from "./db/fix-data";
|
||||||
import createConfigStore from './stores/config'
|
import createConfigStore from "./stores/config";
|
||||||
import createPlayerService from './services/scoresaber/player'
|
import createPlayerService from "./services/scoresaber/player";
|
||||||
import createBeatSaviorService from './services/beatsavior'
|
import createBeatSaviorService from "./services/beatsavior";
|
||||||
import createRankedsStore from './stores/scoresaber/rankeds'
|
import createRankedsStore from "./stores/scoresaber/rankeds";
|
||||||
import initDownloadManager from './network/download-manager'
|
import initDownloadManager from "./network/download-manager";
|
||||||
import initCommandProcessor from './network/command-processor'
|
import initCommandProcessor from "./network/command-processor";
|
||||||
import {enablePatches, setAutoFreeze} from 'immer'
|
import { enablePatches, setAutoFreeze } from "immer";
|
||||||
import {initCompareEnhancer} from './stores/http/enhancers/scores/compare'
|
import { initCompareEnhancer } from "./stores/http/enhancers/scores/compare";
|
||||||
import ErrorComponent from './components/Common/Error.svelte'
|
import ErrorComponent from "./components/Common/Error.svelte";
|
||||||
import initializeWorkers from './utils/worker-wrappers'
|
import initializeWorkers from "./utils/worker-wrappers";
|
||||||
|
|
||||||
let app = null;
|
let app = null;
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ let app = null;
|
|||||||
// log.setLevel(log.TRACE);
|
// log.setLevel(log.TRACE);
|
||||||
// log.logOnly(['AccSaberService']);
|
// log.logOnly(['AccSaberService']);
|
||||||
|
|
||||||
log.info('Starting up...', 'Main')
|
log.info("Starting up...", "Main");
|
||||||
|
|
||||||
await initDb();
|
await initDb();
|
||||||
await initializeRepositories();
|
await initializeRepositories();
|
||||||
@ -47,7 +47,7 @@ let app = null;
|
|||||||
|
|
||||||
initCommandProcessor(await initDownloadManager());
|
initCommandProcessor(await initDownloadManager());
|
||||||
|
|
||||||
log.info('Site initialized', 'Main')
|
log.info("Site initialized", "Main");
|
||||||
|
|
||||||
app = new App({
|
app = new App({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
@ -56,8 +56,14 @@ let app = null;
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
if (error instanceof DOMException && error.toString() === 'InvalidStateError: A mutation operation was attempted on a database that did not allow mutations.')
|
if (
|
||||||
error = new Error('Firefox in private mode does not support the database. Please run the site in normal mode.')
|
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({
|
app = new ErrorComponent({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
@ -66,5 +72,4 @@ let app = null;
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
export default app;
|
export default app;
|
@ -1,5 +1,5 @@
|
|||||||
// import eventBus from '../utils/broadcast-channel-pubsub'
|
// import eventBus from '../utils/broadcast-channel-pubsub'
|
||||||
import {addToDate, MINUTE} from '../utils/date'
|
import { addToDate, MINUTE } from "../utils/date";
|
||||||
|
|
||||||
const DEFAULT_CACHE_SIZE = 100;
|
const DEFAULT_CACHE_SIZE = 100;
|
||||||
|
|
||||||
@ -7,20 +7,25 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
|||||||
let cache = {};
|
let cache = {};
|
||||||
let cacheSize = size;
|
let cacheSize = size;
|
||||||
|
|
||||||
const isWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope
|
const isWorker =
|
||||||
|
typeof WorkerGlobalScope !== "undefined" &&
|
||||||
|
self instanceof WorkerGlobalScope;
|
||||||
|
|
||||||
const defaultExpiryIn = expiryIn;
|
const defaultExpiryIn = expiryIn;
|
||||||
|
|
||||||
const packValue = value => {
|
const packValue = (value) => {
|
||||||
if (!value || typeof value !== 'object') return value;
|
if (!value || typeof value !== "object") return value;
|
||||||
|
|
||||||
const newValue = { ...value };
|
const newValue = { ...value };
|
||||||
|
|
||||||
if (value.headers && value.headers instanceof Headers) {
|
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;
|
cum[key] = value;
|
||||||
return cum;
|
return cum;
|
||||||
}, {})
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.body && value.body instanceof Document) {
|
if (value.body && value.body instanceof Document) {
|
||||||
@ -28,25 +33,29 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return newValue;
|
return newValue;
|
||||||
}
|
};
|
||||||
|
|
||||||
const unpackValue = value => {
|
const unpackValue = (value) => {
|
||||||
if (!value || typeof value !== 'object') return value;
|
if (!value || typeof value !== "object") return value;
|
||||||
|
|
||||||
const newValue = { ...value };
|
const newValue = { ...value };
|
||||||
|
|
||||||
if (value.headers) {
|
if (value.headers) {
|
||||||
const headers = new 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;
|
newValue.headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.body) {
|
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;
|
return newValue;
|
||||||
}
|
};
|
||||||
|
|
||||||
// update data cached on another node
|
// 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);
|
// 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 flushUnsubscribe = eventBus.on('net-cache-flush', (_, isLocal) => !isLocal ? flush(false) : null);
|
||||||
|
|
||||||
const has = (key, maxAge = null, withExpired = false) =>
|
const has = (key, maxAge = null, withExpired = false) =>
|
||||||
cache.hasOwnProperty(key) && cache[key] &&
|
cache.hasOwnProperty(key) &&
|
||||||
(withExpired || !cache[key].expiryAt || cache[key].expiryAt >= new Date()) &&
|
cache[key] &&
|
||||||
(!Number.isFinite(maxAge) || !cache[key].cachedAt || addToDate(maxAge, cache[key].cachedAt) >= new Date());
|
(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) => {
|
const set = (key, value, expiryIn = null, emitEvent = true) => {
|
||||||
expiryIn = expiryIn ? expiryIn : defaultExpiryIn;
|
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});
|
// 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;
|
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;
|
const getAll = () => cache;
|
||||||
|
|
||||||
@ -82,7 +107,7 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
|||||||
// if (emitEvent) eventBus.publish('net-cache-key-forget', {key});
|
// if (emitEvent) eventBus.publish('net-cache-key-forget', {key});
|
||||||
|
|
||||||
return cache;
|
return cache;
|
||||||
}
|
};
|
||||||
|
|
||||||
const flush = (emitEvent = true) => {
|
const flush = (emitEvent = true) => {
|
||||||
cache = {};
|
cache = {};
|
||||||
@ -90,7 +115,7 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
|||||||
// if (emitEvent) eventBus.publish('net-cache-flush', {});
|
// if (emitEvent) eventBus.publish('net-cache-flush', {});
|
||||||
|
|
||||||
return cache;
|
return cache;
|
||||||
}
|
};
|
||||||
|
|
||||||
const garbageCollect = (size = cacheSize) => {
|
const garbageCollect = (size = cacheSize) => {
|
||||||
const values = Object.values(cache);
|
const values = Object.values(cache);
|
||||||
@ -99,14 +124,17 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
|||||||
cache = values
|
cache = values
|
||||||
.sort((a, b) => b.expiryAt - a.expiryAt)
|
.sort((a, b) => b.expiryAt - a.expiryAt)
|
||||||
.slice(0, size)
|
.slice(0, size)
|
||||||
.reduce((cum, item) => {cum[item.key] = item; return cum;}, {});
|
.reduce((cum, item) => {
|
||||||
}
|
cum[item.key] = item;
|
||||||
|
return cum;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
const destroy = () => {
|
const destroy = () => {
|
||||||
// setUnsubscribe();
|
// setUnsubscribe();
|
||||||
// forgetUnsubscribe();
|
// forgetUnsubscribe();
|
||||||
// flushUnsubscribe();
|
// flushUnsubscribe();
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
has,
|
has,
|
||||||
@ -117,5 +145,5 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
|||||||
forget,
|
forget,
|
||||||
flush,
|
flush,
|
||||||
destroy,
|
destroy,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
if (!response || !Array.isArray(response)) return [];
|
if (!response || !Array.isArray(response)) return [];
|
||||||
|
|
||||||
return response.map(c => ({
|
return response.map((c) => ({
|
||||||
name: c.categoryName,
|
name: c.categoryName,
|
||||||
displayName: c.categoryDisplayName,
|
displayName: c.categoryDisplayName,
|
||||||
countsTowardsOverall: c.countsTowardsOverall,
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
import {dateFromString, formatDateRelative} from '../../../utils/date'
|
import { dateFromString, formatDateRelative } from "../../../utils/date";
|
||||||
import {LEADERBOARD_SCORES_PER_PAGE} from '../../../utils/accsaber/consts'
|
import { LEADERBOARD_SCORES_PER_PAGE } from "../../../utils/accsaber/consts";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
if (!response || !Array.isArray(response.responses) || response.responses.length !== 2 || !Array.isArray(response.responses[0])) return [];
|
if (
|
||||||
|
!response ||
|
||||||
|
!Array.isArray(response.responses) ||
|
||||||
|
response.responses.length !== 2 ||
|
||||||
|
!Array.isArray(response.responses[0])
|
||||||
|
)
|
||||||
|
return [];
|
||||||
|
|
||||||
const page = response?.fetchOptions.page ?? 1;
|
const page = response?.fetchOptions.page ?? 1;
|
||||||
const totalItems = response.responses[0].length;
|
const totalItems = response.responses[0].length;
|
||||||
@ -25,16 +31,32 @@ const process = response => {
|
|||||||
difficulty,
|
difficulty,
|
||||||
} = mapInfo;
|
} = mapInfo;
|
||||||
|
|
||||||
const song = {hash, name, subName, authorName, levelAuthorName, beatsaverKey};
|
const song = {
|
||||||
const diffInfo = {type: 'Standard', diff: difficulty?.toLowerCase()?.replace('plus', 'Plus')}
|
hash,
|
||||||
const leaderboard = {leaderboardId, song, diffInfo, complexity, categoryDisplayName};
|
name,
|
||||||
|
subName,
|
||||||
|
authorName,
|
||||||
|
levelAuthorName,
|
||||||
|
beatsaverKey,
|
||||||
|
};
|
||||||
|
const diffInfo = {
|
||||||
|
type: "Standard",
|
||||||
|
diff: difficulty?.toLowerCase()?.replace("plus", "Plus"),
|
||||||
|
};
|
||||||
|
const leaderboard = {
|
||||||
|
leaderboardId,
|
||||||
|
song,
|
||||||
|
diffInfo,
|
||||||
|
complexity,
|
||||||
|
categoryDisplayName,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page,
|
page,
|
||||||
pageQty,
|
pageQty,
|
||||||
totalItems,
|
totalItems,
|
||||||
leaderboard,
|
leaderboard,
|
||||||
scores: response.responses[0].map(s => {
|
scores: response.responses[0].map((s) => {
|
||||||
let {
|
let {
|
||||||
accuracy: acc,
|
accuracy: acc,
|
||||||
ap,
|
ap,
|
||||||
@ -48,14 +70,16 @@ const process = response => {
|
|||||||
|
|
||||||
if (acc && Number.isFinite(acc)) acc *= 100;
|
if (acc && Number.isFinite(acc)) acc *= 100;
|
||||||
|
|
||||||
timeSet = dateFromString(timeSet)
|
timeSet = dateFromString(timeSet);
|
||||||
const timeSetString = formatDateRelative(timeSet);
|
const timeSetString = formatDateRelative(timeSet);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
player: {
|
player: {
|
||||||
name,
|
name,
|
||||||
playerId,
|
playerId,
|
||||||
playerInfo: {avatar: `https://cdn.accsaber.com/avatars/${playerId}.jpg`},
|
playerInfo: {
|
||||||
|
avatar: `https://cdn.accsaber.com/avatars/${playerId}.jpg`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
score: {
|
score: {
|
||||||
acc,
|
acc,
|
||||||
@ -67,19 +91,30 @@ const process = response => {
|
|||||||
timeSetString,
|
timeSetString,
|
||||||
},
|
},
|
||||||
other: rest,
|
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([
|
const responses = await Promise.all([
|
||||||
queue.ACCSABER.leaderboard(leaderboardId, page, priority, queueOptions),
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,28 +1,43 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
import {fromAccSaberDateString} from '../../../utils/date'
|
import { fromAccSaberDateString } from "../../../utils/date";
|
||||||
import {isDateObject} from '../../../utils/js'
|
import { isDateObject } from "../../../utils/js";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
const playerId = response?.fetchOptions?.playerId ?? null;
|
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 {
|
return {
|
||||||
playerId,
|
playerId,
|
||||||
history: Object.entries(response.response)
|
history: Object.entries(response.response)
|
||||||
.map(([date, rank]) => ({ date: fromAccSaberDateString(date), rank }))
|
.map(([date, rank]) => ({ date: fromAccSaberDateString(date), rank }))
|
||||||
.filter(obj => isDateObject(obj?.date))
|
.filter((obj) => isDateObject(obj?.date))
|
||||||
.sort((a,b) => a.date.getTime() - b.date.getTime())
|
.sort((a, b) => a.date.getTime() - b.date.getTime()),
|
||||||
,
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
|
const get = async ({
|
||||||
const response = await queue.ACCSABER.playerRankHistory(playerId, priority, queueOptions);
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,23 +1,36 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
const category = response?.fetchOptions?.category ?? 'overall';
|
const category = response?.fetchOptions?.category ?? "overall";
|
||||||
if (!response?.response || !Array.isArray(response.response)) return [];
|
if (!response?.response || !Array.isArray(response.response)) return [];
|
||||||
|
|
||||||
return response.response.map(p => ({
|
return response.response.map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
id: `${p.playerId}-${category}`,
|
id: `${p.playerId}-${category}`,
|
||||||
category,
|
category,
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
}));
|
}));
|
||||||
}
|
};
|
||||||
|
|
||||||
const get = async ({category = 'overall', page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
|
const get = async ({
|
||||||
const response = await queue.ACCSABER.ranking(category, page, priority, queueOptions);
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
import {dateFromString} from '../../../utils/date'
|
import { dateFromString } from "../../../utils/date";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
const playerId = response?.fetchOptions?.playerId ?? null;
|
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 {
|
let {
|
||||||
songHash: hash,
|
songHash: hash,
|
||||||
songName: name,
|
songName: name,
|
||||||
@ -28,11 +29,27 @@ const process = response => {
|
|||||||
leaderboardId = parseInt(leaderboardId, 10);
|
leaderboardId = parseInt(leaderboardId, 10);
|
||||||
if (isNaN(leaderboardId)) leaderboardId = null;
|
if (isNaN(leaderboardId)) leaderboardId = null;
|
||||||
|
|
||||||
const song = {hash, name, subName: '', authorName, levelAuthorName, beatsaverKey};
|
const song = {
|
||||||
const diffInfo = {type: 'Standard', diff: difficulty?.toLowerCase()?.replace('plus', 'Plus')}
|
hash,
|
||||||
const leaderboard = {leaderboardId, song, diffInfo, complexity, categoryDisplayName};
|
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 {
|
return {
|
||||||
id: `${playerId}-${s.leaderboardId}`,
|
id: `${playerId}-${s.leaderboardId}`,
|
||||||
playerId,
|
playerId,
|
||||||
@ -41,18 +58,41 @@ const process = response => {
|
|||||||
ap,
|
ap,
|
||||||
acc,
|
acc,
|
||||||
leaderboard,
|
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(),
|
fetchedAt: new Date(),
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
|
const get = async ({
|
||||||
const response = await queue.ACCSABER.scores(playerId, page, priority, queueOptions);
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
import process from './utils/process'
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
import process from './utils/process'
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
|
|
||||||
export default response => {
|
export default (response) => {
|
||||||
const versions = opt(response, 'versions');
|
const versions = opt(response, "versions");
|
||||||
if (!versions || !Array.isArray(versions) || !versions.length) return null;
|
if (!versions || !Array.isArray(versions) || !versions.length) return null;
|
||||||
|
|
||||||
const lastIdx = versions.length - 1;
|
const lastIdx = versions.length - 1;
|
||||||
|
|
||||||
const hash = opt(versions, `${lastIdx}.hash`);
|
const hash = opt(versions, `${lastIdx}.hash`);
|
||||||
const key = opt(response, 'id');
|
const key = opt(response, "id");
|
||||||
|
|
||||||
if (!hash || !key || !hash.toLowerCase) return null;
|
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 queue from "../../queues/queues";
|
||||||
import {dateFromString} from '../../../utils/date'
|
import { dateFromString } from "../../../utils/date";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
|
|
||||||
const SONG_DATA_TYPES = {
|
const SONG_DATA_TYPES = {
|
||||||
None: 0,
|
None: 0,
|
||||||
@ -8,14 +8,14 @@ const SONG_DATA_TYPES = {
|
|||||||
Fail: 2,
|
Fail: 2,
|
||||||
Practice: 3,
|
Practice: 3,
|
||||||
Replay: 4,
|
Replay: 4,
|
||||||
Campaign: 5
|
Campaign: 5,
|
||||||
}
|
};
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
if (!response || !Array.isArray(response)) return null;
|
if (!response || !Array.isArray(response)) return null;
|
||||||
|
|
||||||
return response
|
return response
|
||||||
.map(s => {
|
.map((s) => {
|
||||||
let {
|
let {
|
||||||
_id: beatSaviorId,
|
_id: beatSaviorId,
|
||||||
playerID: playerId,
|
playerID: playerId,
|
||||||
@ -29,28 +29,65 @@ const process = response => {
|
|||||||
timeSet,
|
timeSet,
|
||||||
trackers,
|
trackers,
|
||||||
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 },
|
winTracker: { won, nbOfPause: pauses, rank },
|
||||||
hitTracker: {bombHit, miss, missedNotes, badCuts, nbOfWallHit: wallHit, maxCombo},
|
hitTracker: {
|
||||||
|
bombHit,
|
||||||
|
miss,
|
||||||
|
missedNotes,
|
||||||
|
badCuts,
|
||||||
|
nbOfWallHit: wallHit,
|
||||||
|
maxCombo,
|
||||||
|
},
|
||||||
scoreTracker: { score },
|
scoreTracker: { score },
|
||||||
},
|
},
|
||||||
} = s;
|
} = 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;
|
const leaderboardId = null;
|
||||||
|
|
||||||
hash = hash ? hash.toLowerCase() : 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 = {
|
const leaderboard = {
|
||||||
leaderboardId,
|
leaderboardId,
|
||||||
difficulty,
|
difficulty,
|
||||||
diffInfo: {diff: diff === 'expertplus' ? 'expertPlus' : diff, type: 'Standard'},
|
diffInfo: {
|
||||||
|
diff: diff === "expertplus" ? "expertPlus" : diff,
|
||||||
|
type: "Standard",
|
||||||
|
},
|
||||||
song,
|
song,
|
||||||
}
|
};
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
won,
|
won,
|
||||||
@ -62,9 +99,17 @@ const process = response => {
|
|||||||
bombHit,
|
bombHit,
|
||||||
wallHit,
|
wallHit,
|
||||||
maxCombo,
|
maxCombo,
|
||||||
accLeft, accRight, leftAverageCut, rightAverageCut, leftTimeDependence, rightTimeDependence,
|
accLeft,
|
||||||
leftPreswing, leftPostswing, rightPreswing, rightPostswing,
|
accRight,
|
||||||
}
|
leftAverageCut,
|
||||||
|
rightAverageCut,
|
||||||
|
leftTimeDependence,
|
||||||
|
rightTimeDependence,
|
||||||
|
leftPreswing,
|
||||||
|
leftPostswing,
|
||||||
|
rightPreswing,
|
||||||
|
rightPostswing,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
beatSaviorId,
|
beatSaviorId,
|
||||||
@ -72,24 +117,27 @@ const process = response => {
|
|||||||
leaderboardId,
|
leaderboardId,
|
||||||
scoreId: null,
|
scoreId: null,
|
||||||
hash,
|
hash,
|
||||||
diff: diff === 'expertplus' ? 'expertPlus' : diff,
|
diff: diff === "expertplus" ? "expertPlus" : diff,
|
||||||
score,
|
score,
|
||||||
type,
|
type,
|
||||||
leaderboard,
|
leaderboard,
|
||||||
timeSet: dateFromString(timeSet),
|
timeSet: dateFromString(timeSet),
|
||||||
stats,
|
stats,
|
||||||
trackers,
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...client,
|
...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) => {
|
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 });
|
const response = await get({ ...getOptions, priority });
|
||||||
|
|
||||||
return fullResponse ? response : getResponseBody(response);
|
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 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 {
|
return {
|
||||||
get: clientGet,
|
get: clientGet,
|
||||||
@ -21,5 +37,5 @@ export default (get, process) => {
|
|||||||
getProcessed: clientGetProcessed,
|
getProcessed: clientGetProcessed,
|
||||||
getDataFromResponse: getResponseBody,
|
getDataFromResponse: getResponseBody,
|
||||||
isResponseCached,
|
isResponseCached,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,34 +1,63 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
import createClient from '../../generic'
|
import createClient from "../../generic";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
if (!opt(response, 'scores') || !Array.isArray(response.scores)) return null;
|
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;
|
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;
|
else if (!mods) mods = null;
|
||||||
|
|
||||||
const acc = unmodifiedScore && opt(score, 'maxScore') ? unmodifiedScore / score.maxScore * 100 : opt(score, 'acc', null);
|
const acc =
|
||||||
const percentage = opt(score, 'score') && opt(score, 'maxScore') ? score.score / score.maxScore * 100 : opt(score, 'percentage', null);
|
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 {
|
return {
|
||||||
...s,
|
...s,
|
||||||
score: {...score, unmodifiedScore: unmodifiedScore || null, mods, acc, percentage, ppWeighted},
|
score: {
|
||||||
|
...score,
|
||||||
|
unmodifiedScore: unmodifiedScore || null,
|
||||||
|
mods,
|
||||||
|
acc,
|
||||||
|
percentage,
|
||||||
|
ppWeighted,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...response,
|
...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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,34 +1,56 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import createClient from '../../generic'
|
import createClient from "../../generic";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
if (!opt(response, 'playerInfo')) return null;
|
if (!opt(response, "playerInfo")) return null;
|
||||||
|
|
||||||
const { playerInfo: info, scoreStats } = response;
|
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) {
|
||||||
if (!avatar.startsWith('http'))
|
if (!avatar.startsWith("http"))
|
||||||
playerInfo.avatar = `${queue.SCORESABER_API.SS_API_HOST}${!avatar.startsWith('/') ? '/' : ''}${avatar}`;
|
playerInfo.avatar = `${queue.SCORESABER_API.SS_API_HOST}${
|
||||||
else
|
!avatar.startsWith("/") ? "/" : ""
|
||||||
playerInfo.avatar = avatar;
|
}${avatar}`;
|
||||||
|
else playerInfo.avatar = avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerInfo.banned = !!playerInfo.banned;
|
playerInfo.banned = !!playerInfo.banned;
|
||||||
playerInfo.inactive = !!playerInfo.inactive;
|
playerInfo.inactive = !!playerInfo.inactive;
|
||||||
playerInfo.rankHistory = playerInfo.history && playerInfo.history.length
|
playerInfo.rankHistory =
|
||||||
? playerInfo.history.split(',').map(r => parseInt(r, 10)).filter(r => !isNaN(r))
|
playerInfo.history && playerInfo.history.length
|
||||||
|
? playerInfo.history
|
||||||
|
.split(",")
|
||||||
|
.map((r) => parseInt(r, 10))
|
||||||
|
.filter((r) => !isNaN(r))
|
||||||
: [];
|
: [];
|
||||||
delete playerInfo.history;
|
delete playerInfo.history;
|
||||||
|
|
||||||
playerInfo.externalProfileUrl = null;
|
playerInfo.externalProfileUrl = null;
|
||||||
playerInfo.countries = [{ country, rank: countryRank }];
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,21 +1,27 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import api from './api'
|
import api from "./api";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
import createClient from '../../generic'
|
import createClient from "../../generic";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
const apiProcessedResponse = api.process(response && response.player ? response.player : null);
|
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 recentPlay = opt(response, "player.recentPlay");
|
||||||
const recentPlayLastUpdated = opt(response, 'player.recentPlayLastUpdated');
|
const recentPlayLastUpdated = opt(response, "player.recentPlayLastUpdated");
|
||||||
if (recentPlay && recentPlayLastUpdated) {
|
if (recentPlay && recentPlayLastUpdated) {
|
||||||
apiProcessedResponse.playerInfo.recentPlay = recentPlay;
|
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) {
|
if (externalProfileUrl) {
|
||||||
apiProcessedResponse.playerInfo.externalProfileUrl = externalProfileUrl;
|
apiProcessedResponse.playerInfo.externalProfileUrl = externalProfileUrl;
|
||||||
}
|
}
|
||||||
@ -23,7 +29,11 @@ const process = response => {
|
|||||||
return apiProcessedResponse;
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import process from './utils/process'
|
import process from "./utils/process";
|
||||||
import createClient from '../../generic'
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
import createClient from '../../generic'
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import process from './utils/process'
|
import process from "./utils/process";
|
||||||
import createClient from '../../generic'
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import api from './api-ranking-global'
|
import api from "./api-ranking-global";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
import createClient from '../../generic'
|
import createClient from "../../generic";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
const apiProcessedResponse = api.process(response);
|
const apiProcessedResponse = api.process(response);
|
||||||
|
|
||||||
if (!opt(response, 'players')) return null;
|
if (!opt(response, "players")) return null;
|
||||||
|
|
||||||
return apiProcessedResponse;
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
import {opt} from '../../../../../utils/js'
|
import { opt } from "../../../../../utils/js";
|
||||||
import queue from '../../../../queues/queues'
|
import queue from "../../../../queues/queues";
|
||||||
|
|
||||||
export default response => {
|
export default (response) => {
|
||||||
if (!opt(response, 'players')) return null;
|
if (!opt(response, "players")) return null;
|
||||||
|
|
||||||
if (!Array.isArray(response.players)) return null;
|
if (!Array.isArray(response.players)) return null;
|
||||||
|
|
||||||
return response.players.map(player => {
|
return response.players.map((player) => {
|
||||||
let {avatar, country, difference, history, playerId, playerName: name, pp, rank} = player;
|
let {
|
||||||
|
avatar,
|
||||||
|
country,
|
||||||
|
difference,
|
||||||
|
history,
|
||||||
|
playerId,
|
||||||
|
playerName: name,
|
||||||
|
pp,
|
||||||
|
rank,
|
||||||
|
} = player;
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
if (!avatar.startsWith('http'))
|
if (!avatar.startsWith("http"))
|
||||||
avatar = `${queue.SCORESABER_API.SS_API_HOST}${!avatar.startsWith('/') ? '/' : ''}${avatar}`;
|
avatar = `${queue.SCORESABER_API.SS_API_HOST}${
|
||||||
|
!avatar.startsWith("/") ? "/" : ""
|
||||||
|
}${avatar}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -22,13 +33,17 @@ export default response => {
|
|||||||
countries: [{ country, rank: null }],
|
countries: [{ country, rank: null }],
|
||||||
pp,
|
pp,
|
||||||
rank,
|
rank,
|
||||||
rankHistory: history && history.length
|
rankHistory:
|
||||||
? history.split(',').map(r => parseInt(r, 10)).filter(r => !isNaN(r))
|
history && history.length
|
||||||
|
? history
|
||||||
|
.split(",")
|
||||||
|
.map((r) => parseInt(r, 10))
|
||||||
|
.filter((r) => !isNaN(r))
|
||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
others: {
|
others: {
|
||||||
difference,
|
difference,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,9 +1,13 @@
|
|||||||
import createClient from '../../generic'
|
import createClient from "../../generic";
|
||||||
import queues from '../../../queues/queues'
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import process from './utils/process';
|
import process from "./utils/process";
|
||||||
import createClient from '../../generic'
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import queue from '../../../queues/queues'
|
import queue from "../../../queues/queues";
|
||||||
import createClient from '../../generic'
|
import createClient from "../../generic";
|
||||||
import process from './utils/process'
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import {dateFromString} from '../../../../../utils/date'
|
import { dateFromString } from "../../../../../utils/date";
|
||||||
import {extractDiffAndType} from '../../../../../utils/scoresaber/format'
|
import { extractDiffAndType } from "../../../../../utils/scoresaber/format";
|
||||||
import {opt} from '../../../../../utils/js'
|
import { opt } from "../../../../../utils/js";
|
||||||
|
|
||||||
export default response => {
|
export default (response) => {
|
||||||
if (!opt(response, 'scores') || !Array.isArray(response.scores) || !opt(response, 'scores.0.scoreId')) return [];
|
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 {
|
const {
|
||||||
songHash: hash,
|
songHash: hash,
|
||||||
songName: name,
|
songName: name,
|
||||||
@ -24,19 +29,38 @@ export default response => {
|
|||||||
|
|
||||||
let { unmodififiedScore: unmodifiedScore, mods, ...score } = originalScore;
|
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;
|
else if (!mods) mods = null;
|
||||||
|
|
||||||
const acc = unmodifiedScore && opt(score, 'maxScore') ? unmodifiedScore / score.maxScore * 100 : null;
|
const acc =
|
||||||
const percentage = opt(score, 'score') && opt(score, 'maxScore') ? score.score / score.maxScore * 100 : null;
|
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 {
|
return {
|
||||||
leaderboard,
|
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(),
|
fetchedAt: new Date(),
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
import {opt} from '../../../utils/js'
|
import { opt } from "../../../utils/js";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
if (!opt(response, 'data.0')) return null;
|
if (!opt(response, "data.0")) return null;
|
||||||
|
|
||||||
return { ...response.data[0], profileLastUpdated: new Date() };
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...client
|
...client,
|
||||||
}
|
};
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
import queue from '../../queues/queues'
|
import queue from "../../queues/queues";
|
||||||
import createClient from '../generic'
|
import createClient from "../generic";
|
||||||
|
|
||||||
const process = response => {
|
const process = (response) => {
|
||||||
if (!response || !response.data || !Array.isArray(response.data)) return null;
|
if (!response || !response.data || !Array.isArray(response.data)) return null;
|
||||||
|
|
||||||
return response.data;
|
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);
|
const client = createClient(get, process);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...client,
|
...client,
|
||||||
}
|
};
|
||||||
|
@ -1,45 +1,48 @@
|
|||||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||||
import createPlayerService from '../services/scoresaber/player'
|
import createPlayerService from "../services/scoresaber/player";
|
||||||
import log from '../utils/logger'
|
import log from "../utils/logger";
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
export default (dlManager) => {
|
export default (dlManager) => {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
log.debug(`Command processor already initialized.`, 'CmdProcessor');
|
log.debug(`Command processor already initialized.`, "CmdProcessor");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerService = createPlayerService();
|
const playerService = createPlayerService();
|
||||||
|
|
||||||
eventBus.on('data-imported', () => {
|
eventBus.on("data-imported", () => {
|
||||||
if (window) window.location.reload()
|
if (window) window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
eventBus.on('player-add-cmd', async ({playerId}) => {
|
eventBus.on("player-add-cmd", async ({ playerId }) => {
|
||||||
await dlManager.enqueuePlayer(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;
|
if (!playerId) return;
|
||||||
|
|
||||||
await playerService.remove(playerId, purgeScores);
|
await playerService.remove(playerId, purgeScores);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
eventBus.on('dl-manager-pause-cmd', () => {
|
eventBus.on("dl-manager-pause-cmd", () => {
|
||||||
log.debug('Pause Dl Manager', 'CmdProcessor');
|
log.debug("Pause Dl Manager", "CmdProcessor");
|
||||||
|
|
||||||
dlManager.pause();
|
dlManager.pause();
|
||||||
});
|
});
|
||||||
|
|
||||||
eventBus.on('dl-manager-unpause-cmd', () => {
|
eventBus.on("dl-manager-unpause-cmd", () => {
|
||||||
log.debug('Unpause Dl Manager', 'CmdProcessor');
|
log.debug("Unpause Dl Manager", "CmdProcessor");
|
||||||
|
|
||||||
dlManager.start();
|
dlManager.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
initialized = true;
|
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 eventBus from "../utils/broadcast-channel-pubsub";
|
||||||
import log from '../utils/logger'
|
import log from "../utils/logger";
|
||||||
import createQueue, {PRIORITY} from '../utils/queue'
|
import createQueue, { PRIORITY } from "../utils/queue";
|
||||||
import {configStore} from '../stores/config'
|
import { configStore } from "../stores/config";
|
||||||
import createRankedsStore from '../stores/scoresaber/rankeds'
|
import createRankedsStore from "../stores/scoresaber/rankeds";
|
||||||
import createPlayerService from '../services/scoresaber/player'
|
import createPlayerService from "../services/scoresaber/player";
|
||||||
import createScoresService from '../services/scoresaber/scores'
|
import createScoresService from "../services/scoresaber/scores";
|
||||||
import createBeatSaviorService from '../services/beatsavior'
|
import createBeatSaviorService from "../services/beatsavior";
|
||||||
import createAccSaberService from '../services/accsaber'
|
import createAccSaberService from "../services/accsaber";
|
||||||
import {PRIORITY as HTTP_QUEUE_PRIORITY} from './queues/http-queue'
|
import { PRIORITY as HTTP_QUEUE_PRIORITY } from "./queues/http-queue";
|
||||||
import {HOUR, MINUTE} from '../utils/date'
|
import { HOUR, MINUTE } from "../utils/date";
|
||||||
import {opt} from '../utils/js'
|
import { opt } from "../utils/js";
|
||||||
|
|
||||||
const INTERVAL_TICK = MINUTE;
|
const INTERVAL_TICK = MINUTE;
|
||||||
|
|
||||||
@ -22,110 +22,198 @@ let beatSaviorService = null;
|
|||||||
let accSaberService = null;
|
let accSaberService = null;
|
||||||
|
|
||||||
const TYPES = {
|
const TYPES = {
|
||||||
BEATSAVIOR: {name: 'BEATSAVIOR', priority: PRIORITY.LOW},
|
BEATSAVIOR: { name: "BEATSAVIOR", priority: PRIORITY.LOW },
|
||||||
RANKEDS: {name: 'RANKEDS', priority: PRIORITY.LOW},
|
RANKEDS: { name: "RANKEDS", priority: PRIORITY.LOW },
|
||||||
ACCSABER: {name: 'ACCSABER', priority: PRIORITY.NORMAL},
|
ACCSABER: { name: "ACCSABER", priority: PRIORITY.NORMAL },
|
||||||
PLAYER_SCORES: {name: 'PLAYER-SCORES', priority: PRIORITY.NORMAL},
|
PLAYER_SCORES: { name: "PLAYER-SCORES", priority: PRIORITY.NORMAL },
|
||||||
PLAYER_SCORES_UPDATE_QUEUE: {name: 'PLAYER_SCORES_UPDATE_QUEUE', priority: PRIORITY.LOWEST},
|
PLAYER_SCORES_UPDATE_QUEUE: {
|
||||||
ACTIVE_PLAYERS: {name: 'ACTIVE-PLAYERS', priority: PRIORITY.HIGH},
|
name: "PLAYER_SCORES_UPDATE_QUEUE",
|
||||||
MAIN_PLAYER: {name: 'MAIN-PLAYER', priority: PRIORITY.HIGHEST},
|
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)) {
|
if (!type || !type.name || !Number.isFinite(type.priority)) {
|
||||||
log.warn(`Unknown type enqueued.`, 'DlManager', type);
|
log.warn(`Unknown type enqueued.`, "DlManager", type);
|
||||||
|
|
||||||
return;
|
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 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) => {
|
const processThen = async (promise, then = null) => {
|
||||||
promise.then(result => {
|
promise.then((result) => {
|
||||||
if(then) log.debug('Processing then command...', 'DlManager');
|
if (then) log.debug("Processing then command...", "DlManager");
|
||||||
|
|
||||||
return then ? { result, thenResult: then() } : result;
|
return then ? { result, thenResult: then() } : result;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TYPES.MAIN_PLAYER:
|
case TYPES.MAIN_PLAYER:
|
||||||
if (mainPlayerId) {
|
if (mainPlayerId) {
|
||||||
log.debug(`Enqueue main player`, 'DlManager');
|
log.debug(`Enqueue main player`, "DlManager");
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
enqueue(queue, {...TYPES.ACTIVE_PLAYERS, priority: PRIORITY.HIGHEST}, force, {playerId: mainPlayerId}),
|
enqueue(
|
||||||
enqueue(queue, {...TYPES.PLAYER_SCORES, priority: PRIORITY.HIGHEST}, force, {playerId: mainPlayerId}),
|
queue,
|
||||||
|
{ ...TYPES.ACTIVE_PLAYERS, priority: PRIORITY.HIGHEST },
|
||||||
|
force,
|
||||||
|
{ playerId: mainPlayerId },
|
||||||
|
),
|
||||||
|
enqueue(
|
||||||
|
queue,
|
||||||
|
{ ...TYPES.PLAYER_SCORES, priority: PRIORITY.HIGHEST },
|
||||||
|
force,
|
||||||
|
{ playerId: mainPlayerId },
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TYPES.RANKEDS:
|
case TYPES.RANKEDS:
|
||||||
log.debug(`Enqueue rankeds`, 'DlManager');
|
log.debug(`Enqueue rankeds`, "DlManager");
|
||||||
|
|
||||||
if (!rankedsStore) rankedsStore = await createRankedsStore();
|
if (!rankedsStore) rankedsStore = await createRankedsStore();
|
||||||
|
|
||||||
processThen(queue.add(async () => rankedsStore.refresh(force, networkPriority), priority), then)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued rankeds processed.', 'DlManager'));
|
queue.add(
|
||||||
|
async () => rankedsStore.refresh(force, networkPriority),
|
||||||
|
priority,
|
||||||
|
),
|
||||||
|
then,
|
||||||
|
).then((_) => log.debug("Enqueued rankeds processed.", "DlManager"));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TYPES.ACTIVE_PLAYERS:
|
case TYPES.ACTIVE_PLAYERS:
|
||||||
log.debug(`Enqueue active players`, 'DlManager');
|
log.debug(`Enqueue active players`, "DlManager");
|
||||||
|
|
||||||
if (data && data.playerId) {
|
if (data && data.playerId) {
|
||||||
if (data.add)
|
if (data.add)
|
||||||
processThen(queue.add(async () => playerService.add(data.playerId, networkPriority), priority), then)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
|
queue.add(
|
||||||
|
async () => playerService.add(data.playerId, networkPriority),
|
||||||
|
priority,
|
||||||
|
),
|
||||||
|
then,
|
||||||
|
).then((_) =>
|
||||||
|
log.debug("Enqueued active players processed.", "DlManager"),
|
||||||
|
);
|
||||||
else
|
else
|
||||||
processThen(queue.add(async () => playerService.refresh(data.playerId, force, networkPriority), priority), then)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
|
queue.add(
|
||||||
|
async () =>
|
||||||
|
playerService.refresh(data.playerId, force, networkPriority),
|
||||||
|
priority,
|
||||||
|
),
|
||||||
|
then,
|
||||||
|
).then((_) =>
|
||||||
|
log.debug("Enqueued active players processed.", "DlManager"),
|
||||||
|
);
|
||||||
} else
|
} else
|
||||||
processThen(queue.add(async () => playerService.refreshAll(force, networkPriority), priority), then)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
|
queue.add(
|
||||||
|
async () => playerService.refreshAll(force, networkPriority),
|
||||||
|
priority,
|
||||||
|
),
|
||||||
|
then,
|
||||||
|
).then((_) =>
|
||||||
|
log.debug("Enqueued active players processed.", "DlManager"),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TYPES.PLAYER_SCORES:
|
case TYPES.PLAYER_SCORES:
|
||||||
log.debug(`Enqueue players scores`, 'DlManager');
|
log.debug(`Enqueue players scores`, "DlManager");
|
||||||
|
|
||||||
if (data && data.playerId)
|
if (data && data.playerId)
|
||||||
processThen(queue.add(async () => scoresService.refresh(data.playerId, force, networkPriority), priority), then)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued players scores processed.', 'DlManager'));
|
queue.add(
|
||||||
|
async () =>
|
||||||
|
scoresService.refresh(data.playerId, force, networkPriority),
|
||||||
|
priority,
|
||||||
|
),
|
||||||
|
then,
|
||||||
|
).then((_) =>
|
||||||
|
log.debug("Enqueued players scores processed.", "DlManager"),
|
||||||
|
);
|
||||||
else
|
else
|
||||||
processThen(queue.add(async () => scoresService.refreshAll(force, networkPriority), priority), then)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued players scores processed.', 'DlManager'));
|
queue.add(
|
||||||
|
async () => scoresService.refreshAll(force, networkPriority),
|
||||||
|
priority,
|
||||||
|
),
|
||||||
|
then,
|
||||||
|
).then((_) =>
|
||||||
|
log.debug("Enqueued players scores processed.", "DlManager"),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TYPES.BEATSAVIOR:
|
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)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued Beat Savior processed.', 'DlManager'));
|
queue.add(
|
||||||
|
async () => beatSaviorService.refreshAll(force, networkPriority),
|
||||||
|
priority,
|
||||||
|
),
|
||||||
|
then,
|
||||||
|
).then((_) => log.debug("Enqueued Beat Savior processed.", "DlManager"));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TYPES.PLAYER_SCORES_UPDATE_QUEUE:
|
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)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued player scores rank & pp updates processed.', 'DlManager'));
|
queue.add(
|
||||||
|
async () => scoresService.updateRankAndPpFromTheQueue(),
|
||||||
|
priority,
|
||||||
|
),
|
||||||
|
then,
|
||||||
|
).then((_) =>
|
||||||
|
log.debug(
|
||||||
|
"Enqueued player scores rank & pp updates processed.",
|
||||||
|
"DlManager",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TYPES.ACCSABER:
|
case TYPES.ACCSABER:
|
||||||
log.debug(`Enqueue AccSaber updates`, 'DlManager');
|
log.debug(`Enqueue AccSaber updates`, "DlManager");
|
||||||
|
|
||||||
processThen(queue.add(async () => accSaberService.refreshAll(), priority), then)
|
processThen(
|
||||||
.then(_ => log.debug('Enqueued AccSaber updates processed.', 'DlManager'));
|
queue.add(async () => accSaberService.refreshAll(), priority),
|
||||||
|
then,
|
||||||
|
).then((_) =>
|
||||||
|
log.debug("Enqueued AccSaber updates processed.", "DlManager"),
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const enqueueAllJobs = async queue => {
|
const enqueueAllJobs = async (queue) => {
|
||||||
log.debug(`Try to enqueue & process queue.`, 'DlManager');
|
log.debug(`Try to enqueue & process queue.`, "DlManager");
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
enqueue(queue, TYPES.MAIN_PLAYER),
|
enqueue(queue, TYPES.MAIN_PLAYER),
|
||||||
@ -137,18 +225,18 @@ const enqueueAllJobs = async queue => {
|
|||||||
|
|
||||||
// it should be at the end of the queue
|
// it should be at the end of the queue
|
||||||
enqueue(queue, TYPES.PLAYER_SCORES_UPDATE_QUEUE),
|
enqueue(queue, TYPES.PLAYER_SCORES_UPDATE_QUEUE),
|
||||||
])
|
]);
|
||||||
}
|
};
|
||||||
|
|
||||||
let intervalId;
|
let intervalId;
|
||||||
const startSyncing = async queue => {
|
const startSyncing = async (queue) => {
|
||||||
await enqueueAllJobs(queue);
|
await enqueueAllJobs(queue);
|
||||||
intervalId = setInterval(() => enqueueAllJobs(queue), INTERVAL_TICK);
|
intervalId = setInterval(() => enqueueAllJobs(queue), INTERVAL_TICK);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
log.debug(`Download manager already initialized.`, 'DlManager');
|
log.debug(`Download manager already initialized.`, "DlManager");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -161,49 +249,54 @@ export default async () => {
|
|||||||
|
|
||||||
mainPlayerId = configStore.getMainPlayerId();
|
mainPlayerId = configStore.getMainPlayerId();
|
||||||
|
|
||||||
configStore.subscribe(config => {
|
configStore.subscribe((config) => {
|
||||||
const newMainPlayerId = opt(config, 'users.main')
|
const newMainPlayerId = opt(config, "users.main");
|
||||||
if (mainPlayerId !== newMainPlayerId) {
|
if (mainPlayerId !== newMainPlayerId) {
|
||||||
mainPlayerId = newMainPlayerId;
|
mainPlayerId = newMainPlayerId;
|
||||||
|
|
||||||
log.debug(`Main player changed to ${mainPlayerId}`, 'DlManager')
|
log.debug(`Main player changed to ${mainPlayerId}`, "DlManager");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
playerService = createPlayerService();
|
playerService = createPlayerService();
|
||||||
scoresService = createScoresService();
|
scoresService = createScoresService();
|
||||||
beatSaviorService = createBeatSaviorService();
|
beatSaviorService = createBeatSaviorService();
|
||||||
accSaberService = createAccSaberService();
|
accSaberService = createAccSaberService();
|
||||||
|
|
||||||
eventBus.leaderStore.subscribe(async isLeader => {
|
eventBus.leaderStore.subscribe(async (isLeader) => {
|
||||||
if (isLeader) {
|
if (isLeader) {
|
||||||
queue.clear();
|
queue.clear();
|
||||||
queue.start();
|
queue.start();
|
||||||
|
|
||||||
const nodeId = eventBus.getNodeId();
|
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(
|
await enqueue(
|
||||||
queue, TYPES.ACTIVE_PLAYERS, true,
|
queue,
|
||||||
|
TYPES.ACTIVE_PLAYERS,
|
||||||
|
true,
|
||||||
{ playerId, add: true },
|
{ playerId, add: true },
|
||||||
async () => enqueue(queue, TYPES.PLAYER_SCORES, true, { playerId }),
|
async () => enqueue(queue, TYPES.PLAYER_SCORES, true, { playerId }),
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
log.debug('Pause Dl Manager', 'DlManager');
|
log.debug("Pause Dl Manager", "DlManager");
|
||||||
|
|
||||||
queue.clear();
|
queue.clear();
|
||||||
queue.pause();
|
queue.pause();
|
||||||
};
|
};
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
log.debug('Unpause Dl Manager', 'DlManager');
|
log.debug("Unpause Dl Manager", "DlManager");
|
||||||
|
|
||||||
queue.clear();
|
queue.clear();
|
||||||
queue.start();
|
queue.start();
|
||||||
@ -213,11 +306,11 @@ export default async () => {
|
|||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
log.info(`Download manager initialized`, 'DlManager');
|
log.info(`Download manager initialized`, "DlManager");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
pause,
|
pause,
|
||||||
enqueuePlayer
|
enqueuePlayer,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {SsrError} from '../others/errors'
|
import { SsrError } from "../others/errors";
|
||||||
import {delay} from '../utils/promise'
|
import { delay } from "../utils/promise";
|
||||||
import {parseRateLimitHeaders} from './utils'
|
import { parseRateLimitHeaders } from "./utils";
|
||||||
|
|
||||||
export class SsrNetworkError extends SsrError {
|
export class SsrNetworkError extends SsrError {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
@ -20,7 +20,7 @@ export class SsrNetworkError extends SsrError {
|
|||||||
|
|
||||||
export class SsrNetworkTimeoutError extends SsrNetworkError {
|
export class SsrNetworkTimeoutError extends SsrNetworkError {
|
||||||
constructor(timeout, message) {
|
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.name = "SsrNetworkTimeoutError";
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
@ -29,9 +29,14 @@ export class SsrNetworkTimeoutError extends SsrNetworkError {
|
|||||||
|
|
||||||
export class SsrHttpResponseError extends SsrNetworkError {
|
export class SsrHttpResponseError extends SsrNetworkError {
|
||||||
constructor(response, ...args) {
|
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;
|
this.response = response;
|
||||||
|
|
||||||
const { remaining, limit, resetAt } = parseRateLimitHeaders(response);
|
const { remaining, limit, resetAt } = parseRateLimitHeaders(response);
|
||||||
@ -50,7 +55,7 @@ export class SsrHttpClientError extends SsrHttpResponseError {
|
|||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
this.name = 'SsrHttpClientError';
|
this.name = "SsrHttpClientError";
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRetry() {
|
shouldRetry() {
|
||||||
@ -66,7 +71,7 @@ export class SsrHttpRateLimitError extends SsrHttpClientError {
|
|||||||
constructor(response, ...args) {
|
constructor(response, ...args) {
|
||||||
super(response, ...args);
|
super(response, ...args);
|
||||||
|
|
||||||
this.name = 'SsrHttpRateLimitError';
|
this.name = "SsrHttpRateLimitError";
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRetry() {
|
shouldRetry() {
|
||||||
@ -119,6 +124,6 @@ export class SsrHttpServerError extends SsrHttpResponseError {
|
|||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...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(
|
export async function fetchJson(
|
||||||
url,
|
url,
|
||||||
{ cacheTtl = null, maxAge = null, ...restOptions } = {}
|
{ cacheTtl = null, maxAge = null, ...restOptions } = {},
|
||||||
) {
|
) {
|
||||||
const options = getOptionsWithCacheKey(
|
const options = getOptionsWithCacheKey(
|
||||||
url,
|
url,
|
||||||
{ cacheTtl, maxAge, ...restOptions },
|
{ cacheTtl, maxAge, ...restOptions },
|
||||||
"json"
|
"json",
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -129,7 +129,7 @@ export async function fetchJson(
|
|||||||
body,
|
body,
|
||||||
},
|
},
|
||||||
fetchCacheKey,
|
fetchCacheKey,
|
||||||
fetchCacheTtl
|
fetchCacheTtl,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -141,12 +141,12 @@ export async function fetchJson(
|
|||||||
|
|
||||||
export async function fetchHtml(
|
export async function fetchHtml(
|
||||||
url,
|
url,
|
||||||
{ cacheTtl = null, maxAge = null, ...restOptions } = {}
|
{ cacheTtl = null, maxAge = null, ...restOptions } = {},
|
||||||
) {
|
) {
|
||||||
const options = getOptionsWithCacheKey(
|
const options = getOptionsWithCacheKey(
|
||||||
url,
|
url,
|
||||||
{ cacheTtl, maxAge, ...restOptions },
|
{ cacheTtl, maxAge, ...restOptions },
|
||||||
"json"
|
"json",
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -172,7 +172,7 @@ export async function fetchHtml(
|
|||||||
body: new DOMParser().parseFromString(body, "text/html"),
|
body: new DOMParser().parseFromString(body, "text/html"),
|
||||||
},
|
},
|
||||||
fetchCacheKey,
|
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";
|
import { substituteVars } from "../../../utils/format";
|
||||||
|
|
||||||
const ACCSABER_API_URL = 'https://api.accsaber.com';
|
const ACCSABER_API_URL = "https://api.accsaber.com";
|
||||||
const CATEGORIES_URL = ACCSABER_API_URL + '/categories';
|
const CATEGORIES_URL = ACCSABER_API_URL + "/categories";
|
||||||
const RANKING_URL = ACCSABER_API_URL + '/categories/${category}/standings';
|
const RANKING_URL = ACCSABER_API_URL + "/categories/${category}/standings";
|
||||||
const PLAYER_SCORES_URL = ACCSABER_API_URL + '/players/${playerId}/scores';
|
const PLAYER_SCORES_URL = ACCSABER_API_URL + "/players/${playerId}/scores";
|
||||||
const PLAYER_RANK_HISTORY = ACCSABER_API_URL + '/players/${playerId}/recent-rank-history'
|
const PLAYER_RANK_HISTORY =
|
||||||
const LEADERBOARD_URL = ACCSABER_API_URL + '/map-leaderboards/${leaderboardId}';
|
ACCSABER_API_URL + "/players/${playerId}/recent-rank-history";
|
||||||
const LEADERBOARD_INFO_URL = ACCSABER_API_URL + '/ranked-maps/${leaderboardId}';
|
const LEADERBOARD_URL = ACCSABER_API_URL + "/map-leaderboards/${leaderboardId}";
|
||||||
|
const LEADERBOARD_INFO_URL = ACCSABER_API_URL + "/ranked-maps/${leaderboardId}";
|
||||||
|
|
||||||
export default (options = {}) => {
|
export default (options = {}) => {
|
||||||
const queue = createQueue(options);
|
const queue = createQueue(options);
|
||||||
|
|
||||||
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
||||||
|
|
||||||
const categories = async (priority = PRIORITY.FG_LOW, options = {}) => fetchJson(CATEGORIES_URL, options, priority)
|
const categories = async (priority = PRIORITY.FG_LOW, options = {}) =>
|
||||||
const ranking = async (category = 'overall', page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(RANKING_URL, {category, page}), options, priority)
|
fetchJson(CATEGORIES_URL, options, priority);
|
||||||
const scores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(PLAYER_SCORES_URL, {playerId, page}), options, priority)
|
const ranking = async (
|
||||||
const playerRankHistory = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(PLAYER_RANK_HISTORY, {playerId}), options, priority)
|
category = "overall",
|
||||||
const leaderboard = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(LEADERBOARD_URL, {leaderboardId, page}), options, priority)
|
page = 1,
|
||||||
const leaderboardInfo = async (leaderboardId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(LEADERBOARD_INFO_URL, {leaderboardId}), options, priority)
|
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 {
|
return {
|
||||||
categories,
|
categories,
|
||||||
@ -29,5 +79,5 @@ export default (options = {}) => {
|
|||||||
leaderboard,
|
leaderboard,
|
||||||
leaderboardInfo,
|
leaderboardInfo,
|
||||||
...queueToReturn,
|
...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";
|
import { substituteVars } from "../../../utils/format";
|
||||||
|
|
||||||
const BEATMAPS_API_URL = 'https://api.beatsaver.com/';
|
const BEATMAPS_API_URL = "https://api.beatsaver.com/";
|
||||||
const SONG_BY_HASH_URL = BEATMAPS_API_URL + '/maps/hash/${hash}';
|
const SONG_BY_HASH_URL = BEATMAPS_API_URL + "/maps/hash/${hash}";
|
||||||
const SONG_BY_KEY_URL = BEATMAPS_API_URL + '/maps/id/${key}'
|
const SONG_BY_KEY_URL = BEATMAPS_API_URL + "/maps/id/${key}";
|
||||||
|
|
||||||
export default (options = {}) => {
|
export default (options = {}) => {
|
||||||
const queue = createQueue(options);
|
const queue = createQueue(options);
|
||||||
|
|
||||||
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
||||||
|
|
||||||
const byHash = async (hash, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SONG_BY_HASH_URL, {hash}), options, priority)
|
const byHash = async (hash, priority = PRIORITY.FG_LOW, options = {}) =>
|
||||||
const byKey = async (key, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SONG_BY_KEY_URL, {key}), options, priority)
|
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 {
|
return {
|
||||||
byHash,
|
byHash,
|
||||||
byKey,
|
byKey,
|
||||||
...queueToReturn,
|
...queueToReturn,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import {default as createQueue, PRIORITY as QUEUE_PRIORITY} from '../../utils/queue';
|
import {
|
||||||
import {SsrError, SsrTimeoutError} from '../../others/errors'
|
default as createQueue,
|
||||||
import {SsrHttpRateLimitError, SsrHttpResponseError, SsrNetworkError, SsrNetworkTimeoutError} from '../errors'
|
PRIORITY as QUEUE_PRIORITY,
|
||||||
import {fetchHtml, fetchJson} from '../fetch';
|
} from "../../utils/queue";
|
||||||
import makePendingPromisePool from '../../utils/pending-promises'
|
import { SsrError, SsrTimeoutError } from "../../others/errors";
|
||||||
import {AbortError} from '../../utils/promise'
|
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;
|
const DEFAULT_RETRIES = 2;
|
||||||
|
|
||||||
@ -13,24 +21,41 @@ export const PRIORITY = {
|
|||||||
BG_HIGH: QUEUE_PRIORITY.NORMAL,
|
BG_HIGH: QUEUE_PRIORITY.NORMAL,
|
||||||
BG_NORMAL: QUEUE_PRIORITY.LOW,
|
BG_NORMAL: QUEUE_PRIORITY.LOW,
|
||||||
BG_LOW: QUEUE_PRIORITY.LOWEST,
|
BG_LOW: QUEUE_PRIORITY.LOWEST,
|
||||||
}
|
};
|
||||||
|
|
||||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||||
|
|
||||||
export default (options = {}) => {
|
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 queue = createQueue(queueOptions);
|
||||||
|
|
||||||
const { add, emitter, ...queueToReturn } = queue;
|
const { add, emitter, ...queueToReturn } = queue;
|
||||||
|
|
||||||
let lastRateLimitError = null;
|
let lastRateLimitError = null;
|
||||||
let rateLimitTimerId = 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 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) {
|
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);
|
if (rateLimitTimerId) clearTimeout(rateLimitTimerId);
|
||||||
|
|
||||||
@ -38,13 +63,23 @@ export default (options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { remaining, limit, resetAt } = lastRateLimitError;
|
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);
|
if (rateLimitTimerId) clearTimeout(rateLimitTimerId);
|
||||||
rateLimitTimerId = setTimeout(rateLimitTicker, rateLimitTick);
|
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++) {
|
for (let i = 0; i <= retries; i++) {
|
||||||
try {
|
try {
|
||||||
return await add(async () => {
|
return await add(async () => {
|
||||||
@ -55,19 +90,18 @@ export default (options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fetchFunc(url, options)
|
return fetchFunc(url, options)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
currentRateLimit = { ...response.rateLimit, waiting: 0 };
|
currentRateLimit = { ...response.rateLimit, waiting: 0 };
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
if (err instanceof SsrTimeoutError) throw new SsrNetworkTimeoutError(err.timeout);
|
if (err instanceof SsrTimeoutError)
|
||||||
|
throw new SsrNetworkTimeoutError(err.timeout);
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
})
|
});
|
||||||
},
|
}, priority);
|
||||||
priority,
|
|
||||||
)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof SsrHttpResponseError) {
|
if (err instanceof SsrHttpResponseError) {
|
||||||
const { remaining, limit, resetAt } = err;
|
const { remaining, limit, resetAt } = err;
|
||||||
@ -79,7 +113,13 @@ export default (options = {}) => {
|
|||||||
if (!shouldRetry || i === retries) throw err;
|
if (!shouldRetry || i === retries) throw err;
|
||||||
|
|
||||||
if (err instanceof SsrHttpRateLimitError) {
|
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;
|
lastRateLimitError = err;
|
||||||
|
|
||||||
rateLimitTicker();
|
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 queuedFetchJson = async (url, options, priority = PRIORITY.FG_LOW) =>
|
||||||
const queuedFetchHtml = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchHtml, url, options, priority));
|
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;
|
const getRateLimit = () => currentRateLimit;
|
||||||
|
|
||||||
@ -108,5 +154,5 @@ export default (options = {}) => {
|
|||||||
fetchHtml: queuedFetchHtml,
|
fetchHtml: queuedFetchHtml,
|
||||||
getRateLimit,
|
getRateLimit,
|
||||||
...queueToReturn,
|
...queueToReturn,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import {writable} from 'svelte/store'
|
import { writable } from "svelte/store";
|
||||||
import {PRIORITY} from './http-queue'
|
import { PRIORITY } from "./http-queue";
|
||||||
import createScoreSaberApiQueue from './scoresaber/api-queue'
|
import createScoreSaberApiQueue from "./scoresaber/api-queue";
|
||||||
import createScoreSaberPageQueue from './scoresaber/page-queue'
|
import createScoreSaberPageQueue from "./scoresaber/page-queue";
|
||||||
import createBeatMapsApiQueue from './beatmaps/api-queue'
|
import createBeatMapsApiQueue from "./beatmaps/api-queue";
|
||||||
import createBeatSaviorApiQueue from './beatsavior/api-queue'
|
import createBeatSaviorApiQueue from "./beatsavior/api-queue";
|
||||||
import createTwitchApiQueue from './twitch/api-queue'
|
import createTwitchApiQueue from "./twitch/api-queue";
|
||||||
import createAccSaberApiQueue from './accsaber/api-queue'
|
import createAccSaberApiQueue from "./accsaber/api-queue";
|
||||||
|
|
||||||
export const getResponseBody = response => response ? response.body : null;
|
export const getResponseBody = (response) => (response ? response.body : null);
|
||||||
export const isResponseCached = response => !!(response && response.cached)
|
export const isResponseCached = (response) => !!(response && response.cached);
|
||||||
export const updateResponseBody = (response, body) => response ? {...response, body} : null;
|
export const updateResponseBody = (response, body) =>
|
||||||
|
response ? { ...response, body } : null;
|
||||||
|
|
||||||
const initQueue = queue => {
|
const initQueue = (queue) => {
|
||||||
let queueState = {
|
let queueState = {
|
||||||
size: 0,
|
size: 0,
|
||||||
pending: 0,
|
pending: 0,
|
||||||
@ -21,35 +22,78 @@ const initQueue = queue => {
|
|||||||
|
|
||||||
const { subscribe, set } = writable(queueState);
|
const { subscribe, set } = writable(queueState);
|
||||||
|
|
||||||
queue.on('change', ({size, pending}) => {
|
queue.on("change", ({ size, pending }) => {
|
||||||
const {rateLimit: {waiting}} = queueState;
|
const {
|
||||||
|
rateLimit: { waiting },
|
||||||
|
} = queueState;
|
||||||
const { remaining, limit, resetAt } = queue.getRateLimit();
|
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);
|
set(queueState);
|
||||||
});
|
});
|
||||||
queue.on('progress', ({progress, num, count}) => {
|
queue.on("progress", ({ progress, num, count }) => {
|
||||||
const {rateLimit: {waiting}} = queueState;
|
const {
|
||||||
|
rateLimit: { waiting },
|
||||||
|
} = queueState;
|
||||||
const { remaining, limit, resetAt } = queue.getRateLimit();
|
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);
|
set(queueState);
|
||||||
});
|
});
|
||||||
queue.on('waiting', ({waiting, remaining, limit, resetAt}) => {
|
queue.on("waiting", ({ waiting, remaining, limit, resetAt }) => {
|
||||||
queueState = {...queueState, rateLimit: {waiting, remaining, limit, resetAt}}
|
queueState = {
|
||||||
|
...queueState,
|
||||||
|
rateLimit: { waiting, remaining, limit, resetAt },
|
||||||
|
};
|
||||||
set(queueState);
|
set(queueState);
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
...queue,
|
...queue,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
SCORESABER_API: initQueue(createScoreSaberApiQueue({concurrency: 3, timeout: 95000})),
|
SCORESABER_API: initQueue(
|
||||||
SCORESABER_PAGE: initQueue(createScoreSaberPageQueue({concurrency: 3, timeout: 30000})),
|
createScoreSaberApiQueue({ concurrency: 3, timeout: 95000 }),
|
||||||
BEATMAPS: initQueue(createBeatMapsApiQueue({concurrency: 1, timeout: 10000, intervalCap: 10, interval: 1000})),
|
),
|
||||||
BEATSAVIOR: initQueue(createBeatSaviorApiQueue({concurrency: 1, timeout: 10000, intervalCap: 60, interval: 60000})),
|
SCORESABER_PAGE: initQueue(
|
||||||
TWITCH: initQueue(createTwitchApiQueue({concurrency: 8, timeout: 8000, intervalCap: 800, interval: 60000})),
|
createScoreSaberPageQueue({ concurrency: 3, timeout: 30000 }),
|
||||||
ACCSABER: initQueue(createAccSaberApiQueue({concurrency: 2, timeout: 10000})),
|
),
|
||||||
|
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,
|
PRIORITY,
|
||||||
}
|
};
|
||||||
|
@ -1,35 +1,79 @@
|
|||||||
import {default as createQueue, PRIORITY} from '../http-queue';
|
import { default as createQueue, PRIORITY } from "../http-queue";
|
||||||
import {substituteVars} from '../../../utils/format'
|
import { substituteVars } from "../../../utils/format";
|
||||||
import {PLAYER_SCORES_PER_PAGE, PLAYERS_PER_PAGE} from '../../../utils/scoresaber/consts'
|
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_URL = `${SS_API_HOST}/api`;
|
||||||
|
|
||||||
export const SS_API_PLAYER_INFO_URL = SS_API_URL + '/player/${playerId}/full';
|
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_RECENT_SCORES_URL =
|
||||||
export const SS_API_TOP_SCORES_URL = SS_API_URL + '/player/${playerId}/scores/top/${page}';
|
SS_API_URL + "/player/${playerId}/scores/recent/${page}";
|
||||||
export const SS_API_FIND_PLAYER_URL = SS_API_URL + '/players/by-name/${query}'
|
export const SS_API_TOP_SCORES_URL =
|
||||||
export const SS_API_RANKING_GLOBAL_URL = SS_API_URL + '/players/${page}'
|
SS_API_URL + "/player/${playerId}/scores/top/${page}";
|
||||||
export const SS_API_RANKING_GLOBAL_PAGES_URL = SS_API_URL + '/players/pages'
|
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 = {}) => {
|
export default (options = {}) => {
|
||||||
const queue = createQueue(options);
|
const queue = createQueue(options);
|
||||||
|
|
||||||
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
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 {
|
return {
|
||||||
player,
|
player,
|
||||||
@ -42,5 +86,5 @@ export default (options = {}) => {
|
|||||||
PLAYER_SCORES_PER_PAGE,
|
PLAYER_SCORES_PER_PAGE,
|
||||||
PLAYERS_PER_PAGE,
|
PLAYERS_PER_PAGE,
|
||||||
...queueToReturn,
|
...queueToReturn,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -24,7 +24,7 @@ export const parseSsInt = (text) => {
|
|||||||
export const parseSsFloat = (text) =>
|
export const parseSsFloat = (text) =>
|
||||||
text
|
text
|
||||||
? parseFloat(
|
? parseFloat(
|
||||||
getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, ""))
|
getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, "")),
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@ -78,32 +78,32 @@ export default (options = {}) => {
|
|||||||
const rankeds = async (
|
const rankeds = async (
|
||||||
page = 1,
|
page = 1,
|
||||||
priority = PRIORITY.BG_NORMAL,
|
priority = PRIORITY.BG_NORMAL,
|
||||||
options = {}
|
options = {},
|
||||||
) =>
|
) =>
|
||||||
fetchJson(substituteVars(RANKEDS_URL, { page }), options, priority).then(
|
fetchJson(substituteVars(RANKEDS_URL, { page }), options, priority).then(
|
||||||
(r) => {
|
(r) => {
|
||||||
r.body = processRankeds(r.body);
|
r.body = processRankeds(r.body);
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const processPlayerProfile = (playerId, doc) => {
|
const processPlayerProfile = (playerId, doc) => {
|
||||||
cfDecryptEmail(doc);
|
cfDecryptEmail(doc);
|
||||||
|
|
||||||
let avatar = getImgUrl(
|
let avatar = getImgUrl(
|
||||||
opt(doc.querySelector(".column.avatar img"), "src", null)
|
opt(doc.querySelector(".column.avatar img"), "src", null),
|
||||||
);
|
);
|
||||||
|
|
||||||
let playerName = opt(
|
let playerName = opt(
|
||||||
doc.querySelector(".content .column:not(.avatar) .title a"),
|
doc.querySelector(".content .column:not(.avatar) .title a"),
|
||||||
"innerText"
|
"innerText",
|
||||||
);
|
);
|
||||||
playerName = playerName ? playerName.trim() : null;
|
playerName = playerName ? playerName.trim() : null;
|
||||||
|
|
||||||
let country = getFirstRegexpMatch(
|
let country = getFirstRegexpMatch(
|
||||||
/^.*?\/flags\/([^.]+)\..*$/,
|
/^.*?\/flags\/([^.]+)\..*$/,
|
||||||
opt(doc.querySelector(".content .column .title img"), "src")
|
opt(doc.querySelector(".content .column .title img"), "src"),
|
||||||
);
|
);
|
||||||
country = country ? country.toUpperCase() : null;
|
country = country ? country.toUpperCase() : null;
|
||||||
|
|
||||||
@ -111,8 +111,8 @@ export default (options = {}) => {
|
|||||||
opt(
|
opt(
|
||||||
doc.querySelector(".pagination .pagination-list li a.is-current"),
|
doc.querySelector(".pagination .pagination-list li a.is-current"),
|
||||||
"innerText",
|
"innerText",
|
||||||
null
|
null,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
pageNum = !isNaN(pageNum) ? pageNum : null;
|
pageNum = !isNaN(pageNum) ? pageNum : null;
|
||||||
|
|
||||||
@ -120,8 +120,8 @@ export default (options = {}) => {
|
|||||||
opt(
|
opt(
|
||||||
doc.querySelector(".pagination .pagination-list li:last-of-type"),
|
doc.querySelector(".pagination .pagination-list li:last-of-type"),
|
||||||
"innerText",
|
"innerText",
|
||||||
null
|
null,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
pageQty = !isNaN(pageQty) ? pageQty : null;
|
pageQty = !isNaN(pageQty) ? pageQty : null;
|
||||||
|
|
||||||
@ -130,31 +130,31 @@ export default (options = {}) => {
|
|||||||
/^\s*<strong>(?:[^:]+)\s*:?\s*<\/strong>\s*(.*)$/,
|
/^\s*<strong>(?:[^:]+)\s*:?\s*<\/strong>\s*(.*)$/,
|
||||||
opt(
|
opt(
|
||||||
doc.querySelector(
|
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;
|
totalItems = !isNaN(totalItems) ? totalItems : 0;
|
||||||
|
|
||||||
let playerRank = parseSsInt(
|
let playerRank = parseSsInt(
|
||||||
opt(
|
opt(
|
||||||
doc.querySelector(
|
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;
|
playerRank = !isNaN(playerRank) ? playerRank : null;
|
||||||
|
|
||||||
let countryRank = parseSsInt(
|
let countryRank = parseSsInt(
|
||||||
opt(
|
opt(
|
||||||
doc.querySelector(
|
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;
|
countryRank = !isNaN(countryRank) ? countryRank : null;
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ export default (options = {}) => {
|
|||||||
[...doc.querySelectorAll(".content .column ul li")]
|
[...doc.querySelectorAll(".content .column ul li")]
|
||||||
.map((li) => {
|
.map((li) => {
|
||||||
const matches = li.innerHTML.match(
|
const matches = li.innerHTML.match(
|
||||||
/^\s*<strong>([^:]+)\s*:?\s*<\/strong>\s*(.*)$/
|
/^\s*<strong>([^:]+)\s*:?\s*<\/strong>\s*(.*)$/,
|
||||||
);
|
);
|
||||||
if (!matches) return null;
|
if (!matches) return null;
|
||||||
|
|
||||||
@ -219,7 +219,7 @@ export default (options = {}) => {
|
|||||||
const item = mapping.find((m) => m.key === matches[1]);
|
const item = mapping.find((m) => m.key === matches[1]);
|
||||||
return item ? { ...item, value } : { label: matches[1], value };
|
return item ? { ...item, value } : { label: matches[1], value };
|
||||||
})
|
})
|
||||||
.filter((s) => s)
|
.filter((s) => s),
|
||||||
)
|
)
|
||||||
.reduce(
|
.reduce(
|
||||||
(cum, item) => {
|
(cum, item) => {
|
||||||
@ -255,7 +255,7 @@ export default (options = {}) => {
|
|||||||
|
|
||||||
return cum;
|
return cum;
|
||||||
},
|
},
|
||||||
{ inactiveAccount: false, bannedAccount: false }
|
{ inactiveAccount: false, bannedAccount: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const scores = [...doc.querySelectorAll("table.ranking tbody tr")].map(
|
const scores = [...doc.querySelectorAll("table.ranking tbody tr")].map(
|
||||||
@ -274,7 +274,7 @@ export default (options = {}) => {
|
|||||||
if (song) {
|
if (song) {
|
||||||
const leaderboardId = parseInt(
|
const leaderboardId = parseInt(
|
||||||
getFirstRegexpMatch(/leaderboard\/(\d+)/, song.href),
|
getFirstRegexpMatch(/leaderboard\/(\d+)/, song.href),
|
||||||
10
|
10,
|
||||||
);
|
);
|
||||||
ret.leaderboardId = leaderboardId ? leaderboardId : null;
|
ret.leaderboardId = leaderboardId ? leaderboardId : null;
|
||||||
} else {
|
} else {
|
||||||
@ -293,7 +293,7 @@ export default (options = {}) => {
|
|||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
.replace(
|
.replace(
|
||||||
/<span class="__cf_email__" data-cfemail="[^"]+">\[email protected]<\/span>/g,
|
/<span class="__cf_email__" data-cfemail="[^"]+">\[email protected]<\/span>/g,
|
||||||
""
|
"",
|
||||||
)
|
)
|
||||||
.match(/^(.*?)\s*<span[^>]+>(.*?)<\/span>/)
|
.match(/^(.*?)\s*<span[^>]+>(.*?)<\/span>/)
|
||||||
: null;
|
: null;
|
||||||
@ -328,7 +328,7 @@ export default (options = {}) => {
|
|||||||
ret.timeSet = songDate ? dateFromString(songDate.title) : null;
|
ret.timeSet = songDate ? dateFromString(songDate.title) : null;
|
||||||
|
|
||||||
const pp = parseSsFloat(
|
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;
|
ret.pp = !isNaN(pp) ? pp : null;
|
||||||
|
|
||||||
@ -337,9 +337,9 @@ export default (options = {}) => {
|
|||||||
/^\(([0-9.]+)pp\)$/,
|
/^\(([0-9.]+)pp\)$/,
|
||||||
opt(
|
opt(
|
||||||
tr.querySelector("th.score .scoreTop.ppWeightedValue"),
|
tr.querySelector("th.score .scoreTop.ppWeightedValue"),
|
||||||
"innerText"
|
"innerText",
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
ret.ppWeighted = !isNaN(ppWeighted) ? ppWeighted : null;
|
ret.ppWeighted = !isNaN(ppWeighted) ? ppWeighted : null;
|
||||||
|
|
||||||
@ -380,7 +380,7 @@ export default (options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const recentPlay =
|
const recentPlay =
|
||||||
scores && scores.length && scores[0].timeSet ? scores[0].timeSet : null;
|
scores && scores.length && scores[0].timeSet ? scores[0].timeSet : null;
|
||||||
@ -394,18 +394,18 @@ export default (options = {}) => {
|
|||||||
externalProfileUrl: opt(
|
externalProfileUrl: opt(
|
||||||
doc.querySelector(".content .column:not(.avatar) .title a"),
|
doc.querySelector(".content .column:not(.avatar) .title a"),
|
||||||
"href",
|
"href",
|
||||||
null
|
null,
|
||||||
),
|
),
|
||||||
history: getFirstRegexpMatch(
|
history: getFirstRegexpMatch(
|
||||||
/data:\s*\[([0-9,]+)\]/,
|
/data:\s*\[([0-9,]+)\]/,
|
||||||
doc.body.innerHTML
|
doc.body.innerHTML,
|
||||||
),
|
),
|
||||||
country,
|
country,
|
||||||
badges: [...doc.querySelectorAll(".column.avatar center img")].map(
|
badges: [...doc.querySelectorAll(".column.avatar center img")].map(
|
||||||
(img) => ({
|
(img) => ({
|
||||||
image: getImgUrl(img.src),
|
image: getImgUrl(img.src),
|
||||||
description: img.title,
|
description: img.title,
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
rank: stats.rank ? stats.rank : null,
|
rank: stats.rank ? stats.rank : null,
|
||||||
countryRank: stats.countryRank ? stats.countryRank : null,
|
countryRank: stats.countryRank ? stats.countryRank : null,
|
||||||
@ -435,7 +435,7 @@ export default (options = {}) => {
|
|||||||
fetchHtml(
|
fetchHtml(
|
||||||
substituteVars(PLAYER_PROFILE_URL, { playerId }),
|
substituteVars(PLAYER_PROFILE_URL, { playerId }),
|
||||||
options,
|
options,
|
||||||
priority
|
priority,
|
||||||
).then((r) => {
|
).then((r) => {
|
||||||
r.body = processPlayerProfile(playerId, r.body);
|
r.body = processPlayerProfile(playerId, r.body);
|
||||||
|
|
||||||
@ -451,17 +451,17 @@ export default (options = {}) => {
|
|||||||
const id = getFirstRegexpMatch(/\/(\d+)$/, a.href);
|
const id = getFirstRegexpMatch(/\/(\d+)$/, a.href);
|
||||||
|
|
||||||
const avatar = getImgUrl(
|
const avatar = getImgUrl(
|
||||||
opt(tr.querySelector("td.picture img"), "src", null)
|
opt(tr.querySelector("td.picture img"), "src", null),
|
||||||
);
|
);
|
||||||
|
|
||||||
let country = getFirstRegexpMatch(
|
let country = getFirstRegexpMatch(
|
||||||
/^.*?\/flags\/([^.]+)\..*$/,
|
/^.*?\/flags\/([^.]+)\..*$/,
|
||||||
opt(tr.querySelector("td.player img"), "src", null)
|
opt(tr.querySelector("td.player img"), "src", null),
|
||||||
);
|
);
|
||||||
country = country ? country.toUpperCase() : null;
|
country = country ? country.toUpperCase() : null;
|
||||||
|
|
||||||
let difference = parseSsInt(
|
let difference = parseSsInt(
|
||||||
opt(tr.querySelector("td.diff"), "innerText", null)
|
opt(tr.querySelector("td.diff"), "innerText", null),
|
||||||
);
|
);
|
||||||
difference = !isNaN(difference) ? difference : null;
|
difference = !isNaN(difference) ? difference : null;
|
||||||
|
|
||||||
@ -469,15 +469,15 @@ export default (options = {}) => {
|
|||||||
playerName = playerName || playerName === "" ? playerName.trim() : null;
|
playerName = playerName || playerName === "" ? playerName.trim() : null;
|
||||||
|
|
||||||
let pp = parseSsFloat(
|
let pp = parseSsFloat(
|
||||||
opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText")
|
opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText"),
|
||||||
);
|
);
|
||||||
pp = !isNaN(pp) ? pp : null;
|
pp = !isNaN(pp) ? pp : null;
|
||||||
|
|
||||||
let rank = parseSsInt(
|
let rank = parseSsInt(
|
||||||
getFirstRegexpMatch(
|
getFirstRegexpMatch(
|
||||||
/^\s*#(\d+)\s*$/,
|
/^\s*#(\d+)\s*$/,
|
||||||
opt(tr.querySelector("td.rank"), "innerText", null)
|
opt(tr.querySelector("td.rank"), "innerText", null),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
rank = !isNaN(rank) ? rank : null;
|
rank = !isNaN(rank) ? rank : null;
|
||||||
|
|
||||||
@ -491,7 +491,7 @@ export default (options = {}) => {
|
|||||||
pp,
|
pp,
|
||||||
rank,
|
rank,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return { players: data };
|
return { players: data };
|
||||||
@ -501,12 +501,12 @@ export default (options = {}) => {
|
|||||||
country,
|
country,
|
||||||
page = 1,
|
page = 1,
|
||||||
priority = PRIORITY.FG_LOW,
|
priority = PRIORITY.FG_LOW,
|
||||||
options = {}
|
options = {},
|
||||||
) =>
|
) =>
|
||||||
fetchHtml(
|
fetchHtml(
|
||||||
substituteVars(COUNTRY_RANKING_URL, { country, page }),
|
substituteVars(COUNTRY_RANKING_URL, { country, page }),
|
||||||
options,
|
options,
|
||||||
priority
|
priority,
|
||||||
).then((r) => {
|
).then((r) => {
|
||||||
r.body = processCountryRanking(country, r.body);
|
r.body = processCountryRanking(country, r.body);
|
||||||
|
|
||||||
@ -529,11 +529,11 @@ export default (options = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ret.player.playerInfo.avatar = getImgUrl(
|
ret.player.playerInfo.avatar = getImgUrl(
|
||||||
opt(tr.querySelector(".picture img"), "src", null)
|
opt(tr.querySelector(".picture img"), "src", null),
|
||||||
);
|
);
|
||||||
|
|
||||||
ret.score.rank = parseSsInt(
|
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;
|
if (isNaN(ret.score.rank)) ret.score.rank = null;
|
||||||
|
|
||||||
@ -541,7 +541,7 @@ export default (options = {}) => {
|
|||||||
if (player) {
|
if (player) {
|
||||||
let country = getFirstRegexpMatch(
|
let country = getFirstRegexpMatch(
|
||||||
/^.*?\/flags\/([^.]+)\..*$/,
|
/^.*?\/flags\/([^.]+)\..*$/,
|
||||||
opt(player.querySelector("img"), "src", "")
|
opt(player.querySelector("img"), "src", ""),
|
||||||
);
|
);
|
||||||
country = country ? country.toUpperCase() : null;
|
country = country ? country.toUpperCase() : null;
|
||||||
if (country) {
|
if (country) {
|
||||||
@ -551,14 +551,14 @@ export default (options = {}) => {
|
|||||||
|
|
||||||
ret.player.name = opt(
|
ret.player.name = opt(
|
||||||
player.querySelector("span.songTop.pp"),
|
player.querySelector("span.songTop.pp"),
|
||||||
"innerText"
|
"innerText",
|
||||||
);
|
);
|
||||||
ret.player.name = ret.player.name
|
ret.player.name = ret.player.name
|
||||||
? ret.player.name.trim().replace("'", "'")
|
? ret.player.name.trim().replace("'", "'")
|
||||||
: null;
|
: null;
|
||||||
ret.player.playerId = getFirstRegexpMatch(
|
ret.player.playerId = getFirstRegexpMatch(
|
||||||
/\/u\/(\d+)((\?|&|#).*)?$/,
|
/\/u\/(\d+)((\?|&|#).*)?$/,
|
||||||
opt(player, "href", "")
|
opt(player, "href", ""),
|
||||||
);
|
);
|
||||||
ret.player.playerId = ret.player.playerId
|
ret.player.playerId = ret.player.playerId
|
||||||
? ret.player.playerId.trim()
|
? ret.player.playerId.trim()
|
||||||
@ -574,7 +574,7 @@ export default (options = {}) => {
|
|||||||
ret.score.timeSetString = opt(
|
ret.score.timeSetString = opt(
|
||||||
tr.querySelector("td.timeset"),
|
tr.querySelector("td.timeset"),
|
||||||
"innerText",
|
"innerText",
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
if (ret.score.timeSetString)
|
if (ret.score.timeSetString)
|
||||||
ret.score.timeSetString = ret.score.timeSetString.trim();
|
ret.score.timeSetString = ret.score.timeSetString.trim();
|
||||||
@ -602,7 +602,7 @@ export default (options = {}) => {
|
|||||||
const diffs = [...doc.querySelectorAll(".tabs ul li a")].map((a) => {
|
const diffs = [...doc.querySelectorAll(".tabs ul li a")].map((a) => {
|
||||||
let leaderboardId = parseInt(
|
let leaderboardId = parseInt(
|
||||||
getFirstRegexpMatch(/leaderboard\/(\d+)$/, a.href),
|
getFirstRegexpMatch(/leaderboard\/(\d+)$/, a.href),
|
||||||
10
|
10,
|
||||||
);
|
);
|
||||||
if (isNaN(leaderboardId)) leaderboardId = null;
|
if (isNaN(leaderboardId)) leaderboardId = null;
|
||||||
|
|
||||||
@ -615,7 +615,7 @@ export default (options = {}) => {
|
|||||||
const currentDiffHuman = opt(
|
const currentDiffHuman = opt(
|
||||||
doc.querySelector(".tabs li.is-active a span"),
|
doc.querySelector(".tabs li.is-active a span"),
|
||||||
"innerText",
|
"innerText",
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
let diff = null;
|
let diff = null;
|
||||||
@ -628,20 +628,20 @@ export default (options = {}) => {
|
|||||||
|
|
||||||
const songName = opt(
|
const songName = opt(
|
||||||
doc.querySelector(
|
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",
|
"innerText",
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageUrl = getImgUrl(
|
const imageUrl = getImgUrl(
|
||||||
opt(
|
opt(
|
||||||
doc.querySelector(
|
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",
|
"src",
|
||||||
null
|
null,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const songInfo = [
|
const songInfo = [
|
||||||
@ -656,13 +656,13 @@ export default (options = {}) => {
|
|||||||
]
|
]
|
||||||
.map((sid) => {
|
.map((sid) => {
|
||||||
let songInfoBox = doc.querySelector(
|
let songInfoBox = doc.querySelector(
|
||||||
".column.is-one-third-desktop .box:first-of-type"
|
".column.is-one-third-desktop .box:first-of-type",
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...sid,
|
...sid,
|
||||||
value: songInfoBox
|
value: songInfoBox
|
||||||
? songInfoBox.innerHTML.match(
|
? songInfoBox.innerHTML.match(
|
||||||
new RegExp(sid.label + ":\\s*<b>(.*?)</b>", "i")
|
new RegExp(sid.label + ":\\s*<b>(.*?)</b>", "i"),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
@ -708,7 +708,7 @@ export default (options = {}) => {
|
|||||||
|
|
||||||
return cum;
|
return cum;
|
||||||
},
|
},
|
||||||
{ imageUrl, stats: {} }
|
{ imageUrl, stats: {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { stats, ...song } = songInfo;
|
const { stats, ...song } = songInfo;
|
||||||
@ -718,9 +718,9 @@ export default (options = {}) => {
|
|||||||
opt(
|
opt(
|
||||||
doc.querySelector(".pagination .pagination-list li:last-of-type"),
|
doc.querySelector(".pagination .pagination-list li:last-of-type"),
|
||||||
"innerText",
|
"innerText",
|
||||||
null
|
null,
|
||||||
),
|
),
|
||||||
10
|
10,
|
||||||
);
|
);
|
||||||
if (isNaN(pageQty)) pageQty = null;
|
if (isNaN(pageQty)) pageQty = null;
|
||||||
|
|
||||||
@ -736,7 +736,7 @@ export default (options = {}) => {
|
|||||||
|
|
||||||
let diffChartText = getFirstRegexpMatch(
|
let diffChartText = getFirstRegexpMatch(
|
||||||
/'difficulty',\s*([0-9.,\s]+)\s*\]/,
|
/'difficulty',\s*([0-9.,\s]+)\s*\]/,
|
||||||
doc.body.innerHTML
|
doc.body.innerHTML,
|
||||||
);
|
);
|
||||||
let diffChart = (diffChartText ? diffChartText : "")
|
let diffChart = (diffChartText ? diffChartText : "")
|
||||||
.split(",")
|
.split(",")
|
||||||
@ -758,12 +758,12 @@ export default (options = {}) => {
|
|||||||
leaderboardId,
|
leaderboardId,
|
||||||
page = 1,
|
page = 1,
|
||||||
priority = PRIORITY.FG_LOW,
|
priority = PRIORITY.FG_LOW,
|
||||||
options = {}
|
options = {},
|
||||||
) =>
|
) =>
|
||||||
fetchHtml(
|
fetchHtml(
|
||||||
substituteVars(LEADERBOARD_URL, { leaderboardId, page }),
|
substituteVars(LEADERBOARD_URL, { leaderboardId, page }),
|
||||||
options,
|
options,
|
||||||
priority
|
priority,
|
||||||
).then((r) => {
|
).then((r) => {
|
||||||
r.body = processLeaderboard(leaderboardId, page, r.body);
|
r.body = processLeaderboard(leaderboardId, page, r.body);
|
||||||
|
|
||||||
|
@ -1,44 +1,104 @@
|
|||||||
import {default as createQueue, PRIORITY} from '../http-queue';
|
import { default as createQueue, PRIORITY } from "../http-queue";
|
||||||
import ssrConfig from '../../../ssr-config'
|
import ssrConfig from "../../../ssr-config";
|
||||||
import { substituteVars } from "../../../utils/format";
|
import { substituteVars } from "../../../utils/format";
|
||||||
|
|
||||||
const CLIENT_ID = 'u0swxz56n4iumc634at1osoqdk31qt';
|
const CLIENT_ID = "u0swxz56n4iumc634at1osoqdk31qt";
|
||||||
|
|
||||||
const TWITCH_AUTH_URL = 'https://id.twitch.tv/oauth2'
|
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 AUTHORIZATION_URL =
|
||||||
const VALIDATE_URL = `${TWITCH_AUTH_URL}/validate`
|
`${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 TWITCH_API_URL = "https://api.twitch.tv/helix";
|
||||||
const PROFILE_URL = TWITCH_API_URL + '/users?login=${login}';
|
const PROFILE_URL = TWITCH_API_URL + "/users?login=${login}";
|
||||||
const VIDEOS_URL = TWITCH_API_URL + '/videos?user_id=${userId}&type=${type}&first=100';
|
const VIDEOS_URL =
|
||||||
const STREAMS_URL = TWITCH_API_URL + '/streams?user_id=${userId}';
|
TWITCH_API_URL + "/videos?user_id=${userId}&type=${type}&first=100";
|
||||||
|
const STREAMS_URL = TWITCH_API_URL + "/streams?user_id=${userId}";
|
||||||
|
|
||||||
export default (options = {}) => {
|
export default (options = {}) => {
|
||||||
const queue = createQueue(options);
|
const queue = createQueue(options);
|
||||||
|
|
||||||
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
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,
|
url,
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
'Client-ID': CLIENT_ID,
|
"Client-ID": CLIENT_ID,
|
||||||
'Authorization': `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
priority,
|
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 {
|
return {
|
||||||
getAuthUrl,
|
getAuthUrl,
|
||||||
@ -47,5 +107,5 @@ export default (options = {}) => {
|
|||||||
videos,
|
videos,
|
||||||
streams,
|
streams,
|
||||||
...queueToReturn,
|
...queueToReturn,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
export const parseRateLimitHeaders = response => {
|
export const parseRateLimitHeaders = (response) => {
|
||||||
if (!response || !response.headers) return null;
|
if (!response || !response.headers) return null;
|
||||||
|
|
||||||
const remaining = parseInt(response.headers.get('x-ratelimit-remaining'), 10);
|
const remaining = parseInt(response.headers.get("x-ratelimit-remaining"), 10);
|
||||||
const limit = parseInt(response.headers.get('x-ratelimit-limit'), 10);
|
const limit = parseInt(response.headers.get("x-ratelimit-limit"), 10);
|
||||||
const resetAt = parseInt(response.headers.get('x-ratelimit-reset'), 10);
|
const resetAt = parseInt(response.headers.get("x-ratelimit-reset"), 10);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
remaining: !isNaN(remaining) ? remaining : null,
|
remaining: !isNaN(remaining) ? remaining : null,
|
||||||
limit: !isNaN(limit) ? limit : null,
|
limit: !isNaN(limit) ? limit : null,
|
||||||
resetAt: !isNaN(resetAt) ? new Date(resetAt * 1000) : null,
|
resetAt: !isNaN(resetAt) ? new Date(resetAt * 1000) : null,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -12,7 +12,7 @@ export class SsrError extends Error {
|
|||||||
|
|
||||||
export class SsrTimeoutError extends SsrError {
|
export class SsrTimeoutError extends SsrError {
|
||||||
constructor(timeout, message) {
|
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.name = "SsrTimeoutError";
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
@ -21,7 +21,7 @@ export class SsrTimeoutError extends SsrError {
|
|||||||
|
|
||||||
export class SsrDataFormatError extends SsrError {
|
export class SsrDataFormatError extends SsrError {
|
||||||
constructor(message, previous = null) {
|
constructor(message, previous = null) {
|
||||||
super(message && message.length ? message : `Data format error`)
|
super(message && message.length ? message : `Data format error`);
|
||||||
|
|
||||||
this.name = "SsrDataFormatError";
|
this.name = "SsrDataFormatError";
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
|
@ -1,35 +1,36 @@
|
|||||||
import {db} from '../db/db'
|
import { db } from "../db/db";
|
||||||
import queues from '../network/queues/queues';
|
import queues from "../network/queues/queues";
|
||||||
import accSaberCategoriesApiClient from '../network/clients/accsaber/api-categories';
|
import accSaberCategoriesApiClient from "../network/clients/accsaber/api-categories";
|
||||||
import accSaberRankingApiClient from '../network/clients/accsaber/api-ranking';
|
import accSaberRankingApiClient from "../network/clients/accsaber/api-ranking";
|
||||||
import accSaberScoresApiClient from '../network/clients/accsaber/api-scores';
|
import accSaberScoresApiClient from "../network/clients/accsaber/api-scores";
|
||||||
import accSaberPlayerRankHistoryApiClient from '../network/clients/accsaber/api-player-rank-history';
|
import accSaberPlayerRankHistoryApiClient from "../network/clients/accsaber/api-player-rank-history";
|
||||||
import accSaberCategoriesRepository from '../db/repository/accsaber-categories'
|
import accSaberCategoriesRepository from "../db/repository/accsaber-categories";
|
||||||
import accSaberPlayersRepository from '../db/repository/accsaber-players'
|
import accSaberPlayersRepository from "../db/repository/accsaber-players";
|
||||||
import accSaberPlayersHistoryRepository from '../db/repository/accsaber-players-history';
|
import accSaberPlayersHistoryRepository from "../db/repository/accsaber-players-history";
|
||||||
import keyValueRepository from '../db/repository/key-value'
|
import keyValueRepository from "../db/repository/key-value";
|
||||||
import createPlayerService from '../services/scoresaber/player';
|
import createPlayerService from "../services/scoresaber/player";
|
||||||
import {capitalize, convertArrayToObjectByKey} from '../utils/js'
|
import { capitalize, convertArrayToObjectByKey } from "../utils/js";
|
||||||
import log from '../utils/logger'
|
import log from "../utils/logger";
|
||||||
import {
|
import {
|
||||||
addToDate,
|
addToDate,
|
||||||
toAccSaberMidnight,
|
toAccSaberMidnight,
|
||||||
formatDate,
|
formatDate,
|
||||||
HOUR,
|
HOUR,
|
||||||
MINUTE,
|
MINUTE,
|
||||||
dateFromString, truncateDate,
|
dateFromString,
|
||||||
} from '../utils/date'
|
truncateDate,
|
||||||
import {PRIORITY} from '../network/queues/http-queue'
|
} from "../utils/date";
|
||||||
import makePendingPromisePool from '../utils/pending-promises'
|
import { PRIORITY } from "../network/queues/http-queue";
|
||||||
import {getServicePlayerGain, serviceFilterFunc} from './utils'
|
import makePendingPromisePool from "../utils/pending-promises";
|
||||||
import {PLAYER_SCORES_PER_PAGE} from '../utils/accsaber/consts'
|
import { getServicePlayerGain, serviceFilterFunc } from "./utils";
|
||||||
import {roundToPrecision} from '../utils/format'
|
import { PLAYER_SCORES_PER_PAGE } from "../utils/accsaber/consts";
|
||||||
|
import { roundToPrecision } from "../utils/format";
|
||||||
|
|
||||||
const REFRESH_INTERVAL = HOUR;
|
const REFRESH_INTERVAL = HOUR;
|
||||||
const SCORES_NETWORK_TTL = MINUTE * 5;
|
const SCORES_NETWORK_TTL = MINUTE * 5;
|
||||||
const HISTOGRAM_AP_PRECISION = 5;
|
const HISTOGRAM_AP_PRECISION = 5;
|
||||||
|
|
||||||
const CATEGORIES_ORDER = ['overall', 'true', 'standard', 'tech'];
|
const CATEGORIES_ORDER = ["overall", "true", "standard", "tech"];
|
||||||
|
|
||||||
let service = null;
|
let service = null;
|
||||||
export default () => {
|
export default () => {
|
||||||
@ -40,61 +41,120 @@ export default () => {
|
|||||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||||
|
|
||||||
const getCategories = async () => {
|
const getCategories = async () => {
|
||||||
const categories = await resolvePromiseOrWaitForPending(`accSaberCategories`, () => accSaberCategoriesRepository().getAll());
|
const categories = await resolvePromiseOrWaitForPending(
|
||||||
|
`accSaberCategories`,
|
||||||
|
() => accSaberCategoriesRepository().getAll(),
|
||||||
|
);
|
||||||
|
|
||||||
const getIdx = category => {
|
const getIdx = (category) => {
|
||||||
const idx = CATEGORIES_ORDER.findIndex(v => v === category?.name);
|
const idx = CATEGORIES_ORDER.findIndex((v) => v === category?.name);
|
||||||
|
|
||||||
return idx >= 0 ? idx : 100000;
|
return idx >= 0 ? idx : 100000;
|
||||||
}
|
};
|
||||||
return categories.sort((a, b) => getIdx(a) - getIdx(b));
|
return categories.sort((a, b) => getIdx(a) - getIdx(b));
|
||||||
}
|
};
|
||||||
|
|
||||||
const getPlayer = async playerId => resolvePromiseOrWaitForPending(`accSaberPlayer/${playerId}`, () => accSaberPlayersRepository().getAllFromIndex('accsaber-players-playerId', playerId));
|
const getPlayer = async (playerId) =>
|
||||||
const getRanking = async (category = 'overall') => accSaberPlayersRepository().getAllFromIndex('accsaber-players-category', category);
|
resolvePromiseOrWaitForPending(`accSaberPlayer/${playerId}`, () =>
|
||||||
const getPlayerHistory = async playerId => resolvePromiseOrWaitForPending(`accSaberPlayerHistory/${playerId}`, () => accSaberPlayersHistoryRepository().getAllFromIndex('accsaber-players-history-playerId', 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 getLastUpdatedKey = (type) => `accSaber${capitalize(type)}LastUpdated`;
|
||||||
const getLastUpdated = async (type = 'all') => keyValueRepository().get(getLastUpdatedKey(type));
|
const getLastUpdated = async (type = "all") =>
|
||||||
const setLastUpdated = async (type = 'all', date) => keyValueRepository().set(date, getLastUpdatedKey(type));
|
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) {
|
if (!forceUpdate) {
|
||||||
const lastUpdated = await getLastUpdated(type);
|
const lastUpdated = await getLastUpdated(type);
|
||||||
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
|
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 false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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) 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})))
|
return (
|
||||||
.map(s => ({
|
await resolvePromiseOrWaitForPending(
|
||||||
|
`fetchPlayerScores/${playerId}/${page}`,
|
||||||
|
() =>
|
||||||
|
accSaberScoresApiClient.getProcessed({
|
||||||
|
...options,
|
||||||
|
playerId,
|
||||||
|
page,
|
||||||
|
priority,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
leaderboard: {
|
leaderboard: {
|
||||||
...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 getScoresHistogramDefinition = (
|
||||||
const scoreType = serviceParams?.type ?? 'overall';
|
serviceParams = { type: "overall", sort: "ap", order: "desc" },
|
||||||
const sort = serviceParams?.sort ?? 'ap';
|
) => {
|
||||||
const order = serviceParams?.order ?? 'desc';
|
const scoreType = serviceParams?.type ?? "overall";
|
||||||
|
const sort = serviceParams?.sort ?? "ap";
|
||||||
|
const order = serviceParams?.order ?? "desc";
|
||||||
|
|
||||||
const commonFilterFunc = serviceFilterFunc(serviceParams);
|
const commonFilterFunc = serviceFilterFunc(serviceParams);
|
||||||
|
|
||||||
@ -104,68 +164,75 @@ export default () => {
|
|||||||
let maxBucketSize = null;
|
let maxBucketSize = null;
|
||||||
let bucketSizeStep = null;
|
let bucketSizeStep = null;
|
||||||
let bucketSizeValues = null;
|
let bucketSizeValues = null;
|
||||||
let type = 'linear';
|
let type = "linear";
|
||||||
let valFunc = s => s;
|
let valFunc = (s) => s;
|
||||||
let filterFunc = s => commonFilterFunc(s) && (scoreType === 'overall' || s?.leaderboard?.category === scoreType);
|
let filterFunc = (s) =>
|
||||||
let histogramFilterFunc = s => s;
|
commonFilterFunc(s) &&
|
||||||
let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear'
|
(scoreType === "overall" || s?.leaderboard?.category === scoreType);
|
||||||
|
let histogramFilterFunc = (s) => s;
|
||||||
|
let roundedValFunc = (s, type = type, precision = bucketSize) =>
|
||||||
|
type === "linear"
|
||||||
? roundToPrecision(valFunc(s), precision)
|
? roundToPrecision(valFunc(s), precision)
|
||||||
: truncateDate(valFunc(s), precision);
|
: truncateDate(valFunc(s), precision);
|
||||||
let prefix = '';
|
let prefix = "";
|
||||||
let prefixLong = '';
|
let prefixLong = "";
|
||||||
let suffix = '';
|
let suffix = "";
|
||||||
let suffixLong = '';
|
let suffixLong = "";
|
||||||
|
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'ap':
|
case "ap":
|
||||||
valFunc = s => s?.ap;
|
valFunc = (s) => s?.ap;
|
||||||
type = 'linear';
|
type = "linear";
|
||||||
bucketSize = HISTOGRAM_AP_PRECISION;
|
bucketSize = HISTOGRAM_AP_PRECISION;
|
||||||
minBucketSize = 1;
|
minBucketSize = 1;
|
||||||
maxBucketSize = 100;
|
maxBucketSize = 100;
|
||||||
bucketSizeStep = 1;
|
bucketSizeStep = 1;
|
||||||
round = 0;
|
round = 0;
|
||||||
suffix = ' AP';
|
suffix = " AP";
|
||||||
suffixLong = ' AP';
|
suffixLong = " AP";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'recent':
|
case "recent":
|
||||||
valFunc = s => s?.timeSet;
|
valFunc = (s) => s?.timeSet;
|
||||||
type = 'time';
|
type = "time";
|
||||||
bucketSize = 'day'
|
bucketSize = "day";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'acc':
|
case "acc":
|
||||||
valFunc = s => s?.acc;
|
valFunc = (s) => s?.acc;
|
||||||
type = 'linear';
|
type = "linear";
|
||||||
bucketSize = 0.05;
|
bucketSize = 0.05;
|
||||||
minBucketSize = 0.05;
|
minBucketSize = 0.05;
|
||||||
maxBucketSize = 1;
|
maxBucketSize = 1;
|
||||||
bucketSizeStep = 0.05;
|
bucketSizeStep = 0.05;
|
||||||
round = 2;
|
round = 2;
|
||||||
suffix = '%';
|
suffix = "%";
|
||||||
suffixLong = '%';
|
suffixLong = "%";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'rank':
|
case "rank":
|
||||||
valFunc = s => s?.score?.rank;
|
valFunc = (s) => s?.score?.rank;
|
||||||
type = 'linear';
|
type = "linear";
|
||||||
bucketSize = 5;
|
bucketSize = 5;
|
||||||
minBucketSize = 1;
|
minBucketSize = 1;
|
||||||
maxBucketSize = 100;
|
maxBucketSize = 100;
|
||||||
bucketSizeStep = 1;
|
bucketSizeStep = 1;
|
||||||
round = 0;
|
round = 0;
|
||||||
prefix = '';
|
prefix = "";
|
||||||
prefixLong = '#';
|
prefixLong = "#";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getValue: valFunc,
|
getValue: valFunc,
|
||||||
getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize),
|
getRoundedValue:
|
||||||
|
(bucketSize = bucketSize) =>
|
||||||
|
(s) =>
|
||||||
|
roundedValFunc(s, type, bucketSize),
|
||||||
filter: filterFunc,
|
filter: filterFunc,
|
||||||
histogramFilter: histogramFilterFunc,
|
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,
|
type,
|
||||||
bucketSize,
|
bucketSize,
|
||||||
minBucketSize,
|
minBucketSize,
|
||||||
@ -177,36 +244,38 @@ export default () => {
|
|||||||
prefixLong,
|
prefixLong,
|
||||||
suffix,
|
suffix,
|
||||||
suffixLong,
|
suffixLong,
|
||||||
order
|
order,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const getPlayerScores = async playerId => {
|
const getPlayerScores = async (playerId) => {
|
||||||
try {
|
try {
|
||||||
return fetchScoresPage(playerId, 1);
|
return fetchScoresPage(playerId, 1);
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
|
||||||
return [];
|
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;
|
let page = serviceParams?.page ?? 1;
|
||||||
if (page < 1) page = 1;
|
if (page < 1) page = 1;
|
||||||
|
|
||||||
let playerScores;
|
let playerScores;
|
||||||
try {
|
try {
|
||||||
playerScores = await fetchScoresPage(playerId, page);
|
playerScores = await fetchScoresPage(playerId, page);
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
|
||||||
return { total: 0, scores: [] };
|
return { total: 0, scores: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!playerScores?.length) 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;
|
const startIdx = (page - 1) * PLAYER_SCORES_PER_PAGE;
|
||||||
if (playerScores.length < startIdx + 1) return { total: 0, scores: [] };
|
if (playerScores.length < startIdx + 1) return { total: 0, scores: [] };
|
||||||
@ -214,127 +283,185 @@ export default () => {
|
|||||||
return {
|
return {
|
||||||
total: playerScores.length,
|
total: playerScores.length,
|
||||||
itemsPerPage: PLAYER_SCORES_PER_PAGE,
|
itemsPerPage: PLAYER_SCORES_PER_PAGE,
|
||||||
scores: playerScores
|
scores: playerScores.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE),
|
||||||
.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) 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) => {
|
const refreshCategories = async (
|
||||||
log.debug(`Starting AccSaber categories refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
|
forceUpdate = false,
|
||||||
|
priority = queues.PRIORITY.BG_NORMAL,
|
||||||
|
throwErrors = false,
|
||||||
|
) => {
|
||||||
|
log.debug(
|
||||||
|
`Starting AccSaber categories refreshing${
|
||||||
|
forceUpdate ? " (forced)" : ""
|
||||||
|
}...`,
|
||||||
|
"AccSaberService",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.trace(`Fetching categories from DB...`, 'AccSaberService');
|
log.trace(`Fetching categories from DB...`, "AccSaberService");
|
||||||
|
|
||||||
const dbCategories = await getCategories();
|
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) {
|
if (!categories || !categories.length) {
|
||||||
log.warn(`AccSaber returned empty categories list`, 'AccSaberService')
|
log.warn(`AccSaber returned empty categories list`, "AccSaberService");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
categories = categories.concat([{
|
categories = categories.concat([
|
||||||
name: 'overall',
|
{
|
||||||
displayName: 'Overall',
|
name: "overall",
|
||||||
|
displayName: "Overall",
|
||||||
countsTowardsOverall: null,
|
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 dbCategoriesNames = dbCategories.map((c) => c.name);
|
||||||
const newCategories = categories.filter(c => !dbCategories || !dbCategoriesNames.includes(c.name));
|
const newCategories = categories.filter(
|
||||||
|
(c) => !dbCategories || !dbCategoriesNames.includes(c.name),
|
||||||
|
);
|
||||||
|
|
||||||
if (newCategories && newCategories.length)
|
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 => {
|
await db.runInTransaction(
|
||||||
const newCategoriesNames = categories.map(c => c.name);
|
["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();
|
let cursor = await accSaberCategoriesStore.openCursor();
|
||||||
|
|
||||||
log.trace(`Remove old categories from DB`, 'AccSaberService');
|
log.trace(`Remove old categories from DB`, "AccSaberService");
|
||||||
|
|
||||||
while (cursor) {
|
while (cursor) {
|
||||||
const category = cursor.value;
|
const category = cursor.value;
|
||||||
if (!newCategoriesNames.includes(category.name)) await cursor.delete();
|
if (!newCategoriesNames.includes(category.name))
|
||||||
|
await cursor.delete();
|
||||||
|
|
||||||
cursor = await cursor.continue();
|
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);
|
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 };
|
return { changed: newCategories, all: categories };
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
if (throwErrors) throw e;
|
if (throwErrors) throw e;
|
||||||
|
|
||||||
log.debug(`Categories refreshing error`, 'AccSaberService', e)
|
log.debug(`Categories refreshing error`, "AccSaberService", e);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const updatePlayerHistory = async player => {
|
const updatePlayerHistory = async (player) => {
|
||||||
if (!player?.playerId) return;
|
if (!player?.playerId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.debug(`Updating player ${player.playerId} history`, 'AccSaberService');
|
log.debug(
|
||||||
|
`Updating player ${player.playerId} history`,
|
||||||
|
"AccSaberService",
|
||||||
|
);
|
||||||
|
|
||||||
const accSaberDate = toAccSaberMidnight(new Date());
|
const accSaberDate = toAccSaberMidnight(new Date());
|
||||||
const playerIdTimestamp = `${player.playerId}_${accSaberDate.getTime()}`;
|
const playerIdTimestamp = `${player.playerId}_${accSaberDate.getTime()}`;
|
||||||
|
|
||||||
const existingData = await accSaberPlayersHistoryRepository().get(playerIdTimestamp);
|
const existingData =
|
||||||
|
await accSaberPlayersHistoryRepository().get(playerIdTimestamp);
|
||||||
const lastUpdated = dateFromString(existingData?.lastUpdated);
|
const lastUpdated = dateFromString(existingData?.lastUpdated);
|
||||||
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const categories = (await getCategories())?.map(c => c.name) ?? null;
|
const categories = (await getCategories())?.map((c) => c.name) ?? null;
|
||||||
if (!categories) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let accStats = {};
|
let accStats = {};
|
||||||
for (const category of categories) {
|
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;
|
if (!playerAccInfo) continue;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -358,109 +485,175 @@ export default () => {
|
|||||||
accSaberDate,
|
accSaberDate,
|
||||||
lastUpdated: new Date(),
|
lastUpdated: new Date(),
|
||||||
playerIdTimestamp,
|
playerIdTimestamp,
|
||||||
categories: accStats
|
categories: accStats,
|
||||||
}
|
};
|
||||||
|
|
||||||
await accSaberPlayersHistoryRepository().set(stats);
|
await accSaberPlayersHistoryRepository().set(stats);
|
||||||
} else {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(`Player ${player.playerId} history updated`, 'AccSaberService');
|
log.debug(`Player ${player.playerId} history updated`, "AccSaberService");
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
log.debug(
|
||||||
log.debug(`Player ${player.playerId} history updating error.`, 'AccSaberService', e);
|
`Player ${player.playerId} history updating error.`,
|
||||||
}
|
"AccSaberService",
|
||||||
|
e,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshRanking = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
|
const refreshRanking = async (
|
||||||
log.debug(`Starting AccSaber ${category} ranking refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
|
category = "overall",
|
||||||
|
forceUpdate = false,
|
||||||
|
priority = queues.PRIORITY.BG_NORMAL,
|
||||||
|
throwErrors = false,
|
||||||
|
) => {
|
||||||
|
log.debug(
|
||||||
|
`Starting AccSaber ${category} ranking refreshing${
|
||||||
|
forceUpdate ? " (forced)" : ""
|
||||||
|
}...`,
|
||||||
|
"AccSaberService",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.trace(`Fetching ${category} ranking from DB...`, 'AccSaberService');
|
log.trace(`Fetching ${category} ranking from DB...`, "AccSaberService");
|
||||||
|
|
||||||
const dbRanking = await getRanking(category);
|
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) {
|
if (!ranking || !ranking.length) {
|
||||||
log.warn(`AccSaber returned empty ${category} ranking`, 'AccSaberService')
|
log.warn(
|
||||||
|
`AccSaber returned empty ${category} ranking`,
|
||||||
|
"AccSaberService",
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
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 => {
|
await db.runInTransaction(
|
||||||
const newPlayerIds = ranking.map(c => c.playerId);
|
["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();
|
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) {
|
while (cursor) {
|
||||||
const player = cursor.value;
|
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();
|
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);
|
accSaberPlayersRepository().addToCache(ranking);
|
||||||
keyValueRepository().setCache(getLastUpdatedKey(rankingType), new Date());
|
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);
|
return ranking.sort((a, b) => a.rank - b.rank);
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
if (throwErrors) throw 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const refreshAll = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
|
const refreshAll = async (
|
||||||
log.trace(`Starting AccSaber all data refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
|
category = "overall",
|
||||||
|
forceUpdate = false,
|
||||||
|
priority = queues.PRIORITY.BG_NORMAL,
|
||||||
|
throwErrors = false,
|
||||||
|
) => {
|
||||||
|
log.trace(
|
||||||
|
`Starting AccSaber all data refreshing${
|
||||||
|
forceUpdate ? " (forced)" : ""
|
||||||
|
}...`,
|
||||||
|
"AccSaberService",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dbCategories = await refreshCategories();
|
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(
|
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) => {
|
const rankings = allRankings.reduce((cum, ranking) => {
|
||||||
if (!ranking || !ranking.length) return cum;
|
if (!ranking || !ranking.length) return cum;
|
||||||
@ -470,21 +663,28 @@ export default () => {
|
|||||||
return cum;
|
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) {
|
} catch (e) {
|
||||||
if (throwErrors) throw e;
|
if (throwErrors) throw e;
|
||||||
|
|
||||||
log.debug(`All data refreshing error`, 'AccSaberService', e)
|
log.debug(`All data refreshing error`, "AccSaberService", e);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const destroyService = () => {
|
const destroyService = () => {
|
||||||
service = null;
|
service = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
isDataForPlayerAvailable,
|
isDataForPlayerAvailable,
|
||||||
@ -502,7 +702,7 @@ export default () => {
|
|||||||
refreshRanking,
|
refreshRanking,
|
||||||
refreshAll,
|
refreshAll,
|
||||||
destroyService,
|
destroyService,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
};
|
||||||
|
@ -1,28 +1,29 @@
|
|||||||
import hashApiClient from '../network/clients/beatmaps/api-hash';
|
import hashApiClient from "../network/clients/beatmaps/api-hash";
|
||||||
import keyApiClient from '../network/clients/beatmaps/api-key';
|
import keyApiClient from "../network/clients/beatmaps/api-key";
|
||||||
import {PRIORITY} from '../network/queues/http-queue';
|
import { PRIORITY } from "../network/queues/http-queue";
|
||||||
import log from '../utils/logger'
|
import log from "../utils/logger";
|
||||||
import {SsrHttpNotFoundError, SsrNetworkError} from '../network/errors'
|
import { SsrHttpNotFoundError, SsrNetworkError } from "../network/errors";
|
||||||
import songsBeatMapsRepository from "../db/repository/songs-beatmaps";
|
import songsBeatMapsRepository from "../db/repository/songs-beatmaps";
|
||||||
import cacheRepository from "../db/repository/cache";
|
import cacheRepository from "../db/repository/cache";
|
||||||
import {addToDate, dateFromString, HOUR} from '../utils/date'
|
import { addToDate, dateFromString, HOUR } from "../utils/date";
|
||||||
import {capitalize, opt} from '../utils/js'
|
import { capitalize, opt } from "../utils/js";
|
||||||
|
|
||||||
const BM_SUSPENSION_KEY = 'bmSuspension';
|
const BM_SUSPENSION_KEY = "bmSuspension";
|
||||||
const BM_NOT_FOUND_KEY = 'bm404';
|
const BM_NOT_FOUND_KEY = "bm404";
|
||||||
const BM_NOT_FOUND_HOURS_BETWEEN_COUNTS = 1;
|
const BM_NOT_FOUND_HOURS_BETWEEN_COUNTS = 1;
|
||||||
|
|
||||||
const INVALID_NOTES_COUNT_FIXES = {
|
const INVALID_NOTES_COUNT_FIXES = {
|
||||||
'e738b38b594861745bfb0473c66ca5cca15072ff': [
|
e738b38b594861745bfb0473c66ca5cca15072ff: [
|
||||||
{type: 'Standard', diff: "ExpertPlus", notes: 942}
|
{ type: "Standard", diff: "ExpertPlus", notes: 942 },
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const cacheSongInfo = async (songInfo, originalHash) => {
|
const cacheSongInfo = async (songInfo, originalHash) => {
|
||||||
if (!songInfo) return null;
|
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;
|
if (!hash || !songInfo.key) return null;
|
||||||
|
|
||||||
@ -34,30 +35,46 @@ export default () => {
|
|||||||
await songsBeatMapsRepository().set(songInfo);
|
await songsBeatMapsRepository().set(songInfo);
|
||||||
|
|
||||||
return songInfo;
|
return songInfo;
|
||||||
}
|
};
|
||||||
|
|
||||||
const isSuspended = bsSuspension => !!bsSuspension && bsSuspension.activeTo > new Date() && bsSuspension.started > addToDate(-24 * HOUR);
|
const isSuspended = (bsSuspension) =>
|
||||||
const getCurrentSuspension = async () => cacheRepository().get(BM_SUSPENSION_KEY);
|
!!bsSuspension &&
|
||||||
const prolongSuspension = async 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 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++;
|
suspension.count++;
|
||||||
|
|
||||||
return await cacheRepository().set(suspension, BM_SUSPENSION_KEY);
|
return await cacheRepository().set(suspension, BM_SUSPENSION_KEY);
|
||||||
}
|
};
|
||||||
|
|
||||||
const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY);
|
const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY);
|
||||||
const set404Hashes = async hashes => cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
|
const set404Hashes = async (hashes) =>
|
||||||
const setHashNotFound = async hash => {
|
cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
|
||||||
|
const setHashNotFound = async (hash) => {
|
||||||
let songs404 = await get404Hashes();
|
let songs404 = await get404Hashes();
|
||||||
if (!songs404) songs404 = {};
|
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.recentTry = new Date();
|
||||||
item.count++;
|
item.count++;
|
||||||
|
|
||||||
@ -65,31 +82,40 @@ export default () => {
|
|||||||
|
|
||||||
await set404Hashes(songs404);
|
await set404Hashes(songs404);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
const isHashUnavailable = async hash => {
|
const isHashUnavailable = async (hash) => {
|
||||||
const songs404 = await get404Hashes();
|
const songs404 = await get404Hashes();
|
||||||
return songs404 && songs404[hash] && songs404[hash].count >= 3;
|
return songs404 && songs404[hash] && songs404[hash].count >= 3;
|
||||||
}
|
};
|
||||||
|
|
||||||
const fixInvalidNotesCount = (hash, songInfo) => {
|
const fixInvalidNotesCount = (hash, songInfo) => {
|
||||||
if (!hash) return songInfo;
|
if (!hash) return songInfo;
|
||||||
|
|
||||||
if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions)
|
if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions)
|
||||||
songInfo.versions.forEach(si => {
|
songInfo.versions.forEach((si) => {
|
||||||
if (!si?.diffs) return;
|
if (!si?.diffs) return;
|
||||||
|
|
||||||
si.diffs.forEach(d => {
|
si.diffs.forEach((d) => {
|
||||||
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(f => f.type === d?.characteristic && f.diff === d?.difficulty);
|
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(
|
||||||
|
(f) => f.type === d?.characteristic && f.diff === d?.difficulty,
|
||||||
|
);
|
||||||
if (!newNotesCnt) return;
|
if (!newNotesCnt) return;
|
||||||
|
|
||||||
d.notes = newNotesCnt.notes;
|
d.notes = newNotesCnt.notes;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
return songInfo;
|
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 (!forceUpdate && songInfo) return fixInvalidNotesCount(hash, songInfo);
|
||||||
|
|
||||||
if (cacheOnly) return null;
|
if (cacheOnly) return null;
|
||||||
@ -97,7 +123,11 @@ export default () => {
|
|||||||
let bsSuspension = await getCurrentSuspension();
|
let bsSuspension = await getCurrentSuspension();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isSuspended(bsSuspension) || (hash && await isHashUnavailable(hash))) return null;
|
if (
|
||||||
|
isSuspended(bsSuspension) ||
|
||||||
|
(hash && (await isHashUnavailable(hash)))
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
const songInfo = await fetchFunc();
|
const songInfo = await fetchFunc();
|
||||||
if (!songInfo) {
|
if (!songInfo) {
|
||||||
@ -111,36 +141,78 @@ export default () => {
|
|||||||
await setHashNotFound(hash);
|
await setHashNotFound(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof SsrNetworkError && err.message === 'Network error') {
|
if (err instanceof SsrNetworkError && err.message === "Network error") {
|
||||||
try {await prolongSuspension(bsSuspension)} catch {}
|
try {
|
||||||
|
await prolongSuspension(bsSuspension);
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn(`Error fetching BeatSaver song "${errSongId}"`);
|
log.warn(`Error fetching BeatSaver song "${errSongId}"`);
|
||||||
|
|
||||||
return null;
|
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();
|
hash = hash.toLowerCase();
|
||||||
|
|
||||||
const songInfo = await songsBeatMapsRepository().get(hash);
|
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();
|
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 => {
|
const convertOldBeatSaverToBeatMaps = (song) => {
|
||||||
let {key, hash, name, metadata: {characteristics}} = 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();
|
if (hash.toLowerCase) hash = hash.toLowerCase();
|
||||||
|
|
||||||
@ -148,25 +220,25 @@ export default () => {
|
|||||||
if (!ch.name || !ch.difficulties) return diffs;
|
if (!ch.name || !ch.difficulties) return diffs;
|
||||||
const characteristic = ch.name;
|
const characteristic = ch.name;
|
||||||
|
|
||||||
return diffs.concat(
|
return diffs
|
||||||
Object.entries(ch.difficulties)
|
.concat(
|
||||||
.map(([difficulty, obj]) => {
|
Object.entries(ch.difficulties).map(([difficulty, obj]) => {
|
||||||
if (!obj) return null;
|
if (!obj) return null;
|
||||||
difficulty = capitalize(difficulty);
|
difficulty = capitalize(difficulty);
|
||||||
|
|
||||||
const seconds = opt(obj, 'length', null);
|
const seconds = opt(obj, "length", null);
|
||||||
const notes = opt(obj, 'notes', null)
|
const notes = opt(obj, "notes", null);
|
||||||
|
|
||||||
const nps = notes && seconds ? notes / seconds : null;
|
const nps = notes && seconds ? notes / seconds : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
njs: opt(obj, 'njs', null),
|
njs: opt(obj, "njs", null),
|
||||||
offset: opt(obj, 'njsOffset', null),
|
offset: opt(obj, "njsOffset", null),
|
||||||
notes,
|
notes,
|
||||||
bombs: opt(obj, 'bombs', null),
|
bombs: opt(obj, "bombs", null),
|
||||||
obstacles: opt(obj, 'obstacles', null),
|
obstacles: opt(obj, "obstacles", null),
|
||||||
nps,
|
nps,
|
||||||
length: opt(obj, 'duration', null),
|
length: opt(obj, "duration", null),
|
||||||
characteristic,
|
characteristic,
|
||||||
difficulty,
|
difficulty,
|
||||||
events: null,
|
events: null,
|
||||||
@ -182,41 +254,42 @@ export default () => {
|
|||||||
},
|
},
|
||||||
stars: null,
|
stars: null,
|
||||||
};
|
};
|
||||||
}))
|
}),
|
||||||
.filter(diff => diff)
|
)
|
||||||
|
.filter((diff) => diff);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastUpdated: dateFromString(opt(song, 'uploaded', new Date())),
|
lastUpdated: dateFromString(opt(song, "uploaded", new Date())),
|
||||||
oldBeatSaverId: opt(song, '_id', null),
|
oldBeatSaverId: opt(song, "_id", null),
|
||||||
id: key,
|
id: key,
|
||||||
hash,
|
hash,
|
||||||
key,
|
key,
|
||||||
name,
|
name,
|
||||||
description: '',
|
description: "",
|
||||||
uploader: {
|
uploader: {
|
||||||
id: null,
|
id: null,
|
||||||
name: opt(song, 'uploader.username', null),
|
name: opt(song, "uploader.username", null),
|
||||||
hash: null,
|
hash: null,
|
||||||
avatar: null
|
avatar: null,
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
bpm: opt(song, 'metadata.bpm', null),
|
bpm: opt(song, "metadata.bpm", null),
|
||||||
duration: opt(song, 'metadata.duration', null),
|
duration: opt(song, "metadata.duration", null),
|
||||||
songName: opt(song, 'metadata.songName', ''),
|
songName: opt(song, "metadata.songName", ""),
|
||||||
songSubName: opt(song, 'metadata.songSubName', ''),
|
songSubName: opt(song, "metadata.songSubName", ""),
|
||||||
songAuthorName: opt(song, 'metadata.songAuthorName', ''),
|
songAuthorName: opt(song, "metadata.songAuthorName", ""),
|
||||||
levelAuthorName: opt(song, 'metadata.levelAuthorName', '')
|
levelAuthorName: opt(song, "metadata.levelAuthorName", ""),
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
plays: opt(song, 'stats.plays', 0),
|
plays: opt(song, "stats.plays", 0),
|
||||||
downloads: opt(song, 'stats.downloads', 0),
|
downloads: opt(song, "stats.downloads", 0),
|
||||||
upvotes: opt(song, 'stats.upVotes', 0),
|
upvotes: opt(song, "stats.upVotes", 0),
|
||||||
downvotes: opt(song, 'stats.downVotes', 0),
|
downvotes: opt(song, "stats.downVotes", 0),
|
||||||
score: null
|
score: null,
|
||||||
},
|
},
|
||||||
uploaded: opt(song, 'uploaded', null),
|
uploaded: opt(song, "uploaded", null),
|
||||||
automapper: !!opt(song, 'metadata.automapper', false),
|
automapper: !!opt(song, "metadata.automapper", false),
|
||||||
ranked: null,
|
ranked: null,
|
||||||
qualified: null,
|
qualified: null,
|
||||||
versions: [
|
versions: [
|
||||||
@ -224,20 +297,20 @@ export default () => {
|
|||||||
hash,
|
hash,
|
||||||
key,
|
key,
|
||||||
state: "Published",
|
state: "Published",
|
||||||
createdAt: opt(song, 'uploaded', null),
|
createdAt: opt(song, "uploaded", null),
|
||||||
sageScore: null,
|
sageScore: null,
|
||||||
diffs,
|
diffs,
|
||||||
downloadURL: `https://cdn.beatsaver.com/${hash}.zip`,
|
downloadURL: `https://cdn.beatsaver.com/${hash}.zip`,
|
||||||
coverURL: `https://cdn.beatsaver.com/${hash}.jpg`,
|
coverURL: `https://cdn.beatsaver.com/${hash}.jpg`,
|
||||||
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`
|
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
byHash,
|
byHash,
|
||||||
byKey,
|
byKey,
|
||||||
convertOldBeatSaverToBeatMaps
|
convertOldBeatSaverToBeatMaps,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,16 +1,24 @@
|
|||||||
import {PRIORITY} from '../network/queues/http-queue';
|
import { PRIORITY } from "../network/queues/http-queue";
|
||||||
import createPlayerService from './scoresaber/player'
|
import createPlayerService from "./scoresaber/player";
|
||||||
import createScoresService from './scoresaber/scores'
|
import createScoresService from "./scoresaber/scores";
|
||||||
import beatSaviorApiClient from '../network/clients/beatsavior/api';
|
import beatSaviorApiClient from "../network/clients/beatsavior/api";
|
||||||
import beatSaviorRepository from '../db/repository/beat-savior'
|
import beatSaviorRepository from "../db/repository/beat-savior";
|
||||||
import beatSaviorPlayersRepository from '../db/repository/beat-savior-players'
|
import beatSaviorPlayersRepository from "../db/repository/beat-savior-players";
|
||||||
import {addToDate, DAY, formatDate, HOUR, MINUTE, SECOND, truncateDate} from '../utils/date'
|
import {
|
||||||
import log from '../utils/logger'
|
addToDate,
|
||||||
import {opt} from '../utils/js'
|
DAY,
|
||||||
import makePendingPromisePool from '../utils/pending-promises'
|
formatDate,
|
||||||
import {PLAYER_SCORES_PER_PAGE} from '../utils/scoresaber/consts'
|
HOUR,
|
||||||
import {roundToPrecision} from '../utils/format'
|
MINUTE,
|
||||||
import {serviceFilterFunc} from './utils'
|
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 MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 15;
|
||||||
const CACHED_PLAYER_REFRESH_INTERVAL = HOUR * 3;
|
const CACHED_PLAYER_REFRESH_INTERVAL = HOUR * 3;
|
||||||
@ -30,52 +38,73 @@ export default () => {
|
|||||||
const playerService = createPlayerService();
|
const playerService = createPlayerService();
|
||||||
const scoresService = createScoresService();
|
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([
|
const [beatSaviorData, playerScores] = await Promise.all([
|
||||||
getPlayerScores(playerId),
|
getPlayerScores(playerId),
|
||||||
resolvePromiseOrWaitForPending(`getSsPlayerScores/${playerId}`, () => scoresService.getPlayerScoresAsObject(
|
resolvePromiseOrWaitForPending(`getSsPlayerScores/${playerId}`, () =>
|
||||||
|
scoresService.getPlayerScoresAsObject(
|
||||||
playerId,
|
playerId,
|
||||||
score => score?.leaderboard?.song?.hash?.toLowerCase() ?? null,
|
(score) => score?.leaderboard?.song?.hash?.toLowerCase() ?? null,
|
||||||
true,
|
true,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return beatSaviorData.map(bsData => {
|
return beatSaviorData.map((bsData) => {
|
||||||
if (!bsData?.hash || !playerScores?.[bsData?.hash?.toLowerCase()]) return 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 {
|
return {
|
||||||
...bsData,
|
...bsData,
|
||||||
ssScore
|
ssScore,
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const isScoreMatchingBsData = (score, bsData, exact = true) => {
|
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 diff = opt(score, "leaderboard.diffInfo.diff");
|
||||||
const scoreValue = opt(score, 'score.score');
|
const scoreValue = opt(score, "score.score");
|
||||||
const timeSet = opt(score, 'score.timeSet')
|
const timeSet = opt(score, "score.timeSet");
|
||||||
let hash = opt(score, 'leaderboard.song.hash');
|
let hash = opt(score, "leaderboard.song.hash");
|
||||||
|
|
||||||
if (!diff || !score || !timeSet || !hash) return false;
|
if (!diff || !score || !timeSet || !hash) return false;
|
||||||
|
|
||||||
hash = hash.toLowerCase();
|
hash = hash.toLowerCase();
|
||||||
|
|
||||||
if (bsData.hash === hash && bsData.diff === diff) {
|
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;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getScoresHistogramDefinition = (serviceParams = {sort: 'recent', order: 'desc'}) => {
|
const getScoresHistogramDefinition = (
|
||||||
const sort = serviceParams?.sort ?? 'recent';
|
serviceParams = { sort: "recent", order: "desc" },
|
||||||
const order = serviceParams?.order ?? 'desc';
|
) => {
|
||||||
|
const sort = serviceParams?.sort ?? "recent";
|
||||||
|
const order = serviceParams?.order ?? "desc";
|
||||||
|
|
||||||
let round = 2;
|
let round = 2;
|
||||||
let bucketSize = 1;
|
let bucketSize = 1;
|
||||||
@ -83,57 +112,65 @@ export default () => {
|
|||||||
let maxBucketSize = null;
|
let maxBucketSize = null;
|
||||||
let bucketSizeStep = null;
|
let bucketSizeStep = null;
|
||||||
let bucketSizeValues = null;
|
let bucketSizeValues = null;
|
||||||
let type = 'linear';
|
let type = "linear";
|
||||||
let valFunc = s => s;
|
let valFunc = (s) => s;
|
||||||
let filterFunc = serviceFilterFunc(serviceParams);
|
let filterFunc = serviceFilterFunc(serviceParams);
|
||||||
let histogramFilterFunc = s => s;
|
let histogramFilterFunc = (s) => s;
|
||||||
let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear'
|
let roundedValFunc = (s, type = type, precision = bucketSize) =>
|
||||||
|
type === "linear"
|
||||||
? roundToPrecision(valFunc(s), precision)
|
? roundToPrecision(valFunc(s), precision)
|
||||||
: truncateDate(valFunc(s), precision);
|
: truncateDate(valFunc(s), precision);
|
||||||
let prefix = '';
|
let prefix = "";
|
||||||
let prefixLong = '';
|
let prefixLong = "";
|
||||||
let suffix = '';
|
let suffix = "";
|
||||||
let suffixLong = '';
|
let suffixLong = "";
|
||||||
|
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'recent':
|
case "recent":
|
||||||
valFunc = s => s?.timeSet;
|
valFunc = (s) => s?.timeSet;
|
||||||
type = 'time';
|
type = "time";
|
||||||
bucketSize = 'day'
|
bucketSize = "day";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'acc':
|
case "acc":
|
||||||
valFunc = s => (s?.trackers?.scoreTracker?.rawRatio ?? 0) * 100;
|
valFunc = (s) => (s?.trackers?.scoreTracker?.rawRatio ?? 0) * 100;
|
||||||
histogramFilterFunc = h => h?.x >= HISTOGRAM_ACC_THRESHOLD;
|
histogramFilterFunc = (h) => h?.x >= HISTOGRAM_ACC_THRESHOLD;
|
||||||
type = 'linear';
|
type = "linear";
|
||||||
bucketSize = 0.25;
|
bucketSize = 0.25;
|
||||||
minBucketSize = 0.05;
|
minBucketSize = 0.05;
|
||||||
maxBucketSize = 10;
|
maxBucketSize = 10;
|
||||||
bucketSizeStep = 0.05;
|
bucketSizeStep = 0.05;
|
||||||
round = 2;
|
round = 2;
|
||||||
suffix = '%';
|
suffix = "%";
|
||||||
suffixLong = '%';
|
suffixLong = "%";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mistakes':
|
case "mistakes":
|
||||||
valFunc = s => (s?.stats?.miss ?? 0) + (s?.stats?.wallHit ?? 0) + (s?.stats?.bombHit ?? 0);
|
valFunc = (s) =>
|
||||||
histogramFilterFunc = h => h?.x <= HISTOGRAM_MISTAKES_THRESHOLD;
|
(s?.stats?.miss ?? 0) +
|
||||||
type = 'linear';
|
(s?.stats?.wallHit ?? 0) +
|
||||||
|
(s?.stats?.bombHit ?? 0);
|
||||||
|
histogramFilterFunc = (h) => h?.x <= HISTOGRAM_MISTAKES_THRESHOLD;
|
||||||
|
type = "linear";
|
||||||
bucketSize = 1;
|
bucketSize = 1;
|
||||||
minBucketSize = 1;
|
minBucketSize = 1;
|
||||||
maxBucketSize = 50;
|
maxBucketSize = 50;
|
||||||
bucketSizeStep = 1;
|
bucketSizeStep = 1;
|
||||||
round = 0;
|
round = 0;
|
||||||
suffixLong = ' mistake(s)';
|
suffixLong = " mistake(s)";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getValue: valFunc,
|
getValue: valFunc,
|
||||||
getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize),
|
getRoundedValue:
|
||||||
|
(bucketSize = bucketSize) =>
|
||||||
|
(s) =>
|
||||||
|
roundedValFunc(s, type, bucketSize),
|
||||||
filter: filterFunc,
|
filter: filterFunc,
|
||||||
histogramFilter: histogramFilterFunc,
|
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,
|
type,
|
||||||
bucketSize,
|
bucketSize,
|
||||||
minBucketSize,
|
minBucketSize,
|
||||||
@ -145,11 +182,14 @@ export default () => {
|
|||||||
prefixLong,
|
prefixLong,
|
||||||
suffix,
|
suffix,
|
||||||
suffixLong,
|
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;
|
let page = serviceParams?.page ?? 1;
|
||||||
if (page < 1) page = 1;
|
if (page < 1) page = 1;
|
||||||
|
|
||||||
@ -157,9 +197,10 @@ export default () => {
|
|||||||
|
|
||||||
if (!playerScores || !playerScores.length) return { total: 0, scores: [] };
|
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;
|
const startIdx = (page - 1) * PLAYER_SCORES_PER_PAGE;
|
||||||
|
|
||||||
@ -169,13 +210,14 @@ export default () => {
|
|||||||
total: playerScores.length,
|
total: playerScores.length,
|
||||||
scores: playerScores
|
scores: playerScores
|
||||||
.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE)
|
.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE)
|
||||||
.map(bs => {
|
.map((bs) => {
|
||||||
const leaderboard = bs.leaderboard;
|
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
|
leaderboard.leaderboardId += Math.random(); // ScoresSvelte needs different keys for each scores row
|
||||||
|
|
||||||
const rawScore = opt(bs, 'trackers.scoreTracker.rawScore', 0);
|
const rawScore = opt(bs, "trackers.scoreTracker.rawScore", 0);
|
||||||
const rawRatio = opt(bs, 'trackers.scoreTracker.rawRatio', 0);
|
const rawRatio = opt(bs, "trackers.scoreTracker.rawRatio", 0);
|
||||||
const maxScore = rawRatio & rawScore ? rawScore / rawRatio : 0;
|
const maxScore = rawRatio & rawScore ? rawScore / rawRatio : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -188,51 +230,76 @@ export default () => {
|
|||||||
score: {
|
score: {
|
||||||
acc: rawRatio * 100,
|
acc: rawRatio * 100,
|
||||||
maxScore,
|
maxScore,
|
||||||
mods: opt(bs, 'trackers.scoreTracker.modifiers', null),
|
mods: opt(bs, "trackers.scoreTracker.modifiers", null),
|
||||||
percentage: opt(bs, 'trackers.scoreTracker.rawRatio', 0) * 100,
|
percentage: opt(bs, "trackers.scoreTracker.rawRatio", 0) * 100,
|
||||||
pp: 0,
|
pp: 0,
|
||||||
ppWeighted: 0,
|
ppWeighted: 0,
|
||||||
rank: null,
|
rank: null,
|
||||||
score: opt(bs, 'trackers.scoreTracker.score', 0),
|
score: opt(bs, "trackers.scoreTracker.score", 0),
|
||||||
scoreId: bs.beatSaviorId,
|
scoreId: bs.beatSaviorId,
|
||||||
timeSet: bs.timeSet,
|
timeSet: bs.timeSet,
|
||||||
unmodifiedScore: rawScore,
|
unmodifiedScore: rawScore,
|
||||||
weight: 0,
|
weight: 0,
|
||||||
},
|
},
|
||||||
timeSet: bs.timeSet,
|
timeSet: bs.timeSet,
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
}
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const updateData = async (playerId, data) => {
|
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;
|
return data;
|
||||||
}
|
};
|
||||||
|
|
||||||
const fetchPlayer = async (playerId, priority = PRIORITY.BG_NORMAL) => {
|
const fetchPlayer = async (playerId, priority = PRIORITY.BG_NORMAL) => {
|
||||||
try {
|
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) {
|
if (!data) {
|
||||||
log.debug(`No Beat Savior data for player "${playerId}"`, 'BeatSaviorService')
|
log.debug(
|
||||||
|
`No Beat Savior data for player "${playerId}"`,
|
||||||
|
"BeatSaviorService",
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check if data already exists in DB
|
// 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);
|
return updateData(playerId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -240,62 +307,121 @@ export default () => {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const refresh = async (playerId, force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
|
const refresh = async (
|
||||||
log.trace(`Starting refreshing BeatSavior for player "${playerId}" ${force ? ' (forced)' : ''}...`, 'BeatSaviorService')
|
playerId,
|
||||||
|
force = false,
|
||||||
|
priority = PRIORITY.BG_NORMAL,
|
||||||
|
throwErrors = false,
|
||||||
|
) => {
|
||||||
|
log.trace(
|
||||||
|
`Starting refreshing BeatSavior for player "${playerId}" ${
|
||||||
|
force ? " (forced)" : ""
|
||||||
|
}...`,
|
||||||
|
"BeatSaviorService",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const player = await playerService.get(playerId);
|
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 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()) {
|
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;
|
return null;
|
||||||
|
|
||||||
if (player) {
|
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) {
|
if (
|
||||||
log.debug(`Beat Savior data for player "${playerId}" was refreshed after recent play, skipping`, 'BeatSaviorService')
|
player.recentPlay &&
|
||||||
|
player.recentPlay < bsPlayerInfo.lastRefresh
|
||||||
|
) {
|
||||||
|
log.debug(
|
||||||
|
`Beat Savior data for player "${playerId}" was refreshed after recent play, skipping`,
|
||||||
|
"BeatSaviorService",
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvePromiseOrWaitForPending(`refresh/${playerId}`, () => fetchPlayer(playerId, priority));
|
return resolvePromiseOrWaitForPending(`refresh/${playerId}`, () =>
|
||||||
|
fetchPlayer(playerId, priority),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (throwErrors) throw 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
|
const refreshAll = async (
|
||||||
log.trace(`Starting refreshing Beat Savior data for all players${force ? ' (forced)' : ''}...`, 'BeatSaviorService');
|
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();
|
const allPlayers = await playerService.getAll();
|
||||||
if (!allPlayers || !allPlayers.length) {
|
if (!allPlayers || !allPlayers.length) {
|
||||||
log.trace(`No players in DB, skipping.`, 'BeatSaviorService');
|
log.trace(`No players in DB, skipping.`, "BeatSaviorService");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allRefreshed = await Promise.all(allPlayers.map(async player => ({
|
const allRefreshed = await Promise.all(
|
||||||
|
allPlayers.map(async (player) => ({
|
||||||
playerId: player.playerId,
|
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;
|
return allRefreshed;
|
||||||
}
|
};
|
||||||
|
|
||||||
const get = async (playerId, score) => {
|
const get = async (playerId, score) => {
|
||||||
if (score && score.beatSavior) return score.beatSavior;
|
if (score && score.beatSavior) return score.beatSavior;
|
||||||
@ -303,12 +429,18 @@ export default () => {
|
|||||||
const playerBsData = await getPlayerScores(playerId);
|
const playerBsData = await getPlayerScores(playerId);
|
||||||
if (!playerBsData || !playerBsData.length) return null;
|
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;
|
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 = () => {
|
const destroyService = () => {
|
||||||
serviceCreationCount--;
|
serviceCreationCount--;
|
||||||
@ -319,7 +451,7 @@ export default () => {
|
|||||||
|
|
||||||
service = null;
|
service = null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
fetchPlayer,
|
fetchPlayer,
|
||||||
@ -332,7 +464,7 @@ export default () => {
|
|||||||
isDataForPlayerAvailable,
|
isDataForPlayerAvailable,
|
||||||
getScoresHistogramDefinition,
|
getScoresHistogramDefinition,
|
||||||
destroyService,
|
destroyService,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import keyValueRepository from '../db/repository/key-value';
|
import keyValueRepository from "../db/repository/key-value";
|
||||||
import {opt} from '../utils/js'
|
import { opt } from "../utils/js";
|
||||||
|
|
||||||
const STORE_CONFIG_KEY = 'config';
|
const STORE_CONFIG_KEY = "config";
|
||||||
|
|
||||||
let service = null;
|
let service = null;
|
||||||
|
|
||||||
@ -9,22 +9,23 @@ export default () => {
|
|||||||
if (service) return service;
|
if (service) return service;
|
||||||
|
|
||||||
const get = async () => keyValueRepository().get(STORE_CONFIG_KEY);
|
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 getMainPlayerId = async () => {
|
||||||
const config = await get();
|
const config = await get();
|
||||||
|
|
||||||
return opt(config, 'users.main');
|
return opt(config, "users.main");
|
||||||
}
|
};
|
||||||
|
|
||||||
const destroyService = () => {}
|
const destroyService = () => {};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
get,
|
get,
|
||||||
set,
|
set,
|
||||||
getMainPlayerId,
|
getMainPlayerId,
|
||||||
destroyService,
|
destroyService,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
};
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import leaderboardPageClient from '../../network/clients/scoresaber/leaderboard/page-leaderboard'
|
import leaderboardPageClient from "../../network/clients/scoresaber/leaderboard/page-leaderboard";
|
||||||
import accSaberLeaderboardApiClient from '../../network/clients/accsaber/api-leaderboard'
|
import accSaberLeaderboardApiClient from "../../network/clients/accsaber/api-leaderboard";
|
||||||
import makePendingPromisePool from '../../utils/pending-promises'
|
import makePendingPromisePool from "../../utils/pending-promises";
|
||||||
import createPlayersService from '../../services/scoresaber/player'
|
import createPlayersService from "../../services/scoresaber/player";
|
||||||
import createScoresService from '../../services/scoresaber/scores'
|
import createScoresService from "../../services/scoresaber/scores";
|
||||||
import {PRIORITY} from '../../network/queues/http-queue'
|
import { PRIORITY } from "../../network/queues/http-queue";
|
||||||
import {LEADERBOARD_SCORES_PER_PAGE} from '../../utils/scoresaber/consts'
|
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 { LEADERBOARD_SCORES_PER_PAGE as ACCSABER_LEADERBOARD_SCORES_PER_PAGE } from "../../utils/accsaber/consts";
|
||||||
import {formatDateRelative, MINUTE} from '../../utils/date'
|
import { formatDateRelative, MINUTE } from "../../utils/date";
|
||||||
import {convertArrayToObjectByKey, opt} from '../../utils/js'
|
import { convertArrayToObjectByKey, opt } from "../../utils/js";
|
||||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||||
|
|
||||||
const ACCSABER_LEADERBOARD_NETWORK_TTL = MINUTE * 5;
|
const ACCSABER_LEADERBOARD_NETWORK_TTL = MINUTE * 5;
|
||||||
|
|
||||||
@ -20,79 +20,118 @@ export default () => {
|
|||||||
const scoresService = createScoresService();
|
const scoresService = createScoresService();
|
||||||
|
|
||||||
let friendsPromise = Promise.resolve([]);
|
let friendsPromise = Promise.resolve([]);
|
||||||
const refreshFriends = async () => friendsPromise = playersService.getAll();
|
const refreshFriends = async () => (friendsPromise = playersService.getAll());
|
||||||
eventBus.on('player-profile-removed', playerId => refreshFriends());
|
eventBus.on("player-profile-removed", (playerId) => refreshFriends());
|
||||||
eventBus.on('player-profile-added', player => refreshFriends());
|
eventBus.on("player-profile-added", (player) => refreshFriends());
|
||||||
eventBus.on('player-profile-changed', player => refreshFriends());
|
eventBus.on("player-profile-changed", (player) => refreshFriends());
|
||||||
refreshFriends().then(_ => {});
|
refreshFriends().then((_) => {});
|
||||||
|
|
||||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
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}`,
|
`pageClient/leaderboard/${leaderboardId}/${page}`,
|
||||||
() => leaderboardPageClient.getProcessed({
|
() =>
|
||||||
|
leaderboardPageClient.getProcessed({
|
||||||
leaderboardId,
|
leaderboardId,
|
||||||
page,
|
page,
|
||||||
signal,
|
signal,
|
||||||
priority,
|
priority,
|
||||||
cacheTtl: MINUTE,
|
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;
|
if (page < 1) page = 1;
|
||||||
|
|
||||||
const data = await resolvePromiseOrWaitForPending(
|
const data = await resolvePromiseOrWaitForPending(
|
||||||
`accSaberApiClient/leaderboard/${leaderboardId}/${page}`,
|
`accSaberApiClient/leaderboard/${leaderboardId}/${page}`,
|
||||||
() => accSaberLeaderboardApiClient.getProcessed({
|
() =>
|
||||||
|
accSaberLeaderboardApiClient.getProcessed({
|
||||||
leaderboardId,
|
leaderboardId,
|
||||||
page,
|
page,
|
||||||
signal,
|
signal,
|
||||||
priority,
|
priority,
|
||||||
cacheTtl: ACCSABER_LEADERBOARD_NETWORK_TTL,
|
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;
|
const startIdx = (page - 1) * ACCSABER_LEADERBOARD_SCORES_PER_PAGE;
|
||||||
if (data.scores.length < startIdx + 1) return data;
|
if (data.scores.length < startIdx + 1) return data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
scores: data.scores
|
scores: data.scores.slice(
|
||||||
.slice(startIdx, startIdx + ACCSABER_LEADERBOARD_SCORES_PER_PAGE)
|
startIdx,
|
||||||
}
|
startIdx + ACCSABER_LEADERBOARD_SCORES_PER_PAGE,
|
||||||
}
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getFriendsLeaderboard = async (leaderboardId, priority = PRIORITY.FG_LOW, signal = null) => {
|
const getFriendsLeaderboard = async (
|
||||||
const leaderboard = await resolvePromiseOrWaitForPending(`pageClient/leaderboard/${leaderboardId}/1`, () => leaderboardPageClient.getProcessed({leaderboardId, page: 1, signal, priority, cacheTtl: MINUTE}));
|
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))
|
const scores = (await scoresService.getLeaderboardScores(leaderboardId))
|
||||||
.map(score => {
|
.map((score) => {
|
||||||
if (!score || !score.playerId || !friends[score.playerId]) return null;
|
if (!score || !score.playerId || !friends[score.playerId]) return null;
|
||||||
|
|
||||||
const player = friends[score.playerId];
|
const player = friends[score.playerId];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
player: {playerId: player.playerId, name: player.name, playerInfo: {...player.playerInfo}},
|
player: {
|
||||||
|
playerId: player.playerId,
|
||||||
|
name: player.name,
|
||||||
|
playerInfo: { ...player.playerInfo },
|
||||||
|
},
|
||||||
score: { ...score.score },
|
score: { ...score.score },
|
||||||
}
|
};
|
||||||
})
|
})
|
||||||
.filter(s => s)
|
.filter((s) => s)
|
||||||
.sort((a, b) => opt(b, 'score.score', 0) - opt(a, 'score.score', 0))
|
.sort((a, b) => opt(b, "score.score", 0) - opt(a, "score.score", 0))
|
||||||
.map((score, idx) => ({
|
.map((score, idx) => ({
|
||||||
player: score.player,
|
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 };
|
return { ...leaderboard, scores, pageQty: 1, totalItems: scores.length };
|
||||||
}
|
};
|
||||||
|
|
||||||
const destroyService = () => {
|
const destroyService = () => {
|
||||||
service = null;
|
service = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
fetchPage,
|
fetchPage,
|
||||||
@ -100,7 +139,7 @@ export default () => {
|
|||||||
getFriendsLeaderboard,
|
getFriendsLeaderboard,
|
||||||
LEADERBOARD_SCORES_PER_PAGE,
|
LEADERBOARD_SCORES_PER_PAGE,
|
||||||
destroyService,
|
destroyService,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
};
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||||
import {configStore} from '../../stores/config'
|
import { configStore } from "../../stores/config";
|
||||||
import playerApiClient from '../../network/clients/scoresaber/player/api'
|
import playerApiClient from "../../network/clients/scoresaber/player/api";
|
||||||
import playerFindApiClient from '../../network/clients/scoresaber/players/api-player-find'
|
import playerFindApiClient from "../../network/clients/scoresaber/players/api-player-find";
|
||||||
import playerPageClient from '../../network/clients/scoresaber/player/page'
|
import playerPageClient from "../../network/clients/scoresaber/player/page";
|
||||||
import {PRIORITY} from '../../network/queues/http-queue'
|
import { PRIORITY } from "../../network/queues/http-queue";
|
||||||
import playersRepository from '../../db/repository/players'
|
import playersRepository from "../../db/repository/players";
|
||||||
import playersHistoryRepository from '../../db/repository/players-history'
|
import playersHistoryRepository from "../../db/repository/players-history";
|
||||||
import log from '../../utils/logger'
|
import log from "../../utils/logger";
|
||||||
import {
|
import {
|
||||||
addToDate,
|
addToDate,
|
||||||
formatDate,
|
formatDate,
|
||||||
@ -14,12 +14,12 @@ import {
|
|||||||
SECOND,
|
SECOND,
|
||||||
toSsMidnight,
|
toSsMidnight,
|
||||||
truncateDate,
|
truncateDate,
|
||||||
} from '../../utils/date'
|
} from "../../utils/date";
|
||||||
import {opt} from '../../utils/js'
|
import { opt } from "../../utils/js";
|
||||||
import {db} from '../../db/db'
|
import { db } from "../../db/db";
|
||||||
import makePendingPromisePool from '../../utils/pending-promises'
|
import makePendingPromisePool from "../../utils/pending-promises";
|
||||||
import {worker} from '../../utils/worker-wrappers'
|
import { worker } from "../../utils/worker-wrappers";
|
||||||
import {getServicePlayerGain} from '../utils'
|
import { getServicePlayerGain } from "../utils";
|
||||||
|
|
||||||
const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 3;
|
const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 3;
|
||||||
const PLAYER_REFRESH_INTERVAL = MINUTE * 20;
|
const PLAYER_REFRESH_INTERVAL = MINUTE * 20;
|
||||||
@ -34,91 +34,128 @@ export default () => {
|
|||||||
|
|
||||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||||
|
|
||||||
const configStoreUnsubscribe = configStore.subscribe(config => {
|
const configStoreUnsubscribe = configStore.subscribe((config) => {
|
||||||
const newMainPlayerId = opt(config, 'users.main')
|
const newMainPlayerId = opt(config, "users.main");
|
||||||
if (mainPlayerId !== newMainPlayerId) {
|
if (mainPlayerId !== newMainPlayerId) {
|
||||||
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);
|
const getAll = async (force = false) => playersRepository().getAll(force);
|
||||||
|
|
||||||
// TODO: just for now
|
// 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 getAllActive = async () => {
|
||||||
const players = await getAll();
|
const players = await getAll();
|
||||||
if (!players) return [];
|
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) => {
|
const removePlayer = async (playerId, purgeScores = false) => {
|
||||||
await playersRepository().delete(playerId);
|
await playersRepository().delete(playerId);
|
||||||
|
|
||||||
// TODO: purge scores if requested
|
// 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) => {
|
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);
|
const player = await refresh(playerId, true, priority, false, true);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
log.warn(`Can not add player "${playerId}"`, 'PlayerService');
|
log.warn(`Can not add player "${playerId}"`, "PlayerService");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventBus.publish('player-profile-added', player);
|
eventBus.publish("player-profile-added", player);
|
||||||
eventBus.publish('player-profile-changed', player);
|
eventBus.publish("player-profile-changed", player);
|
||||||
|
|
||||||
log.trace(`Player "${playerId}" added.`, 'PlayerService')
|
log.trace(`Player "${playerId}" added.`, "PlayerService");
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
};
|
||||||
|
|
||||||
const setPlayer = async (player) => {
|
const setPlayer = async (player) => {
|
||||||
await playersRepository().set(player);
|
await playersRepository().set(player);
|
||||||
|
|
||||||
eventBus.publish('player-profile-changed', player);
|
eventBus.publish("player-profile-changed", player);
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
};
|
||||||
|
|
||||||
const updatePlayer = async (player, waitForSaving = true, forceAdd = false) => {
|
const updatePlayer = async (
|
||||||
|
player,
|
||||||
|
waitForSaving = true,
|
||||||
|
forceAdd = false,
|
||||||
|
) => {
|
||||||
if (!player || !player.playerId) {
|
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);
|
const dbPlayer = await getPlayer(player.playerId);
|
||||||
if (!dbPlayer && !forceAdd) return player;
|
if (!dbPlayer && !forceAdd) return player;
|
||||||
|
|
||||||
const finalPlayer = {...dbPlayer, ...player}
|
const finalPlayer = { ...dbPlayer, ...player };
|
||||||
|
|
||||||
if (!waitForSaving) {
|
if (!waitForSaving) {
|
||||||
setPlayer(finalPlayer).then(_ => _)
|
setPlayer(finalPlayer).then((_) => _);
|
||||||
|
|
||||||
return finalPlayer;
|
return finalPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await setPlayer(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;
|
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;
|
if (!playerId) return null;
|
||||||
|
|
||||||
@ -130,8 +167,9 @@ export default () => {
|
|||||||
const playerIdLocalTimestamp = `${playerId}_${localDate.getTime()}`;
|
const playerIdLocalTimestamp = `${playerId}_${localDate.getTime()}`;
|
||||||
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
|
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
|
||||||
|
|
||||||
return playersHistoryRepository().getFromIndex('players-history-playerIdSsTimestamp', playerIdSsTimestamp)
|
return playersHistoryRepository()
|
||||||
.then(async ph => {
|
.getFromIndex("players-history-playerIdSsTimestamp", playerIdSsTimestamp)
|
||||||
|
.then(async (ph) => {
|
||||||
if (ph && ph._idbId) {
|
if (ph && ph._idbId) {
|
||||||
await playersHistoryRepository().delete(ph._idbId);
|
await playersHistoryRepository().delete(ph._idbId);
|
||||||
|
|
||||||
@ -142,53 +180,74 @@ export default () => {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.then(async previous => {
|
.then(async (previous) => {
|
||||||
let accStats = {};
|
let accStats = {};
|
||||||
|
|
||||||
if (worker) {
|
if (worker) {
|
||||||
const stats = await worker.calcPlayerStats(playerId);
|
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 ?? {};
|
const { badges, totalScore, playCount, ...playerStats } = stats ?? {};
|
||||||
|
|
||||||
accStats = {...playerStats}
|
accStats = { ...playerStats };
|
||||||
|
|
||||||
if (ppBoundary) accStats.ppBoundary = ppBoundary;
|
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({
|
return playersHistoryRepository().set({
|
||||||
...previous,
|
...previous,
|
||||||
...accStats,
|
...accStats,
|
||||||
playerId, banned, countries, inactive, pp, rank, ...scoreStats,
|
playerId,
|
||||||
localDate, ssDate,
|
banned,
|
||||||
|
countries,
|
||||||
|
inactive,
|
||||||
|
pp,
|
||||||
|
rank,
|
||||||
|
...scoreStats,
|
||||||
|
localDate,
|
||||||
|
ssDate,
|
||||||
playerIdLocalTimestamp,
|
playerIdLocalTimestamp,
|
||||||
playerIdSsTimestamp,
|
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 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);
|
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);
|
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;
|
let player;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.runInTransaction(['players'], async tx => {
|
await db.runInTransaction(["players"], async (tx) => {
|
||||||
const playersStore = tx.objectStore('players')
|
const playersStore = tx.objectStore("players");
|
||||||
player = await playersStore.get(playerId);
|
player = await playersStore.get(playerId);
|
||||||
if (player) {
|
if (player) {
|
||||||
player.recentPlayLastUpdated = recentPlayLastUpdated;
|
player.recentPlayLastUpdated = recentPlayLastUpdated;
|
||||||
@ -200,61 +259,135 @@ export default () => {
|
|||||||
|
|
||||||
if (player) {
|
if (player) {
|
||||||
playersRepository().addToCache([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) {
|
} catch (err) {
|
||||||
// swallow error
|
// 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 isResponseCached = (response) =>
|
||||||
const getDataFromResponse = response => playerApiClient.getDataFromResponse(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);
|
const player = await getPlayer(playerId);
|
||||||
|
|
||||||
if (!player || !isProfileFresh(player, refreshInterval)) {
|
if (!player || !isProfileFresh(player, refreshInterval)) {
|
||||||
const fetchedPlayerResponse = await fetchPlayer(playerId, priority, {signal, cacheTtl: MINUTE, maxAge: force ? 0 : refreshInterval, fullResponse: true});
|
const fetchedPlayerResponse = await fetchPlayer(playerId, priority, {
|
||||||
if (isResponseCached(fetchedPlayerResponse)) return getDataFromResponse(fetchedPlayerResponse);
|
signal,
|
||||||
|
cacheTtl: MINUTE,
|
||||||
|
maxAge: force ? 0 : refreshInterval,
|
||||||
|
fullResponse: true,
|
||||||
|
});
|
||||||
|
if (isResponseCached(fetchedPlayerResponse))
|
||||||
|
return getDataFromResponse(fetchedPlayerResponse);
|
||||||
|
|
||||||
return updatePlayer({...player, ...getDataFromResponse(fetchedPlayerResponse), profileLastUpdated: new Date()}, false)
|
return updatePlayer(
|
||||||
.then(player => {
|
{
|
||||||
|
...player,
|
||||||
|
...getDataFromResponse(fetchedPlayerResponse),
|
||||||
|
profileLastUpdated: new Date(),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
).then((player) => {
|
||||||
fetchPlayerAndUpdateRecentPlay(player.playerId);
|
fetchPlayerAndUpdateRecentPlay(player.playerId);
|
||||||
|
|
||||||
updatePlayerHistory(player);
|
updatePlayerHistory(player);
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
};
|
||||||
|
|
||||||
const refresh = async (playerId, force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false, addIfNotExists = false) => {
|
const refresh = async (
|
||||||
log.trace(`Starting refreshing player "${playerId}" ${force ? ' (forced)' : ''}...`, 'PlayerService')
|
playerId,
|
||||||
|
force = false,
|
||||||
|
priority = PRIORITY.BG_NORMAL,
|
||||||
|
throwErrors = false,
|
||||||
|
addIfNotExists = false,
|
||||||
|
) => {
|
||||||
|
log.trace(
|
||||||
|
`Starting refreshing player "${playerId}" ${force ? " (forced)" : ""}...`,
|
||||||
|
"PlayerService",
|
||||||
|
);
|
||||||
|
|
||||||
if (!playerId) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@ -262,66 +395,101 @@ export default () => {
|
|||||||
try {
|
try {
|
||||||
let player = await getPlayer(playerId);
|
let player = await getPlayer(playerId);
|
||||||
if (!player && !addIfNotExists) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.trace(`Player fetched from DB`, 'PlayerService', player);
|
log.trace(`Player fetched from DB`, "PlayerService", player);
|
||||||
|
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const profileFreshnessDate = getProfileFreshnessDate(player);
|
const profileFreshnessDate = getProfileFreshnessDate(player);
|
||||||
if (profileFreshnessDate > new Date()) {
|
if (profileFreshnessDate > new Date()) {
|
||||||
|
log.debug(
|
||||||
log.debug(`Profile is still fresh, skipping. Next refresh on ${formatDate(profileFreshnessDate)}`, 'PlayerService')
|
`Profile is still fresh, skipping. Next refresh on ${formatDate(
|
||||||
|
profileFreshnessDate,
|
||||||
|
)}`,
|
||||||
|
"PlayerService",
|
||||||
|
);
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.trace(`Fetching player ${playerId} from ScoreSaber...`, 'PlayerService')
|
log.trace(
|
||||||
|
`Fetching player ${playerId} from ScoreSaber...`,
|
||||||
|
"PlayerService",
|
||||||
|
);
|
||||||
|
|
||||||
const fetchedPlayer = await fetchPlayer(playerId, priority);
|
const fetchedPlayer = await fetchPlayer(playerId, priority);
|
||||||
|
|
||||||
if (!fetchedPlayer || !fetchedPlayer.playerId || !fetchedPlayer.name || !fetchedPlayer.playerInfo || !fetchedPlayer.scoreStats) {
|
if (
|
||||||
log.warn(`ScoreSaber returned empty info for player ${playerId}`, 'PlayerService')
|
!fetchedPlayer ||
|
||||||
|
!fetchedPlayer.playerId ||
|
||||||
|
!fetchedPlayer.name ||
|
||||||
|
!fetchedPlayer.playerInfo ||
|
||||||
|
!fetchedPlayer.scoreStats
|
||||||
|
) {
|
||||||
|
log.warn(
|
||||||
|
`ScoreSaber returned empty info for player ${playerId}`,
|
||||||
|
"PlayerService",
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
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;
|
return player;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (throwErrors) throw 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
|
const refreshAll = async (
|
||||||
log.trace(`Starting refreshing all players${force ? ' (forced)' : ''}...`, 'PlayerService');
|
force = false,
|
||||||
|
priority = PRIORITY.BG_NORMAL,
|
||||||
|
throwErrors = false,
|
||||||
|
) => {
|
||||||
|
log.trace(
|
||||||
|
`Starting refreshing all players${force ? " (forced)" : ""}...`,
|
||||||
|
"PlayerService",
|
||||||
|
);
|
||||||
|
|
||||||
const allPlayers = await getAll();
|
const allPlayers = await getAll();
|
||||||
if (!allPlayers || !allPlayers.length) {
|
if (!allPlayers || !allPlayers.length) {
|
||||||
log.trace(`No players in DB, skipping.`, 'PlayerService');
|
log.trace(`No players in DB, skipping.`, "PlayerService");
|
||||||
return null;
|
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;
|
return allRefreshed;
|
||||||
}
|
};
|
||||||
|
|
||||||
const destroyService = () => {
|
const destroyService = () => {
|
||||||
serviceCreationCount--;
|
serviceCreationCount--;
|
||||||
@ -331,7 +499,7 @@ export default () => {
|
|||||||
|
|
||||||
service = null;
|
service = null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
isMainPlayer,
|
isMainPlayer,
|
||||||
@ -356,7 +524,7 @@ export default () => {
|
|||||||
destroyService,
|
destroyService,
|
||||||
isResponseCached,
|
isResponseCached,
|
||||||
getDataFromResponse,
|
getDataFromResponse,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import createScoresService from './scores'
|
import createScoresService from "./scores";
|
||||||
import makePendingPromisePool from '../../utils/pending-promises'
|
import makePendingPromisePool from "../../utils/pending-promises";
|
||||||
import {getTotalPpFromSortedPps} from '../../utils/scoresaber/pp'
|
import { getTotalPpFromSortedPps } from "../../utils/scoresaber/pp";
|
||||||
|
|
||||||
let service = null;
|
let service = null;
|
||||||
let serviceCreationCount = 0;
|
let serviceCreationCount = 0;
|
||||||
@ -12,18 +12,22 @@ export default () => {
|
|||||||
|
|
||||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||||
|
|
||||||
const getTotalPp = scores => scores && Array.isArray(scores)
|
const getTotalPp = (scores) =>
|
||||||
|
scores && Array.isArray(scores)
|
||||||
? getTotalPpFromSortedPps(
|
? getTotalPpFromSortedPps(
|
||||||
scores
|
scores
|
||||||
.filter(s => s.pp > 0)
|
.filter((s) => s.pp > 0)
|
||||||
.map(s => s.pp)
|
.map((s) => s.pp)
|
||||||
.sort((a, b) => b - a),
|
.sort((a, b) => b - a),
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const getTotalPlayerPp = async (playerId, modifiedScores = {}) => getTotalPp(
|
const getTotalPlayerPp = async (playerId, modifiedScores = {}) =>
|
||||||
|
getTotalPp(
|
||||||
Object.values({
|
Object.values({
|
||||||
...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () => scoresService.getPlayerScoresAsObject(playerId))),
|
...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () =>
|
||||||
|
scoresService.getPlayerScoresAsObject(playerId),
|
||||||
|
)),
|
||||||
...modifiedScores,
|
...modifiedScores,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -67,7 +71,7 @@ export default () => {
|
|||||||
if (!acc || acc <= 0) {
|
if (!acc || acc <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let index = ppCurve.findIndex(o => o.at >= acc);
|
let index = ppCurve.findIndex((o) => o.at >= acc);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
return ppCurve[ppCurve.length - 1].value;
|
return ppCurve[ppCurve.length - 1].value;
|
||||||
}
|
}
|
||||||
@ -83,7 +87,7 @@ export default () => {
|
|||||||
function accFromPpFactor(ppFactor) {
|
function accFromPpFactor(ppFactor) {
|
||||||
if (!ppFactor || ppFactor <= 0) return 0;
|
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;
|
if (idx < 0) return ppCurve[ppCurve.length - 1].at;
|
||||||
|
|
||||||
const from = ppCurve[idx - 1];
|
const from = ppCurve[idx - 1];
|
||||||
@ -101,7 +105,7 @@ export default () => {
|
|||||||
|
|
||||||
service = null;
|
service = null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
getWhatIfScore,
|
getWhatIfScore,
|
||||||
@ -111,7 +115,7 @@ export default () => {
|
|||||||
accFromPpFactor,
|
accFromPpFactor,
|
||||||
PP_PER_STAR,
|
PP_PER_STAR,
|
||||||
destroyService,
|
destroyService,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
};
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import {db} from '../../db/db'
|
import { db } from "../../db/db";
|
||||||
import queues from '../../network/queues/queues';
|
import queues from "../../network/queues/queues";
|
||||||
import rankedsPageClient from '../../network/clients/scoresaber/rankeds/page';
|
import rankedsPageClient from "../../network/clients/scoresaber/rankeds/page";
|
||||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||||
import {arrayDifference, convertArrayToObjectByKey, opt} from '../../utils/js'
|
import {
|
||||||
import rankedsRepository from '../../db/repository/rankeds'
|
arrayDifference,
|
||||||
import rankedsChangesRepository from '../../db/repository/rankeds-changes'
|
convertArrayToObjectByKey,
|
||||||
import keyValueRepository from '../../db/repository/key-value'
|
opt,
|
||||||
import log from '../../utils/logger'
|
} from "../../utils/js";
|
||||||
import {addToDate, formatDate, HOUR} from '../../utils/date'
|
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;
|
const REFRESH_INTERVAL = HOUR;
|
||||||
|
|
||||||
@ -16,16 +20,27 @@ export default () => {
|
|||||||
if (service) return service;
|
if (service) return service;
|
||||||
|
|
||||||
const getRankeds = async () => {
|
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 getLastUpdated = async () =>
|
||||||
const setLastUpdated = async date => keyValueRepository().set(date, 'rankedsLastUpdated');
|
keyValueRepository().get("rankedsLastUpdated");
|
||||||
|
const setLastUpdated = async (date) =>
|
||||||
|
keyValueRepository().set(date, "rankedsLastUpdated");
|
||||||
|
|
||||||
const refreshRankeds = async (forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
|
const refreshRankeds = async (
|
||||||
log.trace(`Starting rankeds refreshing${forceUpdate ? ' (forced)' : ''}...`, 'RankedsService')
|
forceUpdate = false,
|
||||||
|
priority = queues.PRIORITY.BG_NORMAL,
|
||||||
|
throwErrors = false,
|
||||||
|
) => {
|
||||||
|
log.trace(
|
||||||
|
`Starting rankeds refreshing${forceUpdate ? " (forced)" : ""}...`,
|
||||||
|
"RankedsService",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let fetchedRankedSongs;
|
let fetchedRankedSongs;
|
||||||
@ -33,40 +48,50 @@ export default () => {
|
|||||||
if (!forceUpdate) {
|
if (!forceUpdate) {
|
||||||
const lastUpdated = await getLastUpdated();
|
const lastUpdated = await getLastUpdated();
|
||||||
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.trace(`Fetching current rankeds from ScoreSaber...`, 'RankedsService')
|
log.trace(
|
||||||
|
`Fetching current rankeds from ScoreSaber...`,
|
||||||
|
"RankedsService",
|
||||||
|
);
|
||||||
fetchedRankedSongs = await rankedsPageClient.getProcessed({ priority });
|
fetchedRankedSongs = await rankedsPageClient.getProcessed({ priority });
|
||||||
if (!fetchedRankedSongs || !fetchedRankedSongs.length) {
|
if (!fetchedRankedSongs || !fetchedRankedSongs.length) {
|
||||||
log.warn(`ScoreSaber returned empty rankeds list`, 'RankedsService')
|
log.warn(`ScoreSaber returned empty rankeds list`, "RankedsService");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.trace('Fetching rankeds from DB', 'RankedsService');
|
log.trace("Fetching rankeds from DB", "RankedsService");
|
||||||
const oldRankedSongs = await getRankeds();
|
const oldRankedSongs = await getRankeds();
|
||||||
|
|
||||||
// add firstSeen & oldStars properties
|
// add firstSeen & oldStars properties
|
||||||
fetchedRankedSongs = convertArrayToObjectByKey(
|
fetchedRankedSongs = convertArrayToObjectByKey(
|
||||||
fetchedRankedSongs.map(s => {
|
fetchedRankedSongs.map((s) => {
|
||||||
const firstSeen = oldRankedSongs[s.leaderboardId] && oldRankedSongs[s.leaderboardId].firstSeen
|
const firstSeen =
|
||||||
|
oldRankedSongs[s.leaderboardId] &&
|
||||||
|
oldRankedSongs[s.leaderboardId].firstSeen
|
||||||
? oldRankedSongs[s.leaderboardId].firstSeen
|
? oldRankedSongs[s.leaderboardId].firstSeen
|
||||||
: new Date();
|
: new Date();
|
||||||
|
|
||||||
return {...s, firstSeen, oldStars: null}
|
return { ...s, firstSeen, oldStars: null };
|
||||||
}),
|
}),
|
||||||
'leaderboardId',
|
"leaderboardId",
|
||||||
);
|
);
|
||||||
|
|
||||||
// find differences between old and new ranked songs
|
// find differences between old and new ranked songs
|
||||||
const newRankeds = arrayDifference(
|
const newRankeds = arrayDifference(
|
||||||
Object.keys(fetchedRankedSongs),
|
Object.keys(fetchedRankedSongs),
|
||||||
Object.keys(oldRankedSongs),
|
Object.keys(oldRankedSongs),
|
||||||
).map(leaderboardId => ({
|
).map((leaderboardId) => ({
|
||||||
leaderboardId: parseInt(leaderboardId, 10),
|
leaderboardId: parseInt(leaderboardId, 10),
|
||||||
oldStars: null,
|
oldStars: null,
|
||||||
stars: fetchedRankedSongs[leaderboardId].stars,
|
stars: fetchedRankedSongs[leaderboardId].stars,
|
||||||
@ -74,77 +99,99 @@ export default () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (newRankeds && newRankeds.length)
|
if (newRankeds && newRankeds.length)
|
||||||
log.debug(`${newRankeds.length} ranked(s) found`, 'RankedsService');
|
log.debug(`${newRankeds.length} ranked(s) found`, "RankedsService");
|
||||||
|
|
||||||
const changed =
|
const changed =
|
||||||
// concat new rankeds with changed rankeds
|
// concat new rankeds with changed rankeds
|
||||||
newRankeds
|
newRankeds.concat(
|
||||||
.concat(
|
|
||||||
Object.values(oldRankedSongs)
|
Object.values(oldRankedSongs)
|
||||||
.filter(s => s.stars !== (fetchedRankedSongs[s.leaderboardId] ? opt(fetchedRankedSongs[s.leaderboardId], 'stars', null) : null))
|
.filter(
|
||||||
.map(s => ({
|
(s) =>
|
||||||
|
s.stars !==
|
||||||
|
(fetchedRankedSongs[s.leaderboardId]
|
||||||
|
? opt(fetchedRankedSongs[s.leaderboardId], "stars", null)
|
||||||
|
: null),
|
||||||
|
)
|
||||||
|
.map((s) => ({
|
||||||
leaderboardId: s.leaderboardId,
|
leaderboardId: s.leaderboardId,
|
||||||
oldStars: s.stars,
|
oldStars: s.stars,
|
||||||
stars: opt(fetchedRankedSongs[s.leaderboardId], 'stars', null),
|
stars: opt(fetchedRankedSongs[s.leaderboardId], "stars", null),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}),
|
})),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newRankeds && changed && changed.length - newRankeds.length > 0)
|
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
|
const changedLeaderboards = changed
|
||||||
.map(s => {
|
.map((s) => {
|
||||||
const ranked = fetchedRankedSongs[s.leaderboardId] ? fetchedRankedSongs[s.leaderboardId] : oldRankedSongs[s.leaderboardId];
|
const ranked = fetchedRankedSongs[s.leaderboardId]
|
||||||
|
? fetchedRankedSongs[s.leaderboardId]
|
||||||
|
: oldRankedSongs[s.leaderboardId];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...ranked,
|
...ranked,
|
||||||
...s,
|
...s,
|
||||||
}
|
};
|
||||||
},
|
})
|
||||||
)
|
.filter((s) => s && s.hash)
|
||||||
.filter(s => s && s.hash)
|
.map((l) => {
|
||||||
.map(l => {
|
|
||||||
const { oldStars, timestamp, ...leaderboard } = l;
|
const { oldStars, timestamp, ...leaderboard } = l;
|
||||||
return leaderboard;
|
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 db.runInTransaction(
|
||||||
await Promise.all(changedLeaderboards.map(async ranked => rankedsRepository().set(ranked, undefined, tx)));
|
["rankeds", "rankeds-changes", "key-value"],
|
||||||
await Promise.all(changed.map(async rc => rankedsChangesRepository().set(rc, undefined, tx)));
|
async (tx) => {
|
||||||
await setLastUpdated(new Date())
|
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) {
|
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;
|
return changed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (throwErrors) throw e;
|
if (throwErrors) throw e;
|
||||||
|
|
||||||
log.debug(`Rankeds refreshing error`, 'RankedsService', e)
|
log.debug(`Rankeds refreshing error`, "RankedsService", e);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const destroyService = () => {
|
const destroyService = () => {
|
||||||
service = null;
|
service = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
get: getRankeds,
|
get: getRankeds,
|
||||||
refresh: refreshRankeds,
|
refresh: refreshRankeds,
|
||||||
destroyService,
|
destroyService,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import playersGlobalRankingApiClient from '../../network/clients/scoresaber/players/api-ranking-global'
|
import playersGlobalRankingApiClient from "../../network/clients/scoresaber/players/api-ranking-global";
|
||||||
import playersGlobalRankingPagesApiClient from '../../network/clients/scoresaber/players/api-ranking-global-pages'
|
import playersGlobalRankingPagesApiClient from "../../network/clients/scoresaber/players/api-ranking-global-pages";
|
||||||
import playersCountryRankingPageClient from '../../network/clients/scoresaber/players/page-ranking-country'
|
import playersCountryRankingPageClient from "../../network/clients/scoresaber/players/page-ranking-country";
|
||||||
import makePendingPromisePool from '../../utils/pending-promises'
|
import makePendingPromisePool from "../../utils/pending-promises";
|
||||||
import {PRIORITY} from '../../network/queues/http-queue'
|
import { PRIORITY } from "../../network/queues/http-queue";
|
||||||
import {PLAYERS_PER_PAGE} from '../../utils/scoresaber/consts'
|
import { PLAYERS_PER_PAGE } from "../../utils/scoresaber/consts";
|
||||||
import {opt} from '../../utils/js'
|
import { opt } from "../../utils/js";
|
||||||
|
|
||||||
let service = null;
|
let service = null;
|
||||||
export default () => {
|
export default () => {
|
||||||
@ -12,24 +12,52 @@ export default () => {
|
|||||||
|
|
||||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
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);
|
const pages = await fetchGlobalPages(priority, signal);
|
||||||
if (!pages || !Number.isFinite(pages)) return 0;
|
if (!pages || !Number.isFinite(pages)) return 0;
|
||||||
|
|
||||||
return pages * PLAYERS_PER_PAGE;
|
return pages * PLAYERS_PER_PAGE;
|
||||||
}
|
};
|
||||||
|
|
||||||
async function fetchMiniRanking(rank, country = null, numOfPlayers = 5) {
|
async function fetchMiniRanking(rank, country = null, numOfPlayers = 5) {
|
||||||
try {
|
try {
|
||||||
if (!Number.isFinite(numOfPlayers)) numOfPlayers = 5;
|
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);
|
const playerPage = getPage(rank);
|
||||||
let firstPlayerRank = rank - (numOfPlayers - (numOfPlayers > 2 ? 2 : 1));
|
let firstPlayerRank = rank - (numOfPlayers - (numOfPlayers > 2 ? 2 : 1));
|
||||||
@ -38,15 +66,23 @@ export default () => {
|
|||||||
const lastPlayerRank = firstPlayerRank + numOfPlayers - 1;
|
const lastPlayerRank = firstPlayerRank + numOfPlayers - 1;
|
||||||
const lastPlayerRankPage = getPage(lastPlayerRank);
|
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), [])
|
.reduce((cum, arr) => cum.concat(arr), [])
|
||||||
.filter(player => {
|
.filter((player) => {
|
||||||
const rank = opt(player, 'playerInfo.rank')
|
const rank = opt(player, "playerInfo.rank");
|
||||||
return rank >= firstPlayerRank && rank <= lastPlayerRank;
|
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;
|
return ranking;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -56,7 +92,7 @@ export default () => {
|
|||||||
|
|
||||||
const destroyService = () => {
|
const destroyService = () => {
|
||||||
service = null;
|
service = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
getGlobal: fetchGlobal,
|
getGlobal: fetchGlobal,
|
||||||
@ -66,7 +102,7 @@ export default () => {
|
|||||||
getMiniRanking: fetchMiniRanking,
|
getMiniRanking: fetchMiniRanking,
|
||||||
PLAYERS_PER_PAGE,
|
PLAYERS_PER_PAGE,
|
||||||
destroyService,
|
destroyService,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,23 @@
|
|||||||
import queues from '../network/queues/queues';
|
import queues from "../network/queues/queues";
|
||||||
import keyValueRepository from '../db/repository/key-value'
|
import keyValueRepository from "../db/repository/key-value";
|
||||||
import twitchRepository from '../db/repository/twitch'
|
import twitchRepository from "../db/repository/twitch";
|
||||||
import createPlayerService from '../services/scoresaber/player'
|
import createPlayerService from "../services/scoresaber/player";
|
||||||
import profileApiClient from '../network/clients/twitch/api-profile'
|
import profileApiClient from "../network/clients/twitch/api-profile";
|
||||||
import videosApiClient from '../network/clients/twitch/api-videos'
|
import videosApiClient from "../network/clients/twitch/api-videos";
|
||||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||||
import log from '../utils/logger'
|
import log from "../utils/logger";
|
||||||
import {addToDate, dateFromString, durationToMillis, formatDate, millisToDuration, MINUTE} from '../utils/date'
|
import {
|
||||||
import {PRIORITY} from '../network/queues/http-queue'
|
addToDate,
|
||||||
import makePendingPromisePool from '../utils/pending-promises'
|
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;
|
const REFRESH_INTERVAL = 5 * MINUTE;
|
||||||
|
|
||||||
@ -24,31 +31,38 @@ export default () => {
|
|||||||
|
|
||||||
const playerService = createPlayerService();
|
const playerService = createPlayerService();
|
||||||
|
|
||||||
const getAuthUrl = (state = '', scopes = '') => queues.TWITCH.getAuthUrl(state, scopes)
|
const getAuthUrl = (state = "", scopes = "") =>
|
||||||
|
queues.TWITCH.getAuthUrl(state, scopes);
|
||||||
|
|
||||||
const getTwitchTokenFromUrl = () => {
|
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) {
|
if (error) {
|
||||||
const errorMsg = url.searchParams.get('error_description');
|
const errorMsg = url.searchParams.get("error_description");
|
||||||
throw new Error(errorMsg ? errorMsg : error);
|
throw new Error(errorMsg ? errorMsg : error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = url.hash;
|
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);
|
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);
|
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
|
// validate token
|
||||||
const tokenValidation = (await queues.TWITCH.validateToken(accessToken)).body;
|
const tokenValidation = (await queues.TWITCH.validateToken(accessToken))
|
||||||
|
.body;
|
||||||
|
|
||||||
const expiresIn = tokenValidation.expires_in * 1000;
|
const expiresIn = tokenValidation.expires_in * 1000;
|
||||||
|
|
||||||
@ -58,39 +72,77 @@ export default () => {
|
|||||||
obtained: new Date(),
|
obtained: new Date(),
|
||||||
expires: new Date(Date.now() + expiresIn),
|
expires: new Date(Date.now() + expiresIn),
|
||||||
expires_in: expiresIn,
|
expires_in: expiresIn,
|
||||||
}
|
};
|
||||||
|
|
||||||
await keyValueRepository().set(twitchToken, TWITCH_TOKEN_KEY);
|
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();
|
const token = await getCurrentToken();
|
||||||
if (!token || !token.expires || token.expires <= new Date()) return null;
|
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();
|
const token = await getCurrentToken();
|
||||||
if (!token || !token.expires || token.expires <= new Date()) return null;
|
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 getPlayerProfile = async (playerId) => twitchRepository().get(playerId);
|
||||||
const updatePlayerProfile = async twitchProfile => twitchRepository().set(twitchProfile);
|
const updatePlayerProfile = async (twitchProfile) =>
|
||||||
|
twitchRepository().set(twitchProfile);
|
||||||
|
|
||||||
const refresh = async (playerId, forceUpdate = false, priority = queues.PRIORITY.FG_LOW, throwErrors = false) => {
|
const refresh = async (
|
||||||
log.trace(`Starting Twitch videos refreshing${forceUpdate ? ' (forced)' : ''}...`, 'TwitchService')
|
playerId,
|
||||||
|
forceUpdate = false,
|
||||||
|
priority = queues.PRIORITY.FG_LOW,
|
||||||
|
throwErrors = false,
|
||||||
|
) => {
|
||||||
|
log.trace(
|
||||||
|
`Starting Twitch videos refreshing${forceUpdate ? " (forced)" : ""}...`,
|
||||||
|
"TwitchService",
|
||||||
|
);
|
||||||
|
|
||||||
if (!playerId) {
|
if (!playerId) {
|
||||||
log.debug(`No playerId provided, skipping`, 'TwitchService')
|
log.debug(`No playerId provided, skipping`, "TwitchService");
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -98,7 +150,10 @@ export default () => {
|
|||||||
try {
|
try {
|
||||||
let twitchProfile = await twitchRepository().get(playerId);
|
let twitchProfile = await twitchRepository().get(playerId);
|
||||||
if (!twitchProfile || !twitchProfile.login) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@ -106,7 +161,12 @@ export default () => {
|
|||||||
const lastUpdated = twitchProfile.lastUpdated;
|
const lastUpdated = twitchProfile.lastUpdated;
|
||||||
if (!forceUpdate) {
|
if (!forceUpdate) {
|
||||||
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
|
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;
|
return twitchProfile;
|
||||||
}
|
}
|
||||||
@ -115,7 +175,10 @@ export default () => {
|
|||||||
const player = playerService.get(playerId);
|
const player = playerService.get(playerId);
|
||||||
if (player && player.recentPlay) {
|
if (player && player.recentPlay) {
|
||||||
if (lastUpdated && lastUpdated > 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;
|
return twitchProfile;
|
||||||
}
|
}
|
||||||
@ -124,7 +187,10 @@ export default () => {
|
|||||||
if (!twitchProfile.id) {
|
if (!twitchProfile.id) {
|
||||||
const fetchedProfile = await fetchProfile(twitchProfile.login);
|
const fetchedProfile = await fetchProfile(twitchProfile.login);
|
||||||
if (!fetchedProfile) {
|
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;
|
return twitchProfile;
|
||||||
}
|
}
|
||||||
@ -141,7 +207,7 @@ export default () => {
|
|||||||
await updatePlayerProfile(twitchProfile);
|
await updatePlayerProfile(twitchProfile);
|
||||||
|
|
||||||
if (videos && videos.length) {
|
if (videos && videos.length) {
|
||||||
eventBus.publish('player-twitch-videos-updated', {
|
eventBus.publish("player-twitch-videos-updated", {
|
||||||
playerId,
|
playerId,
|
||||||
twitchProfile,
|
twitchProfile,
|
||||||
});
|
});
|
||||||
@ -151,25 +217,46 @@ export default () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (throwErrors) throw 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async function findTwitchVideo(playerTwitchProfile, timeset, songLength) {
|
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
|
const video = playerTwitchProfile.videos
|
||||||
.map(v => ({
|
.map((v) => ({
|
||||||
...v,
|
...v,
|
||||||
created_at: dateFromString(v.created_at),
|
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 = () => {
|
const destroyService = () => {
|
||||||
@ -179,7 +266,7 @@ export default () => {
|
|||||||
service = null;
|
service = null;
|
||||||
playerService.destroyService();
|
playerService.destroyService();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
getAuthUrl,
|
getAuthUrl,
|
||||||
@ -192,7 +279,7 @@ export default () => {
|
|||||||
findTwitchVideo,
|
findTwitchVideo,
|
||||||
refresh,
|
refresh,
|
||||||
destroyService,
|
destroyService,
|
||||||
}
|
};
|
||||||
|
|
||||||
return service;
|
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;
|
if (!playerHistory?.length) return null;
|
||||||
|
|
||||||
let todayServiceMidnightDate = dateTruncFunc(new Date());
|
let todayServiceMidnightDate = dateTruncFunc(new Date());
|
||||||
|
|
||||||
const compareToDate = DateTime.fromJSDate(todayServiceMidnightDate).minus({days: daysAgo}).toJSDate();
|
const compareToDate = DateTime.fromJSDate(todayServiceMidnightDate)
|
||||||
const maxServiceDate = DateTime.fromJSDate(todayServiceMidnightDate).minus({days: maxDaysAgo}).toJSDate();
|
.minus({ days: daysAgo })
|
||||||
|
.toJSDate();
|
||||||
|
const maxServiceDate = DateTime.fromJSDate(todayServiceMidnightDate)
|
||||||
|
.minus({ days: maxDaysAgo })
|
||||||
|
.toJSDate();
|
||||||
|
|
||||||
return playerHistory
|
return playerHistory
|
||||||
.sort((a, b) => b?.[dateKey]?.getTime() - a?.[dateKey]?.getTime())
|
.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
|
// 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;
|
let filterVal = true;
|
||||||
|
|
||||||
if (serviceParams?.filters?.search?.length) {
|
if (serviceParams?.filters?.search?.length) {
|
||||||
const song = s?.leaderboard?.song ?? null;
|
const song = s?.leaderboard?.song ?? null;
|
||||||
if (song) {
|
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 {
|
} else {
|
||||||
filterVal &= false;
|
filterVal &= false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceParams?.filters.diff?.length) {
|
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) {
|
if (serviceParams?.filters?.songType?.length) {
|
||||||
filterVal &= (serviceParams.filters.songType === 'ranked' && s?.pp > 0) ||
|
filterVal &=
|
||||||
(serviceParams.filters.songType === 'unranked' && (s?.pp ?? 0) === 0)
|
(serviceParams.filters.songType === "ranked" && s?.pp > 0) ||
|
||||||
|
(serviceParams.filters.songType === "unranked" && (s?.pp ?? 0) === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterVal;
|
return filterVal;
|
||||||
}
|
};
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
import {writable} from 'svelte/store'
|
import { writable } from "svelte/store";
|
||||||
import keyValueRepository from '../db/repository/key-value';
|
import keyValueRepository from "../db/repository/key-value";
|
||||||
import {opt} from '../utils/js'
|
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;
|
export let configStore = null;
|
||||||
|
|
||||||
const locales = {
|
const locales = {
|
||||||
'de-DE': {id: 'de-DE', name: 'Deutschland'},
|
"de-DE": { id: "de-DE", name: "Deutschland" },
|
||||||
'es-ES': {id: 'es-ES', name: 'España'},
|
"es-ES": { id: "es-ES", name: "España" },
|
||||||
'pl-PL': {id: 'pl-PL', name: 'Polska'},
|
"pl-PL": { id: "pl-PL", name: "Polska" },
|
||||||
'en-GB': {id: 'en-GB', name: 'United Kingdom'},
|
"en-GB": { id: "en-GB", name: "United Kingdom" },
|
||||||
'en-US': {id: 'en-US', name: 'United States'},
|
"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);
|
export const getSupportedLocales = () => Object.values(locales);
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
@ -24,21 +25,21 @@ const DEFAULT_CONFIG = {
|
|||||||
country: null,
|
country: null,
|
||||||
},
|
},
|
||||||
scoreComparison: {
|
scoreComparison: {
|
||||||
method: 'in-place',
|
method: "in-place",
|
||||||
},
|
},
|
||||||
preferences: {
|
preferences: {
|
||||||
secondaryPp: 'attribution',
|
secondaryPp: "attribution",
|
||||||
avatarIcons: 'only-if-needed',
|
avatarIcons: "only-if-needed",
|
||||||
},
|
},
|
||||||
locale: DEFAULT_LOCALE,
|
locale: DEFAULT_LOCALE,
|
||||||
}
|
};
|
||||||
|
|
||||||
const newSettingsAvailableDefinition = {
|
const newSettingsAvailableDefinition = {
|
||||||
'scoreComparison.method': 'Method of displaying the comparison of scores',
|
"scoreComparison.method": "Method of displaying the comparison of scores",
|
||||||
'preferences.secondaryPp': 'Setting the second PP metric',
|
"preferences.secondaryPp": "Setting the second PP metric",
|
||||||
'preferences.avatarIcons': 'Showing icons on avatars',
|
"preferences.avatarIcons": "Showing icons on avatars",
|
||||||
'locale': 'Locale selection',
|
locale: "Locale selection",
|
||||||
}
|
};
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
if (configStore) return configStore;
|
if (configStore) return configStore;
|
||||||
@ -49,16 +50,17 @@ export default async () => {
|
|||||||
|
|
||||||
const { subscribe, set: storeSet } = writable(currentConfig);
|
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 set = async (config, persist = true) => {
|
||||||
const newConfig = { ...DEFAULT_CONFIG };
|
const newConfig = { ...DEFAULT_CONFIG };
|
||||||
Object.keys(config).forEach(key => {
|
Object.keys(config).forEach((key) => {
|
||||||
if (key === 'locale') {
|
if (key === "locale") {
|
||||||
newConfig[key] = config?.[key] ?? newConfig?.[key] ?? DEFAULT_LOCALE;
|
newConfig[key] = config?.[key] ?? newConfig?.[key] ?? DEFAULT_LOCALE;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig[key] = {...newConfig?.[key], ...config?.[key]}
|
newConfig[key] = { ...newConfig?.[key], ...config?.[key] };
|
||||||
});
|
});
|
||||||
|
|
||||||
if (persist) await keyValueRepository().set(newConfig, STORE_CONFIG_KEY);
|
if (persist) await keyValueRepository().set(newConfig, STORE_CONFIG_KEY);
|
||||||
@ -69,27 +71,31 @@ export default async () => {
|
|||||||
storeSet(newConfig);
|
storeSet(newConfig);
|
||||||
|
|
||||||
return newConfig;
|
return newConfig;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getLocale = () => opt(currentConfig, 'locale', DEFAULT_LOCALE);
|
const getLocale = () => opt(currentConfig, "locale", DEFAULT_LOCALE);
|
||||||
|
|
||||||
const determineNewSettingsAvailable = dbConfig => Object.entries(newSettingsAvailableDefinition)
|
const determineNewSettingsAvailable = (dbConfig) =>
|
||||||
.map(([key, description]) => opt(dbConfig, key) === undefined ? description : null)
|
Object.entries(newSettingsAvailableDefinition)
|
||||||
.filter(d => d)
|
.map(([key, description]) =>
|
||||||
|
opt(dbConfig, key) === undefined ? description : null,
|
||||||
|
)
|
||||||
|
.filter((d) => d);
|
||||||
|
|
||||||
const dbConfig = await keyValueRepository().get(STORE_CONFIG_KEY);
|
const dbConfig = await keyValueRepository().get(STORE_CONFIG_KEY);
|
||||||
const newSettings = determineNewSettingsAvailable(dbConfig);
|
const newSettings = determineNewSettingsAvailable(dbConfig);
|
||||||
if (dbConfig) await set(dbConfig, false);
|
if (dbConfig) await set(dbConfig, false);
|
||||||
newSettingsAvailable = newSettings && newSettings.length ? newSettings : undefined;
|
newSettingsAvailable =
|
||||||
|
newSettings && newSettings.length ? newSettings : undefined;
|
||||||
|
|
||||||
configStore = {
|
configStore = {
|
||||||
subscribe,
|
subscribe,
|
||||||
set,
|
set,
|
||||||
get,
|
get,
|
||||||
getMainPlayerId: () => opt(currentConfig, 'users.main'),
|
getMainPlayerId: () => opt(currentConfig, "users.main"),
|
||||||
getLocale,
|
getLocale,
|
||||||
getNewSettingsAvailable: () => newSettingsAvailable,
|
getNewSettingsAvailable: () => newSettingsAvailable,
|
||||||
}
|
};
|
||||||
|
|
||||||
return configStore;
|
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}) => {
|
export default (
|
||||||
const defaultValue = {name: null, width: null, nodeWidth: null, rect: null}
|
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);
|
const { subscribe, unsubscribe, set } = writable(defaultValue);
|
||||||
|
|
||||||
let ro = null;
|
let ro = null;
|
||||||
@ -10,12 +12,12 @@ export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => {
|
|||||||
const unobserve = () => {
|
const unobserve = () => {
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
ro.unobserve(node)
|
ro.unobserve(node);
|
||||||
|
|
||||||
node = null;
|
node = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
const observe = nodeToObserve => {
|
const observe = (nodeToObserve) => {
|
||||||
if (!nodeToObserve) return null;
|
if (!nodeToObserve) return null;
|
||||||
|
|
||||||
if (node) unobserve();
|
if (node) unobserve();
|
||||||
@ -34,19 +36,25 @@ export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => {
|
|||||||
set(
|
set(
|
||||||
Object.entries(sizes)
|
Object.entries(sizes)
|
||||||
.sort((a, b) => a[1] - b[1])
|
.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 node;
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
unsubscribe,
|
unsubscribe,
|
||||||
observe,
|
observe,
|
||||||
unobserve,
|
unobserve,
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
import {getFixedLeaderboardMaxScore, getMaxScore} from '../../../../utils/scoresaber/song'
|
import {
|
||||||
|
getFixedLeaderboardMaxScore,
|
||||||
|
getMaxScore,
|
||||||
|
} from "../../../../utils/scoresaber/song";
|
||||||
|
|
||||||
export default (score, bmStats, leaderboardId) => {
|
export default (score, bmStats, leaderboardId) => {
|
||||||
let maxScore;
|
let maxScore;
|
||||||
|
|
||||||
if (bmStats && bmStats.notes) {
|
if (bmStats && bmStats.notes) {
|
||||||
maxScore = getMaxScore(bmStats.notes)
|
maxScore = getMaxScore(bmStats.notes);
|
||||||
} else if (leaderboardId) {
|
} else if (leaderboardId) {
|
||||||
maxScore = getFixedLeaderboardMaxScore(leaderboardId, score?.maxScore ?? null)
|
maxScore = getFixedLeaderboardMaxScore(
|
||||||
|
leaderboardId,
|
||||||
|
score?.maxScore ?? null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxScore) {
|
if (maxScore) {
|
||||||
@ -17,14 +23,14 @@ export default (score, bmStats, leaderboardId) => {
|
|||||||
if (!unmodifiedScore) unmodifiedScore = score?.score ?? null;
|
if (!unmodifiedScore) unmodifiedScore = score?.score ?? null;
|
||||||
|
|
||||||
if (unmodifiedScore && score.maxScore) {
|
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) {
|
if (score?.score && score?.maxScore) {
|
||||||
score.percentage = score.score / score.maxScore * 100;
|
score.percentage = (score.score / score.maxScore) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
return score;
|
return score;
|
||||||
}
|
};
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import createBeatMapsService from '../../../../services/beatmaps'
|
import createBeatMapsService from "../../../../services/beatmaps";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
|
|
||||||
const beatMaps = createBeatMapsService();
|
const beatMaps = createBeatMapsService();
|
||||||
|
|
||||||
export default async (data, cachedOnly = false) => {
|
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 createRankedsStore from "../../../../stores/scoresaber/rankeds";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
|
|
||||||
let rankeds;
|
let rankeds;
|
||||||
|
|
||||||
@ -10,8 +10,11 @@ export default async (data) => {
|
|||||||
|
|
||||||
if (!rankeds) return;
|
if (!rankeds) return;
|
||||||
|
|
||||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
|
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||||
if (!leaderboardId) return;
|
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 { opt } from "../../../../utils/js";
|
||||||
import calculateAcc from '../common/acc-calc'
|
import calculateAcc from "../common/acc-calc";
|
||||||
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
|
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
|
||||||
|
|
||||||
export default async (data) => {
|
export default async (data) => {
|
||||||
if (!data || !data.score) return;
|
if (!data || !data.score) return;
|
||||||
|
|
||||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId')
|
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||||
const diffInfo = opt(data, 'leaderboard.diffInfo');
|
const diffInfo = opt(data, "leaderboard.diffInfo");
|
||||||
|
|
||||||
const versions = opt(data, 'leaderboard.beatMaps.versions')
|
const versions = opt(data, "leaderboard.beatMaps.versions");
|
||||||
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
|
const versionsLastIdx =
|
||||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), diffInfo);
|
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);
|
data.score = calculateAcc(data.score, bmStats, leaderboardId);
|
||||||
}
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import createBeatSaviorService from '../../../../services/beatsavior'
|
import createBeatSaviorService from "../../../../services/beatsavior";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
import {PRIORITY} from '../../../../network/queues/http-queue'
|
import { PRIORITY } from "../../../../network/queues/http-queue";
|
||||||
|
|
||||||
let beatSaviorService;
|
let beatSaviorService;
|
||||||
|
|
||||||
@ -15,19 +15,19 @@ export default async (data, playerId = null) => {
|
|||||||
if (!bsData) return;
|
if (!bsData) return;
|
||||||
|
|
||||||
if (bsData?.stats)
|
if (bsData?.stats)
|
||||||
['left', 'right'].forEach(hand => {
|
["left", "right"].forEach((hand) => {
|
||||||
['Preswing', 'Postswing'].forEach(stat => {
|
["Preswing", "Postswing"].forEach((stat) => {
|
||||||
const key = `${hand}${stat}`;
|
const key = `${hand}${stat}`;
|
||||||
if (!bsData?.stats?.[key])
|
if (!bsData?.stats?.[key])
|
||||||
bsData.stats[key] = bsData?.trackers?.accuracyTracker?.[key] ?? null;
|
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;
|
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;
|
if (percentage) data.score.percentage = percentage * 100;
|
||||||
|
|
||||||
data.beatSavior = bsData;
|
data.beatSavior = bsData;
|
||||||
}
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import {configStore} from '../../../config'
|
import { configStore } from "../../../config";
|
||||||
import createScoresService from '../../../../services/scoresaber/scores'
|
import createScoresService from "../../../../services/scoresaber/scores";
|
||||||
import accEnhancer from './acc'
|
import accEnhancer from "./acc";
|
||||||
import beatSaviorEnhancer from './beatsavior'
|
import beatSaviorEnhancer from "./beatsavior";
|
||||||
import beatMapsEnhancer from '../common/beatmaps'
|
import beatMapsEnhancer from "../common/beatmaps";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
import produce from 'immer'
|
import produce from "immer";
|
||||||
|
|
||||||
let scoresService = null;
|
let scoresService = null;
|
||||||
let mainPlayerId = null;
|
let mainPlayerId = null;
|
||||||
@ -17,20 +17,29 @@ export const initCompareEnhancer = async () => {
|
|||||||
|
|
||||||
scoresService = createScoresService();
|
scoresService = createScoresService();
|
||||||
|
|
||||||
configStoreUnsubscribe = configStore.subscribe(async config => {
|
configStoreUnsubscribe = configStore.subscribe(async (config) => {
|
||||||
const newMainPlayerId = opt(config, 'users.main')
|
const newMainPlayerId = opt(config, "users.main");
|
||||||
if (mainPlayerId !== newMainPlayerId) {
|
if (mainPlayerId !== newMainPlayerId) {
|
||||||
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) => {
|
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;
|
if (!leaderboardId) return;
|
||||||
|
|
||||||
const comparePlayerScores = await playerScores[mainPlayerId];
|
const comparePlayerScores = await playerScores[mainPlayerId];
|
||||||
@ -38,14 +47,15 @@ export default async (data, playerId = null) => {
|
|||||||
|
|
||||||
const mainPlayerScore = await produce(
|
const mainPlayerScore = await produce(
|
||||||
await produce(
|
await produce(
|
||||||
await produce(
|
await produce(comparePlayerScores[leaderboardId], (draft) =>
|
||||||
comparePlayerScores[leaderboardId],
|
beatMapsEnhancer(draft),
|
||||||
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 createScoresService from "../../../../services/scoresaber/scores";
|
||||||
import calculateAcc from '../common/acc-calc'
|
import calculateAcc from "../common/acc-calc";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
|
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
|
||||||
|
|
||||||
let scoresService;
|
let scoresService;
|
||||||
|
|
||||||
@ -10,27 +10,39 @@ export default async (data, playerId = null) => {
|
|||||||
|
|
||||||
if (data.prevScore) delete data.prevScore;
|
if (data.prevScore) delete data.prevScore;
|
||||||
|
|
||||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
|
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||||
|
|
||||||
if (!scoresService) scoresService = createScoresService();
|
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
|
// skip if no cached score
|
||||||
if (!playerScores[leaderboardId]) return;
|
if (!playerScores[leaderboardId]) return;
|
||||||
|
|
||||||
// compare to cached score if cached is equal to current or to cached history score otherwise
|
// 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
|
let prevScore =
|
||||||
? (playerScores[leaderboardId].history && playerScores[leaderboardId].history.length ? playerScores[leaderboardId].history[0] : null)
|
playerScores[leaderboardId].score.score === data.score.score
|
||||||
|
? playerScores[leaderboardId].history &&
|
||||||
|
playerScores[leaderboardId].history.length
|
||||||
|
? playerScores[leaderboardId].history[0]
|
||||||
|
: null
|
||||||
: playerScores[leaderboardId].score;
|
: playerScores[leaderboardId].score;
|
||||||
|
|
||||||
// skip if no score to compare
|
// skip if no score to compare
|
||||||
if (!prevScore) return;
|
if (!prevScore) return;
|
||||||
|
|
||||||
const diffInfo = opt(data, 'leaderboard.diffInfo');
|
const diffInfo = opt(data, "leaderboard.diffInfo");
|
||||||
const versions = opt(data, 'leaderboard.beatMaps.versions')
|
const versions = opt(data, "leaderboard.beatMaps.versions");
|
||||||
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
|
const versionsLastIdx =
|
||||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), diffInfo);
|
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);
|
data.prevScore = calculateAcc(prevScore, bmStats, leaderboardId);
|
||||||
}
|
};
|
||||||
|
@ -1,24 +1,28 @@
|
|||||||
import createPpService from '../../../../services/scoresaber/pp'
|
import createPpService from "../../../../services/scoresaber/pp";
|
||||||
import {configStore} from '../../../config'
|
import { configStore } from "../../../config";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
|
|
||||||
let ppService;
|
let ppService;
|
||||||
|
|
||||||
export default async (data, playerId = null, whatIfOnly = false) => {
|
export default async (data, playerId = null, whatIfOnly = false) => {
|
||||||
if (!playerId) return;
|
if (!playerId) return;
|
||||||
|
|
||||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
|
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||||
if (!leaderboardId) return;
|
if (!leaderboardId) return;
|
||||||
|
|
||||||
const pp = opt(data, 'score.pp');
|
const pp = opt(data, "score.pp");
|
||||||
if (!pp) return;
|
if (!pp) return;
|
||||||
|
|
||||||
if (!ppService) ppService = createPpService();
|
if (!ppService) ppService = createPpService();
|
||||||
|
|
||||||
const mainPlayerId = configStore.getMainPlayerId();
|
const mainPlayerId = configStore.getMainPlayerId();
|
||||||
if (mainPlayerId && mainPlayerId !== playerId) {
|
if (mainPlayerId && mainPlayerId !== playerId) {
|
||||||
const whatIfPp = await ppService.getWhatIfScore(mainPlayerId, leaderboardId, pp)
|
const whatIfPp = await ppService.getWhatIfScore(
|
||||||
if (whatIfPp && whatIfPp.diff >= 0.01) data.score.whatIfPp = whatIfPp
|
mainPlayerId,
|
||||||
|
leaderboardId,
|
||||||
|
pp,
|
||||||
|
);
|
||||||
|
if (whatIfPp && whatIfPp.diff >= 0.01) data.score.whatIfPp = whatIfPp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (whatIfOnly) return;
|
if (whatIfOnly) return;
|
||||||
@ -27,4 +31,4 @@ export default async (data, playerId = null, whatIfOnly = false) => {
|
|||||||
if (!ppAttribution) return;
|
if (!ppAttribution) return;
|
||||||
|
|
||||||
data.score.ppAttribution = -ppAttribution.diff;
|
data.score.ppAttribution = -ppAttribution.diff;
|
||||||
}
|
};
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import createTwitchService from '../../../../services/twitch'
|
import createTwitchService from "../../../../services/twitch";
|
||||||
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
|
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
|
||||||
import {opt} from '../../../../utils/js'
|
import { opt } from "../../../../utils/js";
|
||||||
|
|
||||||
let twitchService;
|
let twitchService;
|
||||||
|
|
||||||
export default async (data, playerId = null) => {
|
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 versions = opt(data, "leaderboard.beatMaps.versions");
|
||||||
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
|
const versionsLastIdx =
|
||||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), data.leaderboard.diffInfo);
|
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 (!bmStats || !bmStats.seconds) return;
|
||||||
|
|
||||||
if (!twitchService) twitchService = createTwitchService();
|
if (!twitchService) twitchService = createTwitchService();
|
||||||
@ -17,8 +24,12 @@ export default async (data, playerId = null) => {
|
|||||||
const twitchProfile = await twitchService.refresh(playerId);
|
const twitchProfile = await twitchService.refresh(playerId);
|
||||||
if (!twitchProfile) return;
|
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;
|
if (!video || !video.url) return;
|
||||||
|
|
||||||
data.twitchVideo = video;
|
data.twitchVideo = video;
|
||||||
}
|
};
|
||||||
|
@ -1,44 +1,53 @@
|
|||||||
import createHttpStore from './http-store';
|
import createHttpStore from "./http-store";
|
||||||
import beatMapsEnhancer from './enhancers/common/beatmaps'
|
import beatMapsEnhancer from "./enhancers/common/beatmaps";
|
||||||
import accEnhancer from './enhancers/scores/acc'
|
import accEnhancer from "./enhancers/scores/acc";
|
||||||
import createLeaderboardPageProvider from './providers/page-leaderboard'
|
import createLeaderboardPageProvider from "./providers/page-leaderboard";
|
||||||
import {writable} from 'svelte/store'
|
import { writable } from "svelte/store";
|
||||||
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../utils/scoresaber/song'
|
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../utils/scoresaber/song";
|
||||||
import {debounce} from '../../utils/debounce'
|
import { debounce } from "../../utils/debounce";
|
||||||
import produce, {applyPatches} from 'immer'
|
import produce, { applyPatches } from "immer";
|
||||||
import ppAttributionEnhancer from './enhancers/scores/pp-attribution'
|
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 currentLeaderboardId = leaderboardId ? leaderboardId : null;
|
||||||
let currentType = type ? type : 'global';
|
let currentType = type ? type : "global";
|
||||||
let currentPage = page ? page : 1;
|
let currentPage = page ? page : 1;
|
||||||
|
|
||||||
const { subscribe: subscribeEnhanced, set: setEnhanced } = writable(null);
|
const { subscribe: subscribeEnhanced, set: setEnhanced } = writable(null);
|
||||||
|
|
||||||
const getCurrentEnhanceTaskId = () => `${currentLeaderboardId}/${currentPage}/${currentType}`;
|
const getCurrentEnhanceTaskId = () =>
|
||||||
const getPatchId = (leaderboardId, scoreRow) => `${leaderboardId}/${scoreRow?.player?.playerId}`
|
`${currentLeaderboardId}/${currentPage}/${currentType}`;
|
||||||
|
const getPatchId = (leaderboardId, scoreRow) =>
|
||||||
|
`${leaderboardId}/${scoreRow?.player?.playerId}`;
|
||||||
|
|
||||||
let enhancePatches = {};
|
let enhancePatches = {};
|
||||||
let currentEnhanceTaskId = null;
|
let currentEnhanceTaskId = null;
|
||||||
|
|
||||||
const onNewData = ({ fetchParams, state, set }) => {
|
const onNewData = ({ fetchParams, state, set }) => {
|
||||||
currentLeaderboardId = fetchParams?.leaderboardId ?? null;
|
currentLeaderboardId = fetchParams?.leaderboardId ?? null;
|
||||||
currentType = fetchParams?.type ?? 'global';
|
currentType = fetchParams?.type ?? "global";
|
||||||
currentPage = fetchParams?.page ?? 1;
|
currentPage = fetchParams?.page ?? 1;
|
||||||
|
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
const enhanceTaskId = getCurrentEnhanceTaskId();
|
const enhanceTaskId = getCurrentEnhanceTaskId();
|
||||||
if (currentEnhanceTaskId !== enhanceTaskId) {
|
if (currentEnhanceTaskId !== enhanceTaskId) {
|
||||||
enhancePatches = {}
|
enhancePatches = {};
|
||||||
currentEnhanceTaskId = enhanceTaskId;
|
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] = [];
|
if (!enhancePatches[patchId]) enhancePatches[patchId] = [];
|
||||||
|
|
||||||
enhancePatches[patchId].push(...patches)
|
enhancePatches[patchId].push(...patches);
|
||||||
})
|
});
|
||||||
|
|
||||||
const debouncedSetState = debounce((enhanceTaskId, state) => {
|
const debouncedSetState = debounce((enhanceTaskId, state) => {
|
||||||
if (currentEnhanceTaskId !== enhanceTaskId) return;
|
if (currentEnhanceTaskId !== enhanceTaskId) return;
|
||||||
@ -51,49 +60,78 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
|
|||||||
const setStateRow = (enhanceTaskId, scoreRow) => {
|
const setStateRow = (enhanceTaskId, scoreRow) => {
|
||||||
if (currentEnhanceTaskId !== enhanceTaskId) return null;
|
if (currentEnhanceTaskId !== enhanceTaskId) return null;
|
||||||
|
|
||||||
const patchId = getPatchId(currentLeaderboardId, scoreRow)
|
const patchId = getPatchId(currentLeaderboardId, scoreRow);
|
||||||
const stateRowIdx = newState.scores.findIndex(s => getPatchId(currentLeaderboardId, s) === patchId)
|
const stateRowIdx = newState.scores.findIndex(
|
||||||
|
(s) => getPatchId(currentLeaderboardId, s) === patchId,
|
||||||
|
);
|
||||||
if (stateRowIdx < 0) return;
|
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);
|
debouncedSetState(enhanceTaskId, newState);
|
||||||
|
|
||||||
return newState.scores[stateRowIdx];
|
return newState.scores[stateRowIdx];
|
||||||
}
|
};
|
||||||
|
|
||||||
if (newState.leaderboard)
|
if (newState.leaderboard)
|
||||||
beatMapsEnhancer(newState)
|
beatMapsEnhancer(newState)
|
||||||
.then(_ => {
|
.then((_) => {
|
||||||
const versions = newState?.leaderboard?.beatMaps?.versions ?? null
|
const versions = newState?.leaderboard?.beatMaps?.versions ?? null;
|
||||||
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
|
const versionsLastIdx =
|
||||||
|
versions && Array.isArray(versions) && versions.length
|
||||||
|
? versions.length - 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
const bpm = newState?.leaderboard?.beatMaps?.metadata?.bpm ?? null;
|
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;
|
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);
|
debouncedSetState(enhanceTaskId, newState);
|
||||||
|
|
||||||
return newState.leaderboard.beatMaps;
|
return newState.leaderboard.beatMaps;
|
||||||
})
|
})
|
||||||
.then(_ => {
|
.then((_) => {
|
||||||
if (!newState.scores || !newState.scores.length) return;
|
if (!newState.scores || !newState.scores.length) return;
|
||||||
|
|
||||||
for (const scoreRow of newState.scores) {
|
for (const scoreRow of newState.scores) {
|
||||||
stateProduce({
|
stateProduce(
|
||||||
|
{
|
||||||
...scoreRow,
|
...scoreRow,
|
||||||
leaderboard: newState.leaderboard
|
leaderboard: newState.leaderboard,
|
||||||
}, getPatchId(currentLeaderboardId, scoreRow), draft => accEnhancer(draft))
|
},
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
getPatchId(currentLeaderboardId, scoreRow),
|
||||||
.then(scoreRow => stateProduce({...scoreRow, leaderboard: newState.leaderboard}, getPatchId(currentLeaderboardId, scoreRow), draft => ppAttributionEnhancer(draft, scoreRow?.player?.playerId, true))
|
(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();
|
const provider = createLeaderboardPageProvider();
|
||||||
|
|
||||||
@ -106,18 +144,30 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
|
|||||||
onAfterStateChange: onNewData,
|
onAfterStateChange: onNewData,
|
||||||
onSetPending: ({ fetchParams }) => ({ ...fetchParams }),
|
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) 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);
|
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 {
|
return {
|
||||||
...httpStore,
|
...httpStore,
|
||||||
@ -127,6 +177,5 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
|
|||||||
getType: () => currentType,
|
getType: () => currentType,
|
||||||
getPage: () => currentPage,
|
getPage: () => currentPage,
|
||||||
enhanced: { subscribe: subscribeEnhanced },
|
enhanced: { subscribe: subscribeEnhanced },
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import createHttpStore from './http-store';
|
import createHttpStore from "./http-store";
|
||||||
import playerApiClient from '../../network/clients/scoresaber/player/api'
|
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;
|
let currentPlayerId = playerId;
|
||||||
|
|
||||||
const onNewData = ({ fetchParams }) => {
|
const onNewData = ({ fetchParams }) => {
|
||||||
currentPlayerId = fetchParams?.playerId ?? null;
|
currentPlayerId = fetchParams?.playerId ?? null;
|
||||||
}
|
};
|
||||||
|
|
||||||
const httpStore = createHttpStore(
|
const httpStore = createHttpStore(
|
||||||
playerApiClient,
|
playerApiClient,
|
||||||
@ -23,12 +27,11 @@ export default (playerId = null, initialState = null, initialStateType = 'initia
|
|||||||
if (!playerId || (playerId === currentPlayerId && !force)) return false;
|
if (!playerId || (playerId === currentPlayerId && !force)) return false;
|
||||||
|
|
||||||
return httpStore.fetch({ playerId }, force, playerApiClient);
|
return httpStore.fetch({ playerId }, force, playerApiClient);
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...httpStore,
|
...httpStore,
|
||||||
fetch,
|
fetch,
|
||||||
getPlayerId: () => currentPlayerId,
|
getPlayerId: () => currentPlayerId,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
import stringify from 'json-stable-stringify';
|
import stringify from "json-stable-stringify";
|
||||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||||
import createHttpStore from './http-store';
|
import createHttpStore from "./http-store";
|
||||||
import createApiPlayerWithScoresProvider from './providers/api-player-with-scores'
|
import createApiPlayerWithScoresProvider from "./providers/api-player-with-scores";
|
||||||
import createPlayerService from '../../services/scoresaber/player'
|
import createPlayerService from "../../services/scoresaber/player";
|
||||||
import {addToDate, MINUTE} from '../../utils/date'
|
import { addToDate, MINUTE } from "../../utils/date";
|
||||||
import {writable} from 'svelte/store'
|
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 currentPlayerId = playerId;
|
||||||
let currentService = service;
|
let currentService = service;
|
||||||
let currentServiceParams = serviceParams;
|
let currentServiceParams = serviceParams;
|
||||||
@ -23,8 +29,8 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
currentService = fetchParams?.service ?? null;
|
currentService = fetchParams?.service ?? null;
|
||||||
currentServiceParams = fetchParams?.serviceParams ?? null;
|
currentServiceParams = fetchParams?.serviceParams ?? null;
|
||||||
|
|
||||||
setParams({currentPlayerId, currentService, currentServiceParams})
|
setParams({ currentPlayerId, currentService, currentServiceParams });
|
||||||
}
|
};
|
||||||
|
|
||||||
const provider = createApiPlayerWithScoresProvider();
|
const provider = createApiPlayerWithScoresProvider();
|
||||||
|
|
||||||
@ -36,14 +42,20 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
onInitialized: onNewData,
|
onInitialized: onNewData,
|
||||||
onAfterStateChange: 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 (
|
if (
|
||||||
(!playerId || playerId === currentPlayerId) &&
|
(!playerId || playerId === currentPlayerId) &&
|
||||||
(!service || stringify(service) === stringify(currentService)) &&
|
(!service || stringify(service) === stringify(currentService)) &&
|
||||||
(!serviceParams || stringify(serviceParams) === stringify(currentServiceParams)) &&
|
(!serviceParams ||
|
||||||
|
stringify(serviceParams) === stringify(currentServiceParams)) &&
|
||||||
!force
|
!force
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
@ -54,12 +66,20 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
playerForLastRecentPlay = playerId;
|
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 (!playerId || !currentPlayerId || playerId !== currentPlayerId) return;
|
||||||
|
|
||||||
if (!recentPlay || !lastRecentPlay || recentPlay <= lastRecentPlay) {
|
if (!recentPlay || !lastRecentPlay || recentPlay <= lastRecentPlay) {
|
||||||
@ -74,37 +94,44 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
playerForLastRecentPlay = playerId;
|
playerForLastRecentPlay = playerId;
|
||||||
|
|
||||||
await refresh();
|
await refresh();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const subscribe = fn => {
|
const subscribe = (fn) => {
|
||||||
const storeUnsubscribe = httpStore.subscribe(fn);
|
const storeUnsubscribe = httpStore.subscribe(fn);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
storeUnsubscribe();
|
storeUnsubscribe();
|
||||||
playerRecentPlayUpdatedUnsubscribe();
|
playerRecentPlayUpdatedUnsubscribe();
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const DEFAULT_RECENT_PLAY_REFRESH_INTERVAL = MINUTE;
|
const DEFAULT_RECENT_PLAY_REFRESH_INTERVAL = MINUTE;
|
||||||
|
|
||||||
const enqueueRecentPlayRefresh = async () => {
|
const enqueueRecentPlayRefresh = async () => {
|
||||||
if (!currentPlayerId) {
|
if (!currentPlayerId) {
|
||||||
setTimeout(() => enqueueRecentPlayRefresh(), DEFAULT_RECENT_PLAY_REFRESH_INTERVAL);
|
setTimeout(
|
||||||
|
() => enqueueRecentPlayRefresh(),
|
||||||
|
DEFAULT_RECENT_PLAY_REFRESH_INTERVAL,
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await playerService.fetchPlayerAndUpdateRecentPlay(currentPlayerId);
|
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
|
? DEFAULT_RECENT_PLAY_REFRESH_INTERVAL
|
||||||
: 15 * MINUTE;
|
: 15 * MINUTE;
|
||||||
|
|
||||||
setTimeout(() => enqueueRecentPlayRefresh(), refreshInterval);
|
setTimeout(() => enqueueRecentPlayRefresh(), refreshInterval);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
setTimeout(
|
||||||
|
() => enqueueRecentPlayRefresh(),
|
||||||
setTimeout(() => enqueueRecentPlayRefresh(), DEFAULT_RECENT_PLAY_REFRESH_INTERVAL);
|
DEFAULT_RECENT_PLAY_REFRESH_INTERVAL,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...httpStore,
|
...httpStore,
|
||||||
@ -114,15 +141,14 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
params: { subscribe: subscribeParams },
|
params: { subscribe: subscribeParams },
|
||||||
getPlayerId: () => currentPlayerId,
|
getPlayerId: () => currentPlayerId,
|
||||||
getService: () => currentService,
|
getService: () => currentService,
|
||||||
setService: type => {
|
setService: (type) => {
|
||||||
currentService = type;
|
currentService = type;
|
||||||
setParams({currentPlayerId, currentService, currentServiceParams})
|
setParams({ currentPlayerId, currentService, currentServiceParams });
|
||||||
},
|
},
|
||||||
getServiceParams: () => currentServiceParams,
|
getServiceParams: () => currentServiceParams,
|
||||||
setServiceParams: page => {
|
setServiceParams: (page) => {
|
||||||
currentServiceParams = page
|
currentServiceParams = page;
|
||||||
setParams({currentPlayerId, currentService, currentServiceParams})
|
setParams({ currentPlayerId, currentService, currentServiceParams });
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import createHttpStore from './http-store';
|
import createHttpStore from "./http-store";
|
||||||
import createApiRankingProvider from './providers/api-ranking'
|
import createApiRankingProvider from "./providers/api-ranking";
|
||||||
|
|
||||||
export default (type = 'global', page = 1, initialState = null, initialStateType = 'initial') => {
|
export default (
|
||||||
let currentType = type ? type : 'global';
|
type = "global",
|
||||||
|
page = 1,
|
||||||
|
initialState = null,
|
||||||
|
initialStateType = "initial",
|
||||||
|
) => {
|
||||||
|
let currentType = type ? type : "global";
|
||||||
let currentPage = page ? page : 1;
|
let currentPage = page ? page : 1;
|
||||||
|
|
||||||
const onNewData = ({ fetchParams }) => {
|
const onNewData = ({ fetchParams }) => {
|
||||||
currentType = fetchParams?.type ?? 'global';
|
currentType = fetchParams?.type ?? "global";
|
||||||
currentPage = fetchParams?.page ?? 1;
|
currentPage = fetchParams?.page ?? 1;
|
||||||
}
|
};
|
||||||
|
|
||||||
const provider = createApiRankingProvider();
|
const provider = createApiRankingProvider();
|
||||||
|
|
||||||
@ -21,14 +26,23 @@ export default (type = 'global', page = 1, initialState = null, initialStateType
|
|||||||
onAfterStateChange: onNewData,
|
onAfterStateChange: onNewData,
|
||||||
onSetPending: ({ fetchParams }) => ({ ...fetchParams }),
|
onSetPending: ({ fetchParams }) => ({ ...fetchParams }),
|
||||||
},
|
},
|
||||||
initialStateType
|
initialStateType,
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetch = async (type = currentType, page = currentPage, force = false) => {
|
const fetch = async (
|
||||||
if ((!type || type === currentType) && (!page || page === currentPage) && !force) return false;
|
type = currentType,
|
||||||
|
page = currentPage,
|
||||||
|
force = false,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
(!type || type === currentType) &&
|
||||||
|
(!page || page === currentPage) &&
|
||||||
|
!force
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
return httpStore.fetch({ type, page }, force, provider);
|
return httpStore.fetch({ type, page }, force, provider);
|
||||||
}
|
};
|
||||||
|
|
||||||
const refresh = async () => fetch(currentType, currentPage, true);
|
const refresh = async () => fetch(currentType, currentPage, true);
|
||||||
|
|
||||||
@ -38,6 +52,5 @@ export default (type = 'global', page = 1, initialState = null, initialStateType
|
|||||||
refresh,
|
refresh,
|
||||||
getType: () => currentType,
|
getType: () => currentType,
|
||||||
getPage: () => currentPage,
|
getPage: () => currentPage,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
@ -1,26 +1,34 @@
|
|||||||
import createHttpStore from './http-store';
|
import createHttpStore from "./http-store";
|
||||||
import beatMapsEnhancer from './enhancers/common/beatmaps'
|
import beatMapsEnhancer from "./enhancers/common/beatmaps";
|
||||||
import accEnhancer from './enhancers/scores/acc'
|
import accEnhancer from "./enhancers/scores/acc";
|
||||||
import beatSaviorEnhancer from './enhancers/scores/beatsavior'
|
import beatSaviorEnhancer from "./enhancers/scores/beatsavior";
|
||||||
import rankedsEnhancer from './enhancers/leaderboard/rankeds'
|
import rankedsEnhancer from "./enhancers/leaderboard/rankeds";
|
||||||
import compareEnhancer from './enhancers/scores/compare'
|
import compareEnhancer from "./enhancers/scores/compare";
|
||||||
import diffEnhancer from './enhancers/scores/diff'
|
import diffEnhancer from "./enhancers/scores/diff";
|
||||||
import twitchEnhancer from './enhancers/scores/twitch'
|
import twitchEnhancer from "./enhancers/scores/twitch";
|
||||||
import ppAttributionEnhancer from './enhancers/scores/pp-attribution'
|
import ppAttributionEnhancer from "./enhancers/scores/pp-attribution";
|
||||||
import {debounce} from '../../utils/debounce'
|
import { debounce } from "../../utils/debounce";
|
||||||
import createApiScoresProvider from './providers/api-scores'
|
import createApiScoresProvider from "./providers/api-scores";
|
||||||
import produce, {applyPatches} from 'immer'
|
import produce, { applyPatches } from "immer";
|
||||||
import stringify from 'json-stable-stringify'
|
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 currentPlayerId = playerId;
|
||||||
let currentService = service;
|
let currentService = service;
|
||||||
let currentServiceParams = serviceParams;
|
let currentServiceParams = serviceParams;
|
||||||
|
|
||||||
let totalScores = null;
|
let totalScores = null;
|
||||||
|
|
||||||
const getCurrentEnhanceTaskId = () => `${currentPlayerId}/${currentService}/${stringify(currentServiceParams)}`;
|
const getCurrentEnhanceTaskId = () =>
|
||||||
const getPatchId = (playerId, scoreRow) => `${playerId}/${scoreRow?.leaderboard?.leaderboardId}`
|
`${currentPlayerId}/${currentService}/${stringify(currentServiceParams)}`;
|
||||||
|
const getPatchId = (playerId, scoreRow) =>
|
||||||
|
`${playerId}/${scoreRow?.leaderboard?.leaderboardId}`;
|
||||||
|
|
||||||
let enhancePatches = {};
|
let enhancePatches = {};
|
||||||
let currentEnhanceTaskId = null;
|
let currentEnhanceTaskId = null;
|
||||||
@ -34,7 +42,7 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
totalScores = state !== null ? null : 0;
|
totalScores = state !== null ? null : 0;
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
};
|
||||||
|
|
||||||
const onNewData = ({ fetchParams, state, stateType, set }) => {
|
const onNewData = ({ fetchParams, state, stateType, set }) => {
|
||||||
currentPlayerId = fetchParams?.playerId ?? null;
|
currentPlayerId = fetchParams?.playerId ?? null;
|
||||||
@ -50,15 +58,16 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
|
|
||||||
const enhanceTaskId = getCurrentEnhanceTaskId();
|
const enhanceTaskId = getCurrentEnhanceTaskId();
|
||||||
if (currentEnhanceTaskId !== enhanceTaskId) {
|
if (currentEnhanceTaskId !== enhanceTaskId) {
|
||||||
enhancePatches = {}
|
enhancePatches = {};
|
||||||
currentEnhanceTaskId = enhanceTaskId;
|
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] = [];
|
if (!enhancePatches[patchId]) enhancePatches[patchId] = [];
|
||||||
|
|
||||||
enhancePatches[patchId].push(...patches)
|
enhancePatches[patchId].push(...patches);
|
||||||
})
|
});
|
||||||
|
|
||||||
const debouncedSetState = debounce((enhanceTaskId, state) => {
|
const debouncedSetState = debounce((enhanceTaskId, state) => {
|
||||||
if (currentEnhanceTaskId !== enhanceTaskId) return;
|
if (currentEnhanceTaskId !== enhanceTaskId) return;
|
||||||
@ -71,48 +80,98 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
const setStateRow = (enhanceTaskId, scoreRow) => {
|
const setStateRow = (enhanceTaskId, scoreRow) => {
|
||||||
if (currentEnhanceTaskId !== enhanceTaskId) return null;
|
if (currentEnhanceTaskId !== enhanceTaskId) return null;
|
||||||
|
|
||||||
const patchId = getPatchId(currentPlayerId, scoreRow)
|
const patchId = getPatchId(currentPlayerId, scoreRow);
|
||||||
const stateRowIdx = newState.findIndex(s => getPatchId(currentPlayerId, s) === patchId)
|
const stateRowIdx = newState.findIndex(
|
||||||
|
(s) => getPatchId(currentPlayerId, s) === patchId,
|
||||||
|
);
|
||||||
if (stateRowIdx < 0) return;
|
if (stateRowIdx < 0) return;
|
||||||
|
|
||||||
newState[stateRowIdx] = applyPatches(newState[stateRowIdx], enhancePatches[patchId]);
|
newState[stateRowIdx] = applyPatches(
|
||||||
|
newState[stateRowIdx],
|
||||||
|
enhancePatches[patchId],
|
||||||
|
);
|
||||||
|
|
||||||
debouncedSetState(enhanceTaskId, newState);
|
debouncedSetState(enhanceTaskId, newState);
|
||||||
|
|
||||||
return newState[stateRowIdx];
|
return newState[stateRowIdx];
|
||||||
}
|
};
|
||||||
|
|
||||||
for (const scoreRow of newState) {
|
for (const scoreRow of newState) {
|
||||||
if (currentService !== 'accsaber') {
|
if (currentService !== "accsaber") {
|
||||||
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatMapsEnhancer(draft))
|
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) =>
|
||||||
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => accEnhancer(draft)))
|
beatMapsEnhancer(draft),
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
)
|
||||||
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => diffEnhancer(draft, currentPlayerId)))
|
.then((scoreRow) =>
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
stateProduce(
|
||||||
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => compareEnhancer(draft, currentPlayerId)))
|
scoreRow,
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
getPatchId(currentPlayerId, scoreRow),
|
||||||
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => twitchEnhancer(draft, currentPlayerId)))
|
(draft) => accEnhancer(draft),
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
),
|
||||||
|
)
|
||||||
|
.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))
|
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) =>
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
rankedsEnhancer(draft),
|
||||||
|
).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
|
||||||
|
|
||||||
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => ppAttributionEnhancer(draft, currentPlayerId))
|
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) =>
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
ppAttributionEnhancer(draft, currentPlayerId),
|
||||||
|
).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
|
||||||
|
|
||||||
if (stateType && stateType === 'live')
|
if (stateType && stateType === "live")
|
||||||
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatSaviorEnhancer(draft, currentPlayerId))
|
stateProduce(
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
scoreRow,
|
||||||
|
getPatchId(currentPlayerId, scoreRow),
|
||||||
|
(draft) => beatSaviorEnhancer(draft, currentPlayerId),
|
||||||
|
).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
|
||||||
} else {
|
} else {
|
||||||
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatMapsEnhancer(draft))
|
stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) =>
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
beatMapsEnhancer(draft),
|
||||||
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => twitchEnhancer(draft, currentPlayerId)))
|
)
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
|
||||||
.then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatSaviorEnhancer(draft, currentPlayerId)))
|
.then((scoreRow) =>
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, 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();
|
const provider = createApiScoresProvider();
|
||||||
|
|
||||||
@ -126,22 +185,34 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
onAfterStateChange: onNewData,
|
onAfterStateChange: onNewData,
|
||||||
onSetPending: ({ fetchParams }) => ({ ...fetchParams }),
|
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 (
|
if (
|
||||||
(!playerId || playerId === currentPlayerId) &&
|
(!playerId || playerId === currentPlayerId) &&
|
||||||
(!service || stringify(service) === stringify(currentService)) &&
|
(!service || stringify(service) === stringify(currentService)) &&
|
||||||
(!serviceParams || stringify(serviceParams) === stringify(currentServiceParams)) &&
|
(!serviceParams ||
|
||||||
|
stringify(serviceParams) === stringify(currentServiceParams)) &&
|
||||||
!force
|
!force
|
||||||
)
|
)
|
||||||
return false;
|
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 {
|
return {
|
||||||
...httpStore,
|
...httpStore,
|
||||||
@ -151,6 +222,5 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
|||||||
getService: () => currentService,
|
getService: () => currentService,
|
||||||
getServiceParams: () => currentServiceParams,
|
getServiceParams: () => currentServiceParams,
|
||||||
getTotalScores: () => totalScores,
|
getTotalScores: () => totalScores,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user