add and run prettier
This commit is contained in:
parent
47a23f0484
commit
f6f56aa09c
328
LICENSE.md
328
LICENSE.md
@ -1,190 +1,190 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
GNU General Public License for most of our software; it applies also to
|
GNU General Public License for most of our software; it applies also to
|
||||||
any other work released this way by its authors. You can apply it to
|
any other work released this way by its authors. You can apply it to
|
||||||
your programs, too.
|
your programs, too.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
have the freedom to distribute copies of free software (and charge for
|
have the freedom to distribute copies of free software (and charge for
|
||||||
them if you wish), that you receive source code or can get it if you
|
them if you wish), that you receive source code or can get it if you
|
||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
To protect your rights, we need to prevent others from denying you
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
For example, if you distribute copies of such a program, whether
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
or can get the source code. And you must show them these terms so they
|
or can get the source code. And you must show them these terms so they
|
||||||
know their rights.
|
know their rights.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
that there is no warranty for this free software. For both users' and
|
that there is no warranty for this free software. For both users' and
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
changed, so that their problems will not be attributed erroneously to
|
changed, so that their problems will not be attributed erroneously to
|
||||||
authors of previous versions.
|
authors of previous versions.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
Some devices are designed to deny users access to install or run
|
||||||
modified versions of the software inside them, although the manufacturer
|
modified versions of the software inside them, although the manufacturer
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
protecting users' freedom to change the software. The systematic
|
protecting users' freedom to change the software. The systematic
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
products. If such problems arise substantially in other domains, we
|
products. If such problems arise substantially in other domains, we
|
||||||
stand ready to extend this provision to those domains in future versions
|
stand ready to extend this provision to those domains in future versions
|
||||||
of the GPL, as needed to protect the freedom of users.
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
Finally, every program is threatened constantly by software patents.
|
||||||
States should not allow patents to restrict development and use of
|
States should not allow patents to restrict development and use of
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
avoid the special danger that patents applied to a free program could
|
avoid the special danger that patents applied to a free program could
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
patents cannot be used to render the program non-free.
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
"recipients" may be individuals or organizations.
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
earlier work or a work "based on" the earlier work.
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
A "covered work" means either the unmodified Program or a work based
|
||||||
on the Program.
|
on the Program.
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
To "propagate" a work means to do anything with it that, without
|
||||||
permission, would make you directly or secondarily liable for
|
permission, would make you directly or secondarily liable for
|
||||||
infringement under applicable copyright law, except executing it on a
|
infringement under applicable copyright law, except executing it on a
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
distribution (with or without modification), making available to the
|
distribution (with or without modification), making available to the
|
||||||
public, and in some countries other activities as well.
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
To "convey" a work means any kind of propagation that enables other
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
to the extent that it includes a convenient and prominently visible
|
to the extent that it includes a convenient and prominently visible
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the
|
tells the user that there is no warranty for the work (except to the
|
||||||
extent that warranties are provided), that licensees may convey the
|
extent that warranties are provided), that licensees may convey the
|
||||||
work under this License, and how to view a copy of this License. If
|
work under this License, and how to view a copy of this License. If
|
||||||
the interface presents a list of user commands or options, such as a
|
the interface presents a list of user commands or options, such as a
|
||||||
menu, a prominent item in the list meets this criterion.
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
1. Source Code.
|
1. Source Code.
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
The "source code" for a work means the preferred form of the work
|
||||||
for making modifications to it. "Object code" means any non-source
|
for making modifications to it. "Object code" means any non-source
|
||||||
form of a work.
|
form of a work.
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
A "Standard Interface" means an interface that either is an official
|
||||||
standard defined by a recognized standards body, or, in the case of
|
standard defined by a recognized standards body, or, in the case of
|
||||||
interfaces specified for a particular programming language, one that
|
interfaces specified for a particular programming language, one that
|
||||||
is widely used among developers working in that language.
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
The "System Libraries" of an executable work include anything, other
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
packaging a Major Component, but which is not part of that Major
|
packaging a Major Component, but which is not part of that Major
|
||||||
Component, and (b) serves only to enable use of the work with that
|
Component, and (b) serves only to enable use of the work with that
|
||||||
Major Component, or to implement a Standard Interface for which an
|
Major Component, or to implement a Standard Interface for which an
|
||||||
implementation is available to the public in source code form. A
|
implementation is available to the public in source code form. A
|
||||||
"Major Component", in this context, means a major essential component
|
"Major Component", in this context, means a major essential component
|
||||||
(kernel, window system, and so on) of the specific operating system
|
(kernel, window system, and so on) of the specific operating system
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
produce the work, or an object code interpreter used to run it.
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
The "Corresponding Source" for a work in object code form means all
|
||||||
the source code needed to generate, install, and (for an executable
|
the source code needed to generate, install, and (for an executable
|
||||||
work) run the object code and to modify the work, including scripts to
|
work) run the object code and to modify the work, including scripts to
|
||||||
control those activities. However, it does not include the work's
|
control those activities. However, it does not include the work's
|
||||||
System Libraries, or general-purpose tools or generally available free
|
System Libraries, or general-purpose tools or generally available free
|
||||||
programs which are used unmodified in performing those activities but
|
programs which are used unmodified in performing those activities but
|
||||||
which are not part of the work. For example, Corresponding Source
|
which are not part of the work. For example, Corresponding Source
|
||||||
includes interface definition files associated with source files for
|
includes interface definition files associated with source files for
|
||||||
the work, and the source code for shared libraries and dynamically
|
the work, and the source code for shared libraries and dynamically
|
||||||
linked subprograms that the work is specifically designed to require,
|
linked subprograms that the work is specifically designed to require,
|
||||||
such as by intimate data communication or control flow between those
|
such as by intimate data communication or control flow between those
|
||||||
subprograms and other parts of the work.
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
The Corresponding Source need not include anything that users
|
||||||
can regenerate automatically from other parts of the Corresponding
|
can regenerate automatically from other parts of the Corresponding
|
||||||
Source.
|
Source.
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
The Corresponding Source for a work in source code form is that
|
||||||
same work.
|
same work.
|
||||||
|
|
||||||
2. Basic Permissions.
|
2. Basic Permissions.
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
All rights granted under this License are granted for the term of
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
permission to run the unmodified Program. The output from running a
|
permission to run the unmodified Program. The output from running a
|
||||||
covered work is covered by this License only if the output, given its
|
covered work is covered by this License only if the output, given its
|
||||||
content, constitutes a covered work. This License acknowledges your
|
content, constitutes a covered work. This License acknowledges your
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
You may make, run and propagate covered works that you do not
|
||||||
convey, without conditions so long as your license otherwise remains
|
convey, without conditions so long as your license otherwise remains
|
||||||
in force. You may convey covered works to others for the sole purpose
|
in force. You may convey covered works to others for the sole purpose
|
||||||
of having them make modifications exclusively for you, or provide you
|
of having them make modifications exclusively for you, or provide you
|
||||||
with facilities for running those works, provided that you comply with
|
with facilities for running those works, provided that you comply with
|
||||||
the terms of this License in conveying all material for which you do
|
the terms of this License in conveying all material for which you do
|
||||||
not control copyright. Those thus making or running the covered works
|
not control copyright. Those thus making or running the covered works
|
||||||
for you must do so exclusively on your behalf, under your direction
|
for you must do so exclusively on your behalf, under your direction
|
||||||
and control, on terms that prohibit them from making any copies of
|
and control, on terms that prohibit them from making any copies of
|
||||||
your copyrighted material outside their relationship with you.
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
Conveying under any other circumstances is permitted solely under
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
makes it unnecessary.
|
makes it unnecessary.
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
No covered work shall be deemed part of an effective technological
|
||||||
measure under any applicable law fulfilling obligations under article
|
measure under any applicable law fulfilling obligations under article
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
similar laws prohibiting or restricting circumvention of such
|
similar laws prohibiting or restricting circumvention of such
|
||||||
measures.
|
measures.
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
circumvention of technological measures to the extent such circumvention
|
circumvention of technological measures to the extent such circumvention
|
||||||
is effected by exercising rights under this License with respect to
|
is effected by exercising rights under this License with respect to
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
@ -192,9 +192,9 @@ modification of the work as a means of enforcing, against the work's
|
|||||||
users, your or third parties' legal rights to forbid circumvention of
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
technological measures.
|
technological measures.
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
You may convey verbatim copies of the Program's source code as you
|
||||||
receive it, in any medium, provided that you conspicuously and
|
receive it, in any medium, provided that you conspicuously and
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
keep intact all notices stating that this License and any
|
keep intact all notices stating that this License and any
|
||||||
@ -202,12 +202,12 @@ non-permissive terms added in accord with section 7 apply to the code;
|
|||||||
keep intact all notices of the absence of any warranty; and give all
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
recipients a copy of this License along with the Program.
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
You may charge any price or no price for each copy that you convey,
|
||||||
and you may offer support or warranty protection for a fee.
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
You may convey a work based on the Program, or the modifications to
|
||||||
produce it from the Program, in the form of source code under the
|
produce it from the Program, in the form of source code under the
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
@ -232,19 +232,19 @@ terms of section 4, provided that you also meet all of these conditions:
|
|||||||
interfaces that do not display Appropriate Legal Notices, your
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
work need not make them do so.
|
work need not make them do so.
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
A compilation of a covered work with other separate and independent
|
||||||
works, which are not by their nature extensions of the covered work,
|
works, which are not by their nature extensions of the covered work,
|
||||||
and which are not combined with it such as to form a larger program,
|
and which are not combined with it such as to form a larger program,
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
used to limit the access or legal rights of the compilation's users
|
used to limit the access or legal rights of the compilation's users
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
in an aggregate does not cause this License to apply to the other
|
in an aggregate does not cause this License to apply to the other
|
||||||
parts of the aggregate.
|
parts of the aggregate.
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
You may convey a covered work in object code form under the terms
|
||||||
of sections 4 and 5, provided that you also convey the
|
of sections 4 and 5, provided that you also convey the
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
in one of these ways:
|
in one of these ways:
|
||||||
@ -290,75 +290,75 @@ in one of these ways:
|
|||||||
Source of the work are being offered to the general public at no
|
Source of the work are being offered to the general public at no
|
||||||
charge under subsection 6d.
|
charge under subsection 6d.
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
A separable portion of the object code, whose source code is excluded
|
||||||
from the Corresponding Source as a System Library, need not be
|
from the Corresponding Source as a System Library, need not be
|
||||||
included in conveying the object code work.
|
included in conveying the object code work.
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
tangible personal property which is normally used for personal, family,
|
tangible personal property which is normally used for personal, family,
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
product received by a particular user, "normally used" refers to a
|
product received by a particular user, "normally used" refers to a
|
||||||
typical or common use of that class of product, regardless of the status
|
typical or common use of that class of product, regardless of the status
|
||||||
of the particular user or of the way in which the particular user
|
of the particular user or of the way in which the particular user
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
is a consumer product regardless of whether the product has substantial
|
is a consumer product regardless of whether the product has substantial
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
the only significant mode of use of the product.
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
"Installation Information" for a User Product means any methods,
|
||||||
procedures, authorization keys, or other information required to install
|
procedures, authorization keys, or other information required to install
|
||||||
and execute modified versions of a covered work in that User Product from
|
and execute modified versions of a covered work in that User Product from
|
||||||
a modified version of its Corresponding Source. The information must
|
a modified version of its Corresponding Source. The information must
|
||||||
suffice to ensure that the continued functioning of the modified object
|
suffice to ensure that the continued functioning of the modified object
|
||||||
code is in no case prevented or interfered with solely because
|
code is in no case prevented or interfered with solely because
|
||||||
modification has been made.
|
modification has been made.
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
If you convey an object code work under this section in, or with, or
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
part of a transaction in which the right of possession and use of the
|
part of a transaction in which the right of possession and use of the
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
by the Installation Information. But this requirement does not apply
|
by the Installation Information. But this requirement does not apply
|
||||||
if neither you nor any third party retains the ability to install
|
if neither you nor any third party retains the ability to install
|
||||||
modified object code on the User Product (for example, the work has
|
modified object code on the User Product (for example, the work has
|
||||||
been installed in ROM).
|
been installed in ROM).
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
The requirement to provide Installation Information does not include a
|
||||||
requirement to continue to provide support service, warranty, or updates
|
requirement to continue to provide support service, warranty, or updates
|
||||||
for a work that has been modified or installed by the recipient, or for
|
for a work that has been modified or installed by the recipient, or for
|
||||||
the User Product in which it has been modified or installed. Access to a
|
the User Product in which it has been modified or installed. Access to a
|
||||||
network may be denied when the modification itself materially and
|
network may be denied when the modification itself materially and
|
||||||
adversely affects the operation of the network or violates the rules and
|
adversely affects the operation of the network or violates the rules and
|
||||||
protocols for communication across the network.
|
protocols for communication across the network.
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
in accord with this section must be in a format that is publicly
|
in accord with this section must be in a format that is publicly
|
||||||
documented (and with an implementation available to the public in
|
documented (and with an implementation available to the public in
|
||||||
source code form), and must require no special password or key for
|
source code form), and must require no special password or key for
|
||||||
unpacking, reading or copying.
|
unpacking, reading or copying.
|
||||||
|
|
||||||
7. Additional Terms.
|
7. Additional Terms.
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
License by making exceptions from one or more of its conditions.
|
License by making exceptions from one or more of its conditions.
|
||||||
Additional permissions that are applicable to the entire Program shall
|
Additional permissions that are applicable to the entire Program shall
|
||||||
be treated as though they were included in this License, to the extent
|
be treated as though they were included in this License, to the extent
|
||||||
that they are valid under applicable law. If additional permissions
|
that they are valid under applicable law. If additional permissions
|
||||||
apply only to part of the Program, that part may be used separately
|
apply only to part of the Program, that part may be used separately
|
||||||
under those permissions, but the entire Program remains governed by
|
under those permissions, but the entire Program remains governed by
|
||||||
this License without regard to the additional permissions.
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
When you convey a copy of a covered work, you may at your option
|
||||||
remove any additional permissions from that copy, or from any part of
|
remove any additional permissions from that copy, or from any part of
|
||||||
it. (Additional permissions may be written to require their own
|
it. (Additional permissions may be written to require their own
|
||||||
removal in certain cases when you modify the work.) You may place
|
removal in certain cases when you modify the work.) You may place
|
||||||
additional permissions on material, added by you to a covered work,
|
additional permissions on material, added by you to a covered work,
|
||||||
for which you have or can give appropriate copyright permission.
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
Notwithstanding any other provision of this License, for material you
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
that material) supplement the terms of this License with terms:
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
@ -385,74 +385,74 @@ that material) supplement the terms of this License with terms:
|
|||||||
any liability that these contractual assumptions directly impose on
|
any liability that these contractual assumptions directly impose on
|
||||||
those licensors and authors.
|
those licensors and authors.
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
All other non-permissive additional terms are considered "further
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
received it, or any part of it, contains a notice stating that it is
|
received it, or any part of it, contains a notice stating that it is
|
||||||
governed by this License along with a term that is a further
|
governed by this License along with a term that is a further
|
||||||
restriction, you may remove that term. If a license document contains
|
restriction, you may remove that term. If a license document contains
|
||||||
a further restriction but permits relicensing or conveying under this
|
a further restriction but permits relicensing or conveying under this
|
||||||
License, you may add to a covered work material governed by the terms
|
License, you may add to a covered work material governed by the terms
|
||||||
of that license document, provided that the further restriction does
|
of that license document, provided that the further restriction does
|
||||||
not survive such relicensing or conveying.
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
If you add terms to a covered work in accord with this section, you
|
||||||
must place, in the relevant source files, a statement of the
|
must place, in the relevant source files, a statement of the
|
||||||
additional terms that apply to those files, or a notice indicating
|
additional terms that apply to those files, or a notice indicating
|
||||||
where to find the applicable terms.
|
where to find the applicable terms.
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
form of a separately written license, or stated as exceptions;
|
form of a separately written license, or stated as exceptions;
|
||||||
the above requirements apply either way.
|
the above requirements apply either way.
|
||||||
|
|
||||||
8. Termination.
|
8. Termination.
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
You may not propagate or modify a covered work except as expressly
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
modify it is void, and will automatically terminate your rights under
|
modify it is void, and will automatically terminate your rights under
|
||||||
this License (including any patent licenses granted under the third
|
this License (including any patent licenses granted under the third
|
||||||
paragraph of section 11).
|
paragraph of section 11).
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
However, if you cease all violation of this License, then your
|
||||||
license from a particular copyright holder is reinstated (a)
|
license from a particular copyright holder is reinstated (a)
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
holder fails to notify you of the violation by some reasonable means
|
holder fails to notify you of the violation by some reasonable means
|
||||||
prior to 60 days after the cessation.
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
Moreover, your license from a particular copyright holder is
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
violation by some reasonable means, this is the first time you have
|
violation by some reasonable means, this is the first time you have
|
||||||
received notice of violation of this License (for any work) from that
|
received notice of violation of this License (for any work) from that
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
your receipt of the notice.
|
your receipt of the notice.
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
Termination of your rights under this section does not terminate the
|
||||||
licenses of parties who have received copies or rights from you under
|
licenses of parties who have received copies or rights from you under
|
||||||
this License. If your rights have been terminated and not permanently
|
this License. If your rights have been terminated and not permanently
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
material under section 10.
|
material under section 10.
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
You are not required to accept this License in order to receive or
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
to receive a copy likewise does not require acceptance. However,
|
to receive a copy likewise does not require acceptance. However,
|
||||||
nothing other than this License grants you permission to propagate or
|
nothing other than this License grants you permission to propagate or
|
||||||
modify any covered work. These actions infringe copyright if you do
|
modify any covered work. These actions infringe copyright if you do
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
Each time you convey a covered work, the recipient automatically
|
||||||
receives a license from the original licensors, to run, modify and
|
receives a license from the original licensors, to run, modify and
|
||||||
propagate that work, subject to this License. You are not responsible
|
propagate that work, subject to this License. You are not responsible
|
||||||
for enforcing compliance by third parties with this License.
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
An "entity transaction" is a transaction transferring control of an
|
||||||
organization, or substantially all assets of one, or subdividing an
|
organization, or substantially all assets of one, or subdividing an
|
||||||
organization, or merging organizations. If propagation of a covered
|
organization, or merging organizations. If propagation of a covered
|
||||||
work results from an entity transaction, each party to that
|
work results from an entity transaction, each party to that
|
||||||
transaction who receives a copy of the work also receives whatever
|
transaction who receives a copy of the work also receives whatever
|
||||||
licenses to the work the party's predecessor in interest had or could
|
licenses to the work the party's predecessor in interest had or could
|
||||||
@ -460,43 +460,43 @@ give under the previous paragraph, plus a right to possession of the
|
|||||||
Corresponding Source of the work from the predecessor in interest, if
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
You may not impose any further restrictions on the exercise of the
|
||||||
rights granted or affirmed under this License. For example, you may
|
rights granted or affirmed under this License. For example, you may
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
rights granted under this License, and you may not initiate litigation
|
rights granted under this License, and you may not initiate litigation
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
sale, or importing the Program or any portion of it.
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
11. Patents.
|
11. Patents.
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
License of the Program or a work on which the Program is based. The
|
License of the Program or a work on which the Program is based. The
|
||||||
work thus licensed is called the contributor's "contributor version".
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
A contributor's "essential patent claims" are all patent claims
|
||||||
owned or controlled by the contributor, whether already acquired or
|
owned or controlled by the contributor, whether already acquired or
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
by this License, of making, using, or selling its contributor version,
|
by this License, of making, using, or selling its contributor version,
|
||||||
but do not include claims that would be infringed only as a
|
but do not include claims that would be infringed only as a
|
||||||
consequence of further modification of the contributor version. For
|
consequence of further modification of the contributor version. For
|
||||||
purposes of this definition, "control" includes the right to grant
|
purposes of this definition, "control" includes the right to grant
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
this License.
|
this License.
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
patent license under the contributor's essential patent claims, to
|
patent license under the contributor's essential patent claims, to
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
propagate the contents of its contributor version.
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
In the following three paragraphs, a "patent license" is any express
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
(such as an express permission to practice a patent or covenant not to
|
(such as an express permission to practice a patent or covenant not to
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
party means to make such an agreement or commitment not to enforce a
|
party means to make such an agreement or commitment not to enforce a
|
||||||
patent against the party.
|
patent against the party.
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
and the Corresponding Source of the work is not available for anyone
|
and the Corresponding Source of the work is not available for anyone
|
||||||
to copy, free of charge and under the terms of this License, through a
|
to copy, free of charge and under the terms of this License, through a
|
||||||
publicly available network server or other readily accessible means,
|
publicly available network server or other readily accessible means,
|
||||||
@ -504,13 +504,13 @@ then you must either (1) cause the Corresponding Source to be so
|
|||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
consistent with the requirements of this License, to extend the patent
|
consistent with the requirements of this License, to extend the patent
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
covered work in a country, or your recipient's use of the covered work
|
covered work in a country, or your recipient's use of the covered work
|
||||||
in a country, would infringe one or more identifiable patents in that
|
in a country, would infringe one or more identifiable patents in that
|
||||||
country that you have reason to believe are valid.
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
If, pursuant to or in connection with a single transaction or
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
covered work, and grant a patent license to some of the parties
|
covered work, and grant a patent license to some of the parties
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
@ -518,10 +518,10 @@ or convey a specific copy of the covered work, then the patent license
|
|||||||
you grant is automatically extended to all recipients of the covered
|
you grant is automatically extended to all recipients of the covered
|
||||||
work and works based on it.
|
work and works based on it.
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
A patent license is "discriminatory" if it does not include within
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
specifically granted under this License. You may not convey a covered
|
specifically granted under this License. You may not convey a covered
|
||||||
work if you are a party to an arrangement with a third party that is
|
work if you are a party to an arrangement with a third party that is
|
||||||
in the business of distributing software, under which you make payment
|
in the business of distributing software, under which you make payment
|
||||||
to the third party based on the extent of your activity of conveying
|
to the third party based on the extent of your activity of conveying
|
||||||
@ -533,73 +533,73 @@ for and in connection with specific products or compilations that
|
|||||||
contain the covered work, unless you entered into that arrangement,
|
contain the covered work, unless you entered into that arrangement,
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
any implied license or other defenses to infringement that may
|
any implied license or other defenses to infringement that may
|
||||||
otherwise be available to you under applicable patent law.
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the special requirements of the GNU Affero General Public License,
|
||||||
section 13, concerning interaction through a network will apply to the
|
section 13, concerning interaction through a network will apply to the
|
||||||
combination as such.
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU General Public License from time to time. Such new versions will
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
Later license versions may give you additional or different
|
||||||
permissions. However, no additional obligations are imposed on any
|
permissions. However, no additional obligations are imposed on any
|
||||||
author or copyright holder as a result of your choosing to follow a
|
author or copyright holder as a result of your choosing to follow a
|
||||||
later version.
|
later version.
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
16. Limitation of Liability.
|
16. Limitation of Liability.
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
@ -609,9 +609,9 @@ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
SUCH DAMAGES.
|
SUCH DAMAGES.
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
above cannot be given local legal effect according to their terms,
|
above cannot be given local legal effect according to their terms,
|
||||||
reviewing courts shall apply local law that most closely approximates
|
reviewing courts shall apply local law that most closely approximates
|
||||||
an absolute waiver of all civil liability in connection with the
|
an absolute waiver of all civil liability in connection with the
|
||||||
@ -622,11 +622,11 @@ copy of the Program in return for a fee.
|
|||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
If you develop a new program, and you want it to be of the greatest
|
||||||
possible use to the public, the best way to achieve this is to make it
|
possible use to the public, the best way to achieve this is to make it
|
||||||
free software which everyone can redistribute and change under these terms.
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
To do so, attach the following notices to the program. It is safest
|
||||||
to attach them to the start of each source file to most effectively
|
to attach them to the start of each source file to most effectively
|
||||||
state the exclusion of warranty; and each file should have at least
|
state the exclusion of warranty; and each file should have at least
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
@ -649,7 +649,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If the program does terminal interaction, make it output a short
|
||||||
notice like this when it starts in an interactive mode:
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
<program> Copyright (C) <year> <name of author>
|
||||||
@ -658,17 +658,17 @@ notice like this when it starts in an interactive mode:
|
|||||||
under certain conditions; type `show c' for details.
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
parts of the General Public License. Of course, your program's commands
|
parts of the General Public License. Of course, your program's commands
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
The GNU General Public License does not permit incorporating your program
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
may consider it more useful to permit linking proprietary applications with
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
@ -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
@ -1,206 +1,207 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #222;
|
--background: #222;
|
||||||
--foreground: #252525;
|
--foreground: #252525;
|
||||||
--textColor: #eee;
|
--textColor: #eee;
|
||||||
--ppColour: #8992e8;
|
--ppColour: #8992e8;
|
||||||
--alternate: #72a8ff;
|
--alternate: #72a8ff;
|
||||||
--selected: #3273dc;
|
--selected: #3273dc;
|
||||||
--hover: #333;
|
--hover: #333;
|
||||||
--highlight: #484848;
|
--highlight: #484848;
|
||||||
--decrease: #f94022;
|
--decrease: #f94022;
|
||||||
--increase: #42b129;
|
--increase: #42b129;
|
||||||
--dimmed: #3e3e3e;
|
--dimmed: #3e3e3e;
|
||||||
--faded: #666;
|
--faded: #666;
|
||||||
--color-ahead: rgb(0, 128, 0);
|
--color-ahead: rgb(0, 128, 0);
|
||||||
--color-behind: rgb(128, 0, 0);
|
--color-behind: rgb(128, 0, 0);
|
||||||
--color-highlight: darkgreen;
|
--color-highlight: darkgreen;
|
||||||
--error: red;
|
--error: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: --webkit-fill-available;
|
height: --webkit-fill-available;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
background-color: var(--background)!important;
|
background-color: var(--background) !important;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: -webkit-fill-available;
|
min-height: -webkit-fill-available;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
background-color: var(--foreground);
|
background-color: var(--foreground);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ssr-page-container {
|
.ssr-page-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inc {
|
.inc {
|
||||||
color: var(--increase);
|
color: var(--increase);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dec {
|
.dec {
|
||||||
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%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-image: url("./scoresaber-logo.svg");
|
background-image: url("./scoresaber-logo.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
.beatsavior-icon {
|
.beatsavior-icon {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
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 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-image: url("./accsaber-logo.png");
|
background-image: url("./accsaber-logo.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper {
|
.grid-transition-helper {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > * {
|
.grid-transition-helper > * {
|
||||||
grid-column: 1/1;
|
grid-column: 1/1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-0 {
|
.grid-transition-helper > .row-0 {
|
||||||
grid-row: 1/1;
|
grid-row: 1/1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-1 {
|
.grid-transition-helper > .row-1 {
|
||||||
grid-row: 2/2;
|
grid-row: 2/2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-2 {
|
.grid-transition-helper > .row-2 {
|
||||||
grid-row: 3/3;
|
grid-row: 3/3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-3 {
|
.grid-transition-helper > .row-3 {
|
||||||
grid-row: 4/4;
|
grid-row: 4/4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-4 {
|
.grid-transition-helper > .row-4 {
|
||||||
grid-row: 5/5;
|
grid-row: 5/5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-5 {
|
.grid-transition-helper > .row-5 {
|
||||||
grid-row: 6/6;
|
grid-row: 6/6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-6 {
|
.grid-transition-helper > .row-6 {
|
||||||
grid-row: 7/7;
|
grid-row: 7/7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-7 {
|
.grid-transition-helper > .row-7 {
|
||||||
grid-row: 8/8;
|
grid-row: 8/8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-8 {
|
.grid-transition-helper > .row-8 {
|
||||||
grid-row: 9/9;
|
grid-row: 9/9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-9 {
|
.grid-transition-helper > .row-9 {
|
||||||
grid-row: 10/10;
|
grid-row: 10/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-10 {
|
.grid-transition-helper > .row-10 {
|
||||||
grid-row: 11/11;
|
grid-row: 11/11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-transition-helper > .row-11 {
|
.grid-transition-helper > .row-11 {
|
||||||
grid-row: 12/12;
|
grid-row: 12/12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-pointer-events {
|
.has-pointer-events {
|
||||||
pointer-events: fill;
|
pointer-events: fill;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-only {
|
.mobile-only {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablet-only {
|
.tablet-only {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.up-to-tablet {
|
.up-to-tablet {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.mobile-only {
|
.mobile-only {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablet-and-up {
|
.tablet-and-up {
|
||||||
display: none!important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.above-tablet {
|
.above-tablet {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width:768px) and (max-width: 1023px) {
|
@media screen and (min-width: 768px) and (max-width: 1023px) {
|
||||||
.tablet-only {
|
.tablet-only {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1023px) {
|
@media screen and (max-width: 1023px) {
|
||||||
.up-to-tablet {
|
.up-to-tablet {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-and-up {
|
.desktop-and-up {
|
||||||
display: none!important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1750px) {
|
@media screen and (min-width: 1750px) {
|
||||||
body:not(.slim) .ssr-page-container {
|
body:not(.slim) .ssr-page-container {
|
||||||
max-width: 1750px !important;
|
max-width: 1750px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 360px) {
|
@media screen and (max-width: 360px) {
|
||||||
body {
|
body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 320px) {
|
@media screen and (max-width: 320px) {
|
||||||
html {
|
html {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
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
|
||||||
<link rel="stylesheet" href="/assets/ssr.css?20210925" />
|
rel="stylesheet"
|
||||||
<link rel='stylesheet' href='/build/bundle.css' />
|
href="https://use.fontawesome.com/releases/v5.15.4/css/all.css"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="/assets/ssr.css?20210925" />
|
||||||
|
<link rel="stylesheet" href="/build/bundle.css" />
|
||||||
|
|
||||||
<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>
|
||||||
|
261
rollup.config.js
261
rollup.config.js
@ -1,150 +1,159 @@
|
|||||||
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;
|
||||||
|
|
||||||
function toExit() {
|
function toExit() {
|
||||||
if (server) server.kill(0);
|
if (server) server.kill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
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({
|
||||||
preprocess: sveltePreprocess({sourceMap: !production}),
|
preprocess: sveltePreprocess({ sourceMap: !production }),
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
// enable run-time checks when not in production
|
// enable run-time checks when not in production
|
||||||
dev: !production,
|
dev: !production,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
// 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(),
|
||||||
|
|
||||||
// If you have external dependencies installed from
|
// If you have external dependencies installed from
|
||||||
// npm, you'll most likely need these plugins. In
|
// npm, you'll most likely need these plugins. In
|
||||||
// some cases you'll need additional configuration -
|
// some cases you'll need additional configuration -
|
||||||
// consult the documentation for details:
|
// consult the documentation for details:
|
||||||
// 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(),
|
||||||
|
|
||||||
// In dev mode, call `npm run start` once
|
// In dev mode, call `npm run start` once
|
||||||
// the bundle has been generated
|
// the bundle has been generated
|
||||||
!production && serve(),
|
!production && serve(),
|
||||||
|
|
||||||
// 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"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
watch: {
|
watch: {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
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
|
||||||
// npm, you'll most likely need these plugins. In
|
// npm, you'll most likely need these plugins. In
|
||||||
// some cases you'll need additional configuration -
|
// some cases you'll need additional configuration -
|
||||||
// consult the documentation for details:
|
// consult the documentation for details:
|
||||||
// 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(),
|
||||||
|
|
||||||
// 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-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"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -5,7 +5,7 @@ export const onLegendClick = (event, legendItem, legend) => {
|
|||||||
const scales = legend?.chart?.config?.options?.scales;
|
const scales = legend?.chart?.config?.options?.scales;
|
||||||
if (!scales) return;
|
if (!scales) return;
|
||||||
|
|
||||||
const {x: xAxis, ...yAxes} = scales;
|
const { x: xAxis, ...yAxes } = scales;
|
||||||
|
|
||||||
if (ci.isDatasetVisible(idx)) {
|
if (ci.isDatasetVisible(idx)) {
|
||||||
ci.hide(idx);
|
ci.hide(idx);
|
||||||
@ -17,38 +17,52 @@ 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) =>
|
||||||
// done
|
(ci?.config?.data?.datasets?.[a?.datasetIndex]?.axisOrder ??
|
||||||
if (cum.second) return cum;
|
a?.datasetIndex) -
|
||||||
|
(ci?.config?.data?.datasets?.[b?.datasetIndex]?.axisOrder ??
|
||||||
|
b?.datasetIndex),
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
(cum, legendItem) => {
|
||||||
|
// done
|
||||||
|
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 =
|
||||||
if (!yAxisId) return cum;
|
ci?.getDatasetMeta(legendItem?.datasetIndex)?.yAxisID ?? null;
|
||||||
|
if (!yAxisId) return cum;
|
||||||
|
|
||||||
if (!cum.first) {
|
if (!cum.first) {
|
||||||
cum.first = yAxisId;
|
cum.first = yAxisId;
|
||||||
} else if (yAxisId !== cum.first) {
|
} else if (yAxisId !== cum.first) {
|
||||||
cum.second = yAxisId;
|
cum.second = yAxisId;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,22 +1,29 @@
|
|||||||
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);
|
||||||
if (isNaN(fontSize)) fontSize = 12;
|
if (isNaN(fontSize)) fontSize = 12;
|
||||||
|
|
||||||
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,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
316
src/db/db.js
316
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",
|
||||||
autoIncrement: true,
|
{
|
||||||
});
|
keyPath: "_idbId",
|
||||||
rankedsChangesStore.createIndex('rankeds-changes-timestamp', 'timestamp', {unique: false});
|
autoIncrement: true,
|
||||||
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
|
});
|
||||||
|
|
||||||
case newVersion >= 4 && oldVersion <=3:
|
|
||||||
db.deleteObjectStore('beat-savior-files');
|
|
||||||
|
|
||||||
const beatSaviorStore = transaction.objectStore('beat-savior');
|
|
||||||
beatSaviorStore.deleteIndex('beat-savior-fileId');
|
|
||||||
beatSaviorStore.deleteIndex('beat-savior-songId');
|
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 5 && oldVersion <=4:
|
case newVersion >= 4 && oldVersion <= 3:
|
||||||
const songsBeatMapsStore = db.createObjectStore('songs-beatmaps', {
|
db.deleteObjectStore("beat-savior-files");
|
||||||
keyPath: 'hash',
|
|
||||||
autoIncrement: false,
|
const beatSaviorStore = transaction.objectStore("beat-savior");
|
||||||
});
|
beatSaviorStore.deleteIndex("beat-savior-fileId");
|
||||||
songsBeatMapsStore.createIndex('songs-beatmaps--key', 'key', {unique: true});
|
beatSaviorStore.deleteIndex("beat-savior-songId");
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 6 && oldVersion <=5:
|
case newVersion >= 5 && oldVersion <= 4:
|
||||||
const songsBeatMapsStorev6 = transaction.objectStore('songs-beatmaps');
|
const songsBeatMapsStore = db.createObjectStore("songs-beatmaps", {
|
||||||
songsBeatMapsStorev6.deleteIndex('songs-beatmaps--key');
|
keyPath: "hash",
|
||||||
songsBeatMapsStorev6.createIndex('songs-beatmaps-key', 'key', {unique: true});
|
autoIncrement: false,
|
||||||
|
});
|
||||||
|
songsBeatMapsStore.createIndex("songs-beatmaps--key", "key", {
|
||||||
|
unique: true,
|
||||||
|
});
|
||||||
|
|
||||||
// NO break here
|
// NO break here
|
||||||
|
|
||||||
case newVersion >= 7 && oldVersion <=6:
|
case newVersion >= 6 && oldVersion <= 5:
|
||||||
const scoresUpdateQueue = db.createObjectStore('scores-update-queue', {
|
const songsBeatMapsStorev6 =
|
||||||
keyPath: 'id',
|
transaction.objectStore("songs-beatmaps");
|
||||||
autoIncrement: false,
|
songsBeatMapsStorev6.deleteIndex("songs-beatmaps--key");
|
||||||
|
songsBeatMapsStorev6.createIndex("songs-beatmaps-key", "key", {
|
||||||
|
unique: true,
|
||||||
});
|
});
|
||||||
scoresUpdateQueue.createIndex('scores-update-queue-fetchedAt', 'fetchedAt', {unique: false});
|
|
||||||
|
// NO break here
|
||||||
|
|
||||||
|
case newVersion >= 7 && oldVersion <= 6:
|
||||||
|
const scoresUpdateQueue = db.createObjectStore(
|
||||||
|
"scores-update-queue",
|
||||||
|
{
|
||||||
|
keyPath: "id",
|
||||||
|
autoIncrement: 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",
|
||||||
autoIncrement: false,
|
{
|
||||||
});
|
keyPath: "id",
|
||||||
accSaberPlayersStore.createIndex('accsaber-players-playerId', 'playerId', {unique: false});
|
autoIncrement: 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",
|
||||||
autoIncrement: false,
|
{
|
||||||
});
|
keyPath: "playerIdTimestamp",
|
||||||
accSaberPlayersHistoryStore.createIndex('accsaber-players-history-playerId', 'playerId', {unique: false});
|
autoIncrement: 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,189 +1,231 @@
|
|||||||
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.push(fixName);
|
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||||
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
? allAppliedFixes
|
||||||
});
|
: [];
|
||||||
|
allAppliedFixes.push(fixName);
|
||||||
|
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;
|
||||||
|
|
||||||
const beatmapsService = createBeatMapsService();
|
const beatmapsService = createBeatMapsService();
|
||||||
|
|
||||||
while (cursor) {
|
while (cursor) {
|
||||||
const beatSaverSong = cursor.value;
|
const beatSaverSong = cursor.value;
|
||||||
|
|
||||||
if (beatSaverSong?.metadata?.characteristics) {
|
if (beatSaverSong?.metadata?.characteristics) {
|
||||||
const beatMapsSong = beatmapsService.convertOldBeatSaverToBeatMaps(beatSaverSong);
|
const beatMapsSong =
|
||||||
if (beatMapsSong) {
|
beatmapsService.convertOldBeatSaverToBeatMaps(beatSaverSong);
|
||||||
songsBeatMapsStore.put(beatMapsSong)
|
if (beatMapsSong) {
|
||||||
|
songsBeatMapsStore.put(beatMapsSong);
|
||||||
|
|
||||||
songCount++;
|
songCount++;
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
`Unable to convert, deleting a song`,
|
||||||
|
"DBFix",
|
||||||
|
beatSaverSong,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info(`Unable to convert, deleting a song`, 'DBFix', beatSaverSong);
|
log.info(
|
||||||
|
`No metadata characteristics, skipping a song`,
|
||||||
|
"DBFix",
|
||||||
|
beatSaverSong,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.info(`No metadata characteristics, skipping a song`, 'DBFix', beatSaverSong);
|
cursor = await cursor.continue();
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = await cursor.continue();
|
const keyValueStore = tx.objectStore("key-value");
|
||||||
}
|
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
||||||
|
allAppliedFixes =
|
||||||
|
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||||
|
? allAppliedFixes
|
||||||
|
: [];
|
||||||
|
allAppliedFixes.push(fixName);
|
||||||
|
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
||||||
|
|
||||||
const keyValueStore = tx.objectStore('key-value')
|
log.info(`${songCount} BeatSaver song(s) converted`, "DBFix");
|
||||||
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
},
|
||||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
);
|
||||||
allAppliedFixes.push(fixName);
|
},
|
||||||
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
|
||||||
|
|
||||||
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(
|
||||||
lastUpdated: null,
|
async ([playerId, twitchLogin]) =>
|
||||||
login: twitchLogin,
|
updatePlayerTwitchProfile({
|
||||||
playerId
|
lastUpdated: null,
|
||||||
}
|
login: twitchLogin,
|
||||||
)))
|
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();
|
||||||
|
|
||||||
while (cursor) {
|
while (cursor) {
|
||||||
const history = cursor.value;
|
const history = cursor.value;
|
||||||
|
|
||||||
|
if (!history?.playerId || !isDateObject(history?.ssDate)) {
|
||||||
|
await cursor.delete();
|
||||||
|
cursor = await cursor.continue();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerId = history.playerId;
|
||||||
|
const ssDate = correctOldSsDate(history.ssDate);
|
||||||
|
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
|
||||||
|
|
||||||
if (!history?.playerId || !isDateObject(history?.ssDate)) {
|
|
||||||
await cursor.delete();
|
await cursor.delete();
|
||||||
cursor = await cursor.continue();
|
playersHistoryStore.put({
|
||||||
|
...history,
|
||||||
|
ssDate,
|
||||||
|
playerIdSsTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
continue;
|
cursor = await cursor.continue();
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerId = history.playerId;
|
const keyValueStore = tx.objectStore("key-value");
|
||||||
const ssDate = correctOldSsDate(history.ssDate);
|
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
||||||
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
|
allAppliedFixes =
|
||||||
|
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||||
await cursor.delete();
|
? allAppliedFixes
|
||||||
playersHistoryStore.put({...history, ssDate, playerIdSsTimestamp});
|
: [];
|
||||||
|
allAppliedFixes.push(fixName);
|
||||||
cursor = await cursor.continue();
|
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
const keyValueStore = tx.objectStore('key-value')
|
|
||||||
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
|
||||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
|
||||||
allAppliedFixes.push(fixName);
|
|
||||||
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,92 +143,111 @@ 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 () =>
|
||||||
const data = await getFromDb();
|
resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => {
|
||||||
|
const data = await getFromDb();
|
||||||
|
|
||||||
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
|
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
|
||||||
|
|
||||||
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");
|
||||||
|
53
src/main.js
53
src/main.js
@ -1,35 +1,35 @@
|
|||||||
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;
|
||||||
|
|
||||||
(async() => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// TODO: remove level setting
|
// TODO: remove level setting
|
||||||
// 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();
|
||||||
await setupDataFixes();
|
await setupDataFixes();
|
||||||
|
|
||||||
// WORKAROUND for immer.js esm (see https://github.com/immerjs/immer/issues/557)
|
// WORKAROUND for immer.js esm (see https://github.com/immerjs/immer/issues/557)
|
||||||
window.process = {env: {NODE_ENV: "production"}};
|
window.process = { env: { NODE_ENV: "production" } };
|
||||||
|
|
||||||
// setup immer.js
|
// setup immer.js
|
||||||
enablePatches();
|
enablePatches();
|
||||||
@ -47,24 +47,29 @@ 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,
|
||||||
props: {},
|
props: {},
|
||||||
});
|
});
|
||||||
} 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,
|
||||||
props: {error, withTrace: true},
|
props: { error, withTrace: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
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]) => {
|
||||||
return cum;
|
cum[key] = value;
|
||||||
}, {})
|
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,23 +115,26 @@ 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);
|
||||||
if (values.length < size) return;
|
if (values.length < size) return;
|
||||||
|
|
||||||
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: {
|
||||||
winTracker: {won, nbOfPause: pauses, rank},
|
accLeft,
|
||||||
hitTracker: {bombHit, miss, missedNotes, badCuts, nbOfWallHit: wallHit, maxCombo},
|
accRight,
|
||||||
scoreTracker: {score},
|
leftAverageCut,
|
||||||
|
rightAverageCut,
|
||||||
|
leftTimeDependence,
|
||||||
|
rightTimeDependence,
|
||||||
|
leftPreswing,
|
||||||
|
leftPostswing,
|
||||||
|
rightPreswing,
|
||||||
|
rightPostswing,
|
||||||
|
},
|
||||||
|
winTracker: { won, nbOfPause: pauses, rank },
|
||||||
|
hitTracker: {
|
||||||
|
bombHit,
|
||||||
|
miss,
|
||||||
|
missedNotes,
|
||||||
|
badCuts,
|
||||||
|
nbOfWallHit: wallHit,
|
||||||
|
maxCombo,
|
||||||
|
},
|
||||||
|
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 ({
|
||||||
const response = await get({...getOptions, priority});
|
priority = queue.PRIORITY.FG_LOW,
|
||||||
|
fullResponse = false,
|
||||||
|
...getOptions
|
||||||
|
} = {}) => {
|
||||||
|
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 ({
|
||||||
const response = await clientGet({...getOptions, priority, fullResponse});
|
priority = queue.PRIORITY.FG_LOW,
|
||||||
|
fullResponse = false,
|
||||||
|
...getOptions
|
||||||
|
} = {}) => {
|
||||||
|
const response = await clientGet({ ...getOptions, priority, fullResponse });
|
||||||
|
|
||||||
const processedResponse = process(fullResponse ? getResponseBody(response) : response);
|
const processedResponse = process(
|
||||||
|
fullResponse ? getResponseBody(response) : response,
|
||||||
|
);
|
||||||
|
|
||||||
return fullResponse ? updateResponseBody(response, processedResponse) : processedResponse;
|
return fullResponse
|
||||||
}
|
? updateResponseBody(response, processedResponse)
|
||||||
|
: processedResponse;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
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 {
|
||||||
@ -19,16 +30,20 @@ export default response => {
|
|||||||
name,
|
name,
|
||||||
playerInfo: {
|
playerInfo: {
|
||||||
avatar,
|
avatar,
|
||||||
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,
|
||||||
@ -18,25 +23,44 @@ export default response => {
|
|||||||
...originalScore
|
...originalScore
|
||||||
} = s;
|
} = s;
|
||||||
|
|
||||||
const song = {hash, name, subName, authorName, levelAuthorName};
|
const song = { hash, name, subName, authorName, levelAuthorName };
|
||||||
const diffInfo = extractDiffAndType(difficultyRaw);
|
const diffInfo = extractDiffAndType(difficultyRaw);
|
||||||
const leaderboard = {leaderboardId, song, diffInfo, difficulty};
|
const leaderboard = { leaderboardId, song, diffInfo, difficulty };
|
||||||
|
|
||||||
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(
|
||||||
if (!playerId) return;
|
"player-remove-cmd",
|
||||||
|
async ({ playerId, purgeScores = false }) => {
|
||||||
|
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,
|
||||||
{playerId, add: true},
|
TYPES.ACTIVE_PLAYERS,
|
||||||
async () => enqueue(queue, TYPES.PLAYER_SCORES, true, {playerId}),
|
true,
|
||||||
|
{ playerId, add: true },
|
||||||
|
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,12 +29,17 @@ 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);
|
||||||
|
|
||||||
this.remaining = remaining;
|
this.remaining = remaining;
|
||||||
this.limit = limit;
|
this.limit = limit;
|
||||||
@ -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,65 +21,91 @@ 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);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 () => {
|
||||||
if (lastRateLimitError) {
|
if (lastRateLimitError) {
|
||||||
await lastRateLimitError.waitBeforeRetry();
|
await lastRateLimitError.waitBeforeRetry();
|
||||||
|
|
||||||
lastRateLimitError = null;
|
lastRateLimitError = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
currentRateLimit = {waiting: 0, remaining, limit, resetAt};
|
currentRateLimit = { waiting: 0, remaining, limit, resetAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof SsrNetworkError) {
|
if (err instanceof SsrNetworkError) {
|
||||||
@ -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,55 +1,99 @@
|
|||||||
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,
|
||||||
rateLimit: {waiting: 0, remaining: null, limit: null, resetAt: null},
|
rateLimit: { waiting: 0, remaining: null, limit: null, resetAt: null },
|
||||||
progress: {num: 0, count: 0, progress: 1},
|
progress: { num: 0, count: 0, progress: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
||||||
const {remaining, limit, resetAt} = queue.getRateLimit();
|
rateLimit: { waiting },
|
||||||
queueState = {...queueState, size, pending, rateLimit: {waiting, remaining, limit, resetAt}};
|
} = queueState;
|
||||||
|
const { remaining, limit, resetAt } = queue.getRateLimit();
|
||||||
|
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 {
|
||||||
const {remaining, limit, resetAt} = queue.getRateLimit();
|
rateLimit: { waiting },
|
||||||
queueState = {...queueState, progress: {num, count, progress}, rateLimit: {waiting, remaining, limit, resetAt}}
|
} = queueState;
|
||||||
|
const { remaining, limit, resetAt } = queue.getRateLimit();
|
||||||
|
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,
|
url,
|
||||||
{
|
accessToken,
|
||||||
...options,
|
priority = PRIORITY.FG_LOW,
|
||||||
headers: {
|
options = {},
|
||||||
'Client-ID': CLIENT_ID,
|
) =>
|
||||||
'Authorization': `Bearer ${accessToken}`
|
fetchJson(
|
||||||
}
|
url,
|
||||||
},
|
{
|
||||||
priority,
|
...options,
|
||||||
)
|
headers: {
|
||||||
|
"Client-ID": CLIENT_ID,
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
priority,
|
||||||
|
);
|
||||||
|
|
||||||
const getAuthUrl = (state = '', scopes = '') => substituteVars(AUTHORIZATION_URL, {state: encodeURIComponent(state), scopes: encodeURIComponent(scopes)});
|
const getAuthUrl = (state = "", scopes = "") =>
|
||||||
|
substituteVars(AUTHORIZATION_URL, {
|
||||||
|
state: encodeURIComponent(state),
|
||||||
|
scopes: encodeURIComponent(scopes),
|
||||||
|
});
|
||||||
|
|
||||||
const validateToken = async (accessToken, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(VALIDATE_URL, {...options, headers: {'Authorization': `OAuth ${accessToken}`}}, priority)
|
const validateToken = async (
|
||||||
|
accessToken,
|
||||||
|
priority = PRIORITY.FG_LOW,
|
||||||
|
options = {},
|
||||||
|
) =>
|
||||||
|
fetchJson(
|
||||||
|
VALIDATE_URL,
|
||||||
|
{ ...options, headers: { Authorization: `OAuth ${accessToken}` } },
|
||||||
|
priority,
|
||||||
|
);
|
||||||
|
|
||||||
const profile = async (accessToken, login, priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(PROFILE_URL, {login: encodeURIComponent(login)}), accessToken, priority, options)
|
const profile = async (
|
||||||
|
accessToken,
|
||||||
|
login,
|
||||||
|
priority = PRIORITY.FG_LOW,
|
||||||
|
options = {},
|
||||||
|
) =>
|
||||||
|
fetchApi(
|
||||||
|
substituteVars(PROFILE_URL, { login: encodeURIComponent(login) }),
|
||||||
|
accessToken,
|
||||||
|
priority,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
const videos = async (accessToken, userId, type = 'archive', priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(VIDEOS_URL, {userId: encodeURIComponent(userId), type: encodeURIComponent(type)}), accessToken, priority, options)
|
const videos = async (
|
||||||
|
accessToken,
|
||||||
|
userId,
|
||||||
|
type = "archive",
|
||||||
|
priority = PRIORITY.FG_LOW,
|
||||||
|
options = {},
|
||||||
|
) =>
|
||||||
|
fetchApi(
|
||||||
|
substituteVars(VIDEOS_URL, {
|
||||||
|
userId: encodeURIComponent(userId),
|
||||||
|
type: encodeURIComponent(type),
|
||||||
|
}),
|
||||||
|
accessToken,
|
||||||
|
priority,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
const streams = async (accessToken, userId, priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(STREAMS_URL, {userId: encodeURIComponent(userId)}), accessToken, priority, options)
|
const streams = async (
|
||||||
|
accessToken,
|
||||||
|
userId,
|
||||||
|
priority = PRIORITY.FG_LOW,
|
||||||
|
options = {},
|
||||||
|
) =>
|
||||||
|
fetchApi(
|
||||||
|
substituteVars(STREAMS_URL, { userId: encodeURIComponent(userId) }),
|
||||||
|
accessToken,
|
||||||
|
priority,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
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(
|
||||||
...s,
|
`fetchPlayerScores/${playerId}/${page}`,
|
||||||
leaderboard: {
|
() =>
|
||||||
...s?.leaderboard,
|
accSaberScoresApiClient.getProcessed({
|
||||||
category: categoriesByDisplayName[s?.leaderboard?.categoryDisplayName]?.name ?? null,
|
...options,
|
||||||
}
|
playerId,
|
||||||
}))
|
page,
|
||||||
}
|
priority,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).map((s) => ({
|
||||||
|
...s,
|
||||||
|
leaderboard: {
|
||||||
|
...s?.leaderboard,
|
||||||
|
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);
|
||||||
? roundToPrecision(valFunc(s), precision)
|
let histogramFilterFunc = (s) => s;
|
||||||
: truncateDate(valFunc(s), precision);
|
let roundedValFunc = (s, type = type, precision = bucketSize) =>
|
||||||
let prefix = '';
|
type === "linear"
|
||||||
let prefixLong = '';
|
? roundToPrecision(valFunc(s), precision)
|
||||||
let suffix = '';
|
: truncateDate(valFunc(s), precision);
|
||||||
let suffixLong = '';
|
let prefix = "";
|
||||||
|
let prefixLong = "";
|
||||||
|
let suffix = "";
|
||||||
|
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,164 +244,224 @@ 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: [] };
|
||||||
|
|
||||||
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",
|
||||||
countsTowardsOverall: null,
|
displayName: "Overall",
|
||||||
description: 'Overall'
|
countsTowardsOverall: null,
|
||||||
}]);
|
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) {
|
||||||
|
log.debug(
|
||||||
|
`Player ${player.playerId} history updating error.`,
|
||||||
|
"AccSaberService",
|
||||||
|
e,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch(e) {
|
};
|
||||||
log.debug(`Player ${player.playerId} history updating error.`, 'AccSaberService', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshRanking = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
|
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,243 +1,316 @@
|
|||||||
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;
|
||||||
|
|
||||||
songInfo.hash = hash.toLowerCase();
|
songInfo.hash = hash.toLowerCase();
|
||||||
songInfo.key = songInfo.key.toLowerCase();
|
songInfo.key = songInfo.key.toLowerCase();
|
||||||
|
|
||||||
delete songInfo.description;
|
delete songInfo.description;
|
||||||
|
|
||||||
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 getCurrentSuspension = async () =>
|
||||||
|
cacheRepository().get(BM_SUSPENSION_KEY);
|
||||||
|
const prolongSuspension = async (bsSuspension) => {
|
||||||
|
const current = new Date();
|
||||||
|
|
||||||
|
const suspension = isSuspended(bsSuspension)
|
||||||
|
? bsSuspension
|
||||||
|
: { started: current, activeTo: new Date(), count: 0 };
|
||||||
|
|
||||||
|
suspension.activeTo = addToDate(
|
||||||
|
Math.pow(2, suspension.count) * HOUR,
|
||||||
|
suspension.activeTo,
|
||||||
|
);
|
||||||
|
suspension.count++;
|
||||||
|
|
||||||
|
return await cacheRepository().set(suspension, BM_SUSPENSION_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY);
|
||||||
|
const set404Hashes = async (hashes) =>
|
||||||
|
cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
|
||||||
|
const setHashNotFound = async (hash) => {
|
||||||
|
let songs404 = await get404Hashes();
|
||||||
|
if (!songs404) songs404 = {};
|
||||||
|
|
||||||
|
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()
|
||||||
|
) {
|
||||||
|
item.recentTry = new Date();
|
||||||
|
item.count++;
|
||||||
|
|
||||||
|
songs404[hash] = item;
|
||||||
|
|
||||||
|
await set404Hashes(songs404);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
const isHashUnavailable = async (hash) => {
|
||||||
|
const songs404 = await get404Hashes();
|
||||||
|
return songs404 && songs404[hash] && songs404[hash].count >= 3;
|
||||||
|
};
|
||||||
|
|
||||||
const isSuspended = bsSuspension => !!bsSuspension && bsSuspension.activeTo > new Date() && bsSuspension.started > addToDate(-24 * HOUR);
|
const fixInvalidNotesCount = (hash, songInfo) => {
|
||||||
const getCurrentSuspension = async () => cacheRepository().get(BM_SUSPENSION_KEY);
|
if (!hash) return songInfo;
|
||||||
const prolongSuspension = async bsSuspension => {
|
|
||||||
const current = new Date();
|
|
||||||
|
|
||||||
const suspension = isSuspended(bsSuspension) ? bsSuspension : {started: current, activeTo: new Date(), count: 0};
|
if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions)
|
||||||
|
songInfo.versions.forEach((si) => {
|
||||||
|
if (!si?.diffs) return;
|
||||||
|
|
||||||
suspension.activeTo = addToDate(Math.pow(2, suspension.count) * HOUR, suspension.activeTo);
|
si.diffs.forEach((d) => {
|
||||||
suspension.count++;
|
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(
|
||||||
|
(f) => f.type === d?.characteristic && f.diff === d?.difficulty,
|
||||||
|
);
|
||||||
|
if (!newNotesCnt) return;
|
||||||
|
|
||||||
return await cacheRepository().set(suspension, BM_SUSPENSION_KEY);
|
d.notes = newNotesCnt.notes;
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY);
|
return songInfo;
|
||||||
const set404Hashes = async hashes => cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
|
};
|
||||||
const setHashNotFound = async hash => {
|
|
||||||
let songs404 = await get404Hashes();
|
|
||||||
if (!songs404) songs404 = {};
|
|
||||||
|
|
||||||
const item = songs404[hash] ? songs404[hash] : {firstTry: new Date(), recentTry: null, count: 0};
|
const fetchSong = async (
|
||||||
|
songInfo,
|
||||||
|
fetchFunc,
|
||||||
|
forceUpdate = false,
|
||||||
|
cacheOnly = false,
|
||||||
|
errSongId = "",
|
||||||
|
hash = null,
|
||||||
|
) => {
|
||||||
|
if (!forceUpdate && songInfo) return fixInvalidNotesCount(hash, songInfo);
|
||||||
|
|
||||||
if (!item.recentTry || addToDate(BM_NOT_FOUND_HOURS_BETWEEN_COUNTS * HOUR, item.recentTry) < new Date()) {
|
if (cacheOnly) return null;
|
||||||
item.recentTry = new Date();
|
|
||||||
item.count++;
|
|
||||||
|
|
||||||
songs404[hash] = item;
|
let bsSuspension = await getCurrentSuspension();
|
||||||
|
|
||||||
await set404Hashes(songs404);
|
try {
|
||||||
}
|
if (
|
||||||
}
|
isSuspended(bsSuspension) ||
|
||||||
const isHashUnavailable = async hash => {
|
(hash && (await isHashUnavailable(hash)))
|
||||||
const songs404 = await get404Hashes();
|
)
|
||||||
return songs404 && songs404[hash] && songs404[hash].count >= 3;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
const fixInvalidNotesCount = (hash, songInfo) => {
|
const songInfo = await fetchFunc();
|
||||||
if (!hash) return songInfo;
|
if (!songInfo) {
|
||||||
|
log.warn(`Song "${errSongId}" is no longer available at BeatSaver.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions)
|
return fixInvalidNotesCount(hash, cacheSongInfo(songInfo, hash));
|
||||||
songInfo.versions.forEach(si => {
|
} catch (err) {
|
||||||
if (!si?.diffs) return;
|
if (hash && err instanceof SsrHttpNotFoundError) {
|
||||||
|
await setHashNotFound(hash);
|
||||||
si.diffs.forEach(d => {
|
}
|
||||||
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(f => f.type === d?.characteristic && f.diff === d?.difficulty);
|
|
||||||
if (!newNotesCnt) return;
|
|
||||||
|
|
||||||
d.notes = newNotesCnt.notes;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return songInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchSong = async (songInfo, fetchFunc, forceUpdate = false, cacheOnly = false, errSongId = '', hash = null) => {
|
|
||||||
if (!forceUpdate && songInfo) return fixInvalidNotesCount(hash, songInfo);
|
|
||||||
|
|
||||||
if(cacheOnly) return null;
|
|
||||||
|
|
||||||
let bsSuspension = await getCurrentSuspension();
|
|
||||||
|
|
||||||
|
if (err instanceof SsrNetworkError && err.message === "Network error") {
|
||||||
try {
|
try {
|
||||||
if (isSuspended(bsSuspension) || (hash && await isHashUnavailable(hash))) return null;
|
await prolongSuspension(bsSuspension);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
const songInfo = await fetchFunc();
|
log.warn(`Error fetching BeatSaver song "${errSongId}"`);
|
||||||
if (!songInfo) {
|
|
||||||
log.warn(`Song "${errSongId}" is no longer available at BeatSaver.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fixInvalidNotesCount(hash, cacheSongInfo(songInfo, hash));
|
return null;
|
||||||
} catch (err) {
|
|
||||||
if (hash && err instanceof SsrHttpNotFoundError) {
|
|
||||||
await setHashNotFound(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err instanceof SsrNetworkError && err.message === 'Network error') {
|
|
||||||
try {await prolongSuspension(bsSuspension)} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn(`Error fetching BeatSaver song "${errSongId}"`);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const byHash = async (hash, forceUpdate = false, cacheOnly = false, signal = null, priority = PRIORITY.FG_LOW) => {
|
const byHash = async (
|
||||||
hash = hash.toLowerCase();
|
hash,
|
||||||
|
forceUpdate = false,
|
||||||
|
cacheOnly = false,
|
||||||
|
signal = null,
|
||||||
|
priority = PRIORITY.FG_LOW,
|
||||||
|
) => {
|
||||||
|
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 = key.toLowerCase();
|
key,
|
||||||
|
forceUpdate = false,
|
||||||
|
cacheOnly = false,
|
||||||
|
signal = null,
|
||||||
|
priority = PRIORITY.FG_LOW,
|
||||||
|
) => {
|
||||||
|
key = key.toLowerCase();
|
||||||
|
|
||||||
const songInfo = await songsBeatMapsRepository().getFromIndex('songs-beatmaps-key', key);
|
const songInfo = await songsBeatMapsRepository().getFromIndex(
|
||||||
|
"songs-beatmaps-key",
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
|
||||||
return fetchSong(songInfo, () => keyApiClient.getProcessed({key, signal, priority}), forceUpdate, cacheOnly, key)
|
return fetchSong(
|
||||||
}
|
songInfo,
|
||||||
|
() => keyApiClient.getProcessed({ key, signal, priority }),
|
||||||
|
forceUpdate,
|
||||||
|
cacheOnly,
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const convertOldBeatSaverToBeatMaps = song => {
|
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();
|
||||||
|
|
||||||
const diffs = characteristics.reduce((diffs, ch) => {
|
const diffs = characteristics.reduce((diffs, ch) => {
|
||||||
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,
|
||||||
chroma: null,
|
chroma: null,
|
||||||
me: null,
|
me: null,
|
||||||
ne: null,
|
ne: null,
|
||||||
cinema: null,
|
cinema: null,
|
||||||
seconds,
|
seconds,
|
||||||
paritySummary: {
|
paritySummary: {
|
||||||
errors: null,
|
errors: null,
|
||||||
warns: null,
|
warns: null,
|
||||||
resets: null,
|
resets: null,
|
||||||
},
|
},
|
||||||
stars: null,
|
stars: null,
|
||||||
};
|
};
|
||||||
}))
|
}),
|
||||||
.filter(diff => diff)
|
)
|
||||||
}, []);
|
.filter((diff) => diff);
|
||||||
|
}, []);
|
||||||
return {
|
|
||||||
lastUpdated: dateFromString(opt(song, 'uploaded', new Date())),
|
|
||||||
oldBeatSaverId: opt(song, '_id', null),
|
|
||||||
id: key,
|
|
||||||
hash,
|
|
||||||
key,
|
|
||||||
name,
|
|
||||||
description: '',
|
|
||||||
uploader: {
|
|
||||||
id: null,
|
|
||||||
name: opt(song, 'uploader.username', null),
|
|
||||||
hash: null,
|
|
||||||
avatar: null
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
bpm: opt(song, 'metadata.bpm', null),
|
|
||||||
duration: opt(song, 'metadata.duration', null),
|
|
||||||
songName: opt(song, 'metadata.songName', ''),
|
|
||||||
songSubName: opt(song, 'metadata.songSubName', ''),
|
|
||||||
songAuthorName: opt(song, 'metadata.songAuthorName', ''),
|
|
||||||
levelAuthorName: opt(song, 'metadata.levelAuthorName', '')
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
plays: opt(song, 'stats.plays', 0),
|
|
||||||
downloads: opt(song, 'stats.downloads', 0),
|
|
||||||
upvotes: opt(song, 'stats.upVotes', 0),
|
|
||||||
downvotes: opt(song, 'stats.downVotes', 0),
|
|
||||||
score: null
|
|
||||||
},
|
|
||||||
uploaded: opt(song, 'uploaded', null),
|
|
||||||
automapper: !!opt(song, 'metadata.automapper', false),
|
|
||||||
ranked: null,
|
|
||||||
qualified: null,
|
|
||||||
versions: [
|
|
||||||
{
|
|
||||||
hash,
|
|
||||||
key,
|
|
||||||
state: "Published",
|
|
||||||
createdAt: opt(song, 'uploaded', null),
|
|
||||||
sageScore: null,
|
|
||||||
diffs,
|
|
||||||
downloadURL: `https://cdn.beatsaver.com/${hash}.zip`,
|
|
||||||
coverURL: `https://cdn.beatsaver.com/${hash}.jpg`,
|
|
||||||
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
byHash,
|
lastUpdated: dateFromString(opt(song, "uploaded", new Date())),
|
||||||
byKey,
|
oldBeatSaverId: opt(song, "_id", null),
|
||||||
convertOldBeatSaverToBeatMaps
|
id: key,
|
||||||
}
|
hash,
|
||||||
}
|
key,
|
||||||
|
name,
|
||||||
|
description: "",
|
||||||
|
uploader: {
|
||||||
|
id: null,
|
||||||
|
name: opt(song, "uploader.username", null),
|
||||||
|
hash: null,
|
||||||
|
avatar: null,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
bpm: opt(song, "metadata.bpm", null),
|
||||||
|
duration: opt(song, "metadata.duration", null),
|
||||||
|
songName: opt(song, "metadata.songName", ""),
|
||||||
|
songSubName: opt(song, "metadata.songSubName", ""),
|
||||||
|
songAuthorName: opt(song, "metadata.songAuthorName", ""),
|
||||||
|
levelAuthorName: opt(song, "metadata.levelAuthorName", ""),
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
plays: opt(song, "stats.plays", 0),
|
||||||
|
downloads: opt(song, "stats.downloads", 0),
|
||||||
|
upvotes: opt(song, "stats.upVotes", 0),
|
||||||
|
downvotes: opt(song, "stats.downVotes", 0),
|
||||||
|
score: null,
|
||||||
|
},
|
||||||
|
uploaded: opt(song, "uploaded", null),
|
||||||
|
automapper: !!opt(song, "metadata.automapper", false),
|
||||||
|
ranked: null,
|
||||||
|
qualified: null,
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
hash,
|
||||||
|
key,
|
||||||
|
state: "Published",
|
||||||
|
createdAt: opt(song, "uploaded", null),
|
||||||
|
sageScore: null,
|
||||||
|
diffs,
|
||||||
|
downloadURL: `https://cdn.beatsaver.com/${hash}.zip`,
|
||||||
|
coverURL: `https://cdn.beatsaver.com/${hash}.jpg`,
|
||||||
|
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
byHash,
|
||||||
|
byKey,
|
||||||
|
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}`, () =>
|
||||||
playerId,
|
scoresService.getPlayerScoresAsObject(
|
||||||
score => score?.leaderboard?.song?.hash?.toLowerCase() ?? null,
|
playerId,
|
||||||
true,
|
(score) => score?.leaderboard?.song?.hash?.toLowerCase() ?? null,
|
||||||
)),
|
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) =>
|
||||||
? roundToPrecision(valFunc(s), precision)
|
type === "linear"
|
||||||
: truncateDate(valFunc(s), precision);
|
? roundToPrecision(valFunc(s), precision)
|
||||||
let prefix = '';
|
: truncateDate(valFunc(s), precision);
|
||||||
let prefixLong = '';
|
let prefix = "";
|
||||||
let suffix = '';
|
let prefixLong = "";
|
||||||
let suffixLong = '';
|
let suffix = "";
|
||||||
|
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,37 +182,42 @@ 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;
|
||||||
|
|
||||||
let playerScores = await getPlayerScores(playerId);
|
let playerScores = await getPlayerScores(playerId);
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
if (playerScores.length < startIdx + 1) return {total: 0, scores: []};
|
if (playerScores.length < startIdx + 1) return { total: 0, scores: [] };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
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(
|
||||||
playerId: player.playerId,
|
allPlayers.map(async (player) => ({
|
||||||
beatSavior: await refresh(player.playerId, force, priority, throwErrors),
|
playerId: player.playerId,
|
||||||
})));
|
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 (
|
||||||
`pageClient/leaderboard/${leaderboardId}/${page}`,
|
leaderboardId,
|
||||||
() => leaderboardPageClient.getProcessed({
|
page = 1,
|
||||||
leaderboardId,
|
priority = PRIORITY.FG_LOW,
|
||||||
page,
|
signal = null,
|
||||||
signal,
|
force = false,
|
||||||
priority,
|
) =>
|
||||||
cacheTtl: MINUTE,
|
resolvePromiseOrWaitForPending(
|
||||||
}));
|
`pageClient/leaderboard/${leaderboardId}/${page}`,
|
||||||
|
() =>
|
||||||
|
leaderboardPageClient.getProcessed({
|
||||||
|
leaderboardId,
|
||||||
|
page,
|
||||||
|
signal,
|
||||||
|
priority,
|
||||||
|
cacheTtl: MINUTE,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const fetchAccSaberPage = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, signal = null, force = false) => {
|
const fetchAccSaberPage = async (
|
||||||
|
leaderboardId,
|
||||||
|
page = 1,
|
||||||
|
priority = PRIORITY.FG_LOW,
|
||||||
|
signal = null,
|
||||||
|
force = false,
|
||||||
|
) => {
|
||||||
if (page < 1) page = 1;
|
if (page < 1) page = 1;
|
||||||
|
|
||||||
const data = await resolvePromiseOrWaitForPending(
|
const data = await resolvePromiseOrWaitForPending(
|
||||||
`accSaberApiClient/leaderboard/${leaderboardId}/${page}`,
|
`accSaberApiClient/leaderboard/${leaderboardId}/${page}`,
|
||||||
() => accSaberLeaderboardApiClient.getProcessed({
|
() =>
|
||||||
leaderboardId,
|
accSaberLeaderboardApiClient.getProcessed({
|
||||||
page,
|
leaderboardId,
|
||||||
signal,
|
page,
|
||||||
priority,
|
signal,
|
||||||
cacheTtl: ACCSABER_LEADERBOARD_NETWORK_TTL,
|
priority,
|
||||||
}));
|
cacheTtl: ACCSABER_LEADERBOARD_NETWORK_TTL,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (!data || !data.scores) return data
|
if (!data || !data.scores) return data;
|
||||||
|
|
||||||
const startIdx = (page - 1) * ACCSABER_LEADERBOARD_SCORES_PER_PAGE;
|
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: {
|
||||||
score: {...score.score},
|
playerId: player.playerId,
|
||||||
}
|
name: player.name,
|
||||||
|
playerInfo: { ...player.playerInfo },
|
||||||
|
},
|
||||||
|
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,65 +167,87 @@ 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);
|
||||||
|
|
||||||
const {_idbId, ...previous} = ph;
|
const { _idbId, ...previous } = ph;
|
||||||
|
|
||||||
return previous;
|
return previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 isResponseCached = response => playerApiClient.isResponseCached(response);
|
const fetchPlayerAndUpdateRecentPlay = async (playerId) => {
|
||||||
const getDataFromResponse = response => playerApiClient.getDataFromResponse(response);
|
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;
|
||||||
|
|
||||||
const fetchPlayer = async (playerId, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => resolvePromiseOrWaitForPending(`apiClient/${playerId}/${fullResponse}`, () => playerApiClient.getProcessed({...options, playerId, priority, fullResponse}));
|
return updatePlayerRecentPlay(
|
||||||
|
playerId,
|
||||||
|
recentPlay,
|
||||||
|
recentPlayLastUpdated,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// swallow error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findPlayer = async (query, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => resolvePromiseOrWaitForPending(`apiClient/find/${query}/${fullResponse}`, () => playerFindApiClient.getProcessed({...options, query, priority, fullResponse}));
|
const isResponseCached = (response) =>
|
||||||
|
playerApiClient.isResponseCached(response);
|
||||||
|
const getDataFromResponse = (response) =>
|
||||||
|
playerApiClient.getDataFromResponse(response);
|
||||||
|
|
||||||
const fetchPlayerOrGetFromCache = async (playerId, refreshInterval = MINUTE, priority = PRIORITY.FG_LOW, signal = null, force = false) => {
|
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 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 => {
|
{
|
||||||
fetchPlayerAndUpdateRecentPlay(player.playerId);
|
...player,
|
||||||
|
...getDataFromResponse(fetchedPlayerResponse),
|
||||||
|
profileLastUpdated: new Date(),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
).then((player) => {
|
||||||
|
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,28 +12,32 @@ export default () => {
|
|||||||
|
|
||||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||||
|
|
||||||
const getTotalPp = scores => scores && Array.isArray(scores)
|
const getTotalPp = (scores) =>
|
||||||
? getTotalPpFromSortedPps(
|
scores && Array.isArray(scores)
|
||||||
scores
|
? getTotalPpFromSortedPps(
|
||||||
.filter(s => s.pp > 0)
|
scores
|
||||||
.map(s => s.pp)
|
.filter((s) => s.pp > 0)
|
||||||
.sort((a, b) => b - a),
|
.map((s) => s.pp)
|
||||||
)
|
.sort((a, b) => b - a),
|
||||||
: null;
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
const getTotalPlayerPp = async (playerId, modifiedScores = {}) => getTotalPp(
|
const getTotalPlayerPp = async (playerId, modifiedScores = {}) =>
|
||||||
Object.values({
|
getTotalPp(
|
||||||
...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () => scoresService.getPlayerScoresAsObject(playerId))),
|
Object.values({
|
||||||
...modifiedScores,
|
...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () =>
|
||||||
}),
|
scoresService.getPlayerScoresAsObject(playerId),
|
||||||
);
|
)),
|
||||||
|
...modifiedScores,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
async function getWhatIfScore(playerId, leaderboardId, pp = 0) {
|
async function getWhatIfScore(playerId, leaderboardId, pp = 0) {
|
||||||
const currentTotalPp = await getTotalPlayerPp(playerId);
|
const currentTotalPp = await getTotalPlayerPp(playerId);
|
||||||
if (!currentTotalPp) return null;
|
if (!currentTotalPp) return null;
|
||||||
|
|
||||||
const newTotalPp = await getTotalPlayerPp(playerId, {
|
const newTotalPp = await getTotalPlayerPp(playerId, {
|
||||||
[leaderboardId]: {pp},
|
[leaderboardId]: { pp },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -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(
|
||||||
fetchedRankedSongs = await rankedsPageClient.getProcessed({priority});
|
`Fetching current rankeds from ScoreSaber...`,
|
||||||
|
"RankedsService",
|
||||||
|
);
|
||||||
|
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].firstSeen
|
oldRankedSongs[s.leaderboardId] &&
|
||||||
: new Date();
|
oldRankedSongs[s.leaderboardId].firstSeen
|
||||||
|
? oldRankedSongs[s.leaderboardId].firstSeen
|
||||||
|
: new Date();
|
||||||
|
|
||||||
return {...s, firstSeen, oldStars: null}
|
return { ...s, firstSeen, oldStars: null };
|
||||||
}),
|
}),
|
||||||
'leaderboardId',
|
"leaderboardId",
|
||||||
);
|
);
|
||||||
|
|
||||||
// find differences between old and new ranked songs
|
// 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(
|
||||||
.filter(s => s.stars !== (fetchedRankedSongs[s.leaderboardId] ? opt(fetchedRankedSongs[s.leaderboardId], 'stars', null) : null))
|
(s) =>
|
||||||
.map(s => ({
|
s.stars !==
|
||||||
leaderboardId: s.leaderboardId,
|
(fetchedRankedSongs[s.leaderboardId]
|
||||||
oldStars: s.stars,
|
? opt(fetchedRankedSongs[s.leaderboardId], "stars", null)
|
||||||
stars: opt(fetchedRankedSongs[s.leaderboardId], 'stars', null),
|
: null),
|
||||||
timestamp: Date.now(),
|
)
|
||||||
}),
|
.map((s) => ({
|
||||||
)
|
leaderboardId: s.leaderboardId,
|
||||||
);
|
oldStars: s.stars,
|
||||||
|
stars: opt(fetchedRankedSongs[s.leaderboardId], "stars", null),
|
||||||
|
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,25 +66,33 @@ 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) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,12 +187,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
twitchProfile = {...twitchProfile, ...fetchedProfile, playerId};
|
twitchProfile = { ...twitchProfile, ...fetchedProfile, playerId };
|
||||||
|
|
||||||
await updatePlayerProfile(twitchProfile);
|
await updatePlayerProfile(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,
|
||||||
});
|
});
|
||||||
@ -149,27 +215,48 @@ export default () => {
|
|||||||
|
|
||||||
return twitchProfile;
|
return twitchProfile;
|
||||||
} 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,41 +25,42 @@ 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;
|
||||||
|
|
||||||
let currentConfig = {...DEFAULT_CONFIG};
|
let currentConfig = { ...DEFAULT_CONFIG };
|
||||||
|
|
||||||
let newSettingsAvailable = undefined;
|
let newSettingsAvailable = undefined;
|
||||||
|
|
||||||
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,8 +1,10 @@
|
|||||||
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 {subscribe, unsubscribe, set} = writable(defaultValue);
|
) => {
|
||||||
|
const defaultValue = { name: null, width: null, nodeWidth: null, rect: null };
|
||||||
|
const { subscribe, unsubscribe, set } = writable(defaultValue);
|
||||||
|
|
||||||
let ro = null;
|
let ro = null;
|
||||||
let node = null;
|
let node = 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,17 +1,20 @@
|
|||||||
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;
|
||||||
|
|
||||||
export default async (data) => {
|
export default async (data) => {
|
||||||
if (rankeds === undefined) {
|
if (rankeds === undefined) {
|
||||||
rankeds = (await createRankedsStore()).get();
|
rankeds = (await createRankedsStore()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
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].score;
|
? playerScores[leaderboardId].history &&
|
||||||
|
playerScores[leaderboardId].history.length
|
||||||
|
? playerScores[leaderboardId].history[0]
|
||||||
|
: null
|
||||||
|
: 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) =>
|
||||||
if (!enhancePatches[patchId]) enhancePatches[patchId] = [];
|
produce(state, producer, (patches) => {
|
||||||
|
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;
|
||||||
@ -46,78 +55,119 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
|
|||||||
set(state);
|
set(state);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
const newState = {...state};
|
const newState = { ...state };
|
||||||
|
|
||||||
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,
|
{
|
||||||
leaderboard: newState.leaderboard
|
...scoreRow,
|
||||||
}, getPatchId(currentLeaderboardId, scoreRow), draft => accEnhancer(draft))
|
leaderboard: newState.leaderboard,
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
},
|
||||||
.then(scoreRow => stateProduce({...scoreRow, leaderboard: newState.leaderboard}, getPatchId(currentLeaderboardId, scoreRow), draft => ppAttributionEnhancer(draft, scoreRow?.player?.playerId, true))
|
getPatchId(currentLeaderboardId, scoreRow),
|
||||||
|
(draft) => accEnhancer(draft),
|
||||||
|
)
|
||||||
|
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
|
||||||
|
.then((scoreRow) =>
|
||||||
|
stateProduce(
|
||||||
|
{ ...scoreRow, leaderboard: newState.leaderboard },
|
||||||
|
getPatchId(currentLeaderboardId, scoreRow),
|
||||||
|
(draft) =>
|
||||||
|
ppAttributionEnhancer(
|
||||||
|
draft,
|
||||||
|
scoreRow?.player?.playerId,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const provider = createLeaderboardPageProvider();
|
const provider = createLeaderboardPageProvider();
|
||||||
|
|
||||||
const httpStore = createHttpStore(
|
const httpStore = createHttpStore(
|
||||||
provider,
|
provider,
|
||||||
{leaderboardId, type, page},
|
{ leaderboardId, type, page },
|
||||||
initialState,
|
initialState,
|
||||||
{
|
{
|
||||||
onInitialized: onNewData,
|
onInitialized: onNewData,
|
||||||
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,
|
||||||
@ -126,7 +176,6 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
|
|||||||
getLeaderboardId: () => currentLeaderboardId,
|
getLeaderboardId: () => currentLeaderboardId,
|
||||||
getType: () => currentType,
|
getType: () => currentType,
|
||||||
getPage: () => currentPage,
|
getPage: () => currentPage,
|
||||||
enhanced: {subscribe: subscribeEnhanced},
|
enhanced: { subscribe: subscribeEnhanced },
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
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,
|
||||||
playerId ? {playerId} : null,
|
playerId ? { playerId } : null,
|
||||||
initialState,
|
initialState,
|
||||||
{
|
{
|
||||||
onInitialized: onNewData,
|
onInitialized: onNewData,
|
||||||
@ -22,13 +26,12 @@ export default (playerId = null, initialState = null, initialStateType = 'initia
|
|||||||
const fetch = async (playerId = currentPlayerId, force = false) => {
|
const fetch = async (playerId = currentPlayerId, force = false) => {
|
||||||
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,49 +1,61 @@
|
|||||||
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;
|
||||||
|
|
||||||
const {subscribe: subscribeParams, set: setParams} = writable(null);
|
const { subscribe: subscribeParams, set: setParams } = writable(null);
|
||||||
|
|
||||||
let playerService = createPlayerService();
|
let playerService = createPlayerService();
|
||||||
|
|
||||||
let lastRecentPlay = null;
|
let lastRecentPlay = null;
|
||||||
let playerForLastRecentPlay = null;
|
let playerForLastRecentPlay = null;
|
||||||
|
|
||||||
const onNewData = ({fetchParams}) => {
|
const onNewData = ({ fetchParams }) => {
|
||||||
currentPlayerId = fetchParams?.playerId ?? null;
|
currentPlayerId = fetchParams?.playerId ?? null;
|
||||||
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();
|
||||||
|
|
||||||
const httpStore = createHttpStore(
|
const httpStore = createHttpStore(
|
||||||
provider,
|
provider,
|
||||||
playerId ? {playerId, service, serviceParams} : null,
|
playerId ? { playerId, service, serviceParams } : null,
|
||||||
initialState,
|
initialState,
|
||||||
{
|
{
|
||||||
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,75 +66,89 @@ 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(
|
||||||
if (!playerId || !currentPlayerId || playerId !== currentPlayerId) return;
|
"player-recent-play-updated",
|
||||||
|
async ({ playerId, recentPlay }) => {
|
||||||
|
if (!playerId || !currentPlayerId || playerId !== currentPlayerId) return;
|
||||||
|
|
||||||
if (!recentPlay || !lastRecentPlay || recentPlay <= lastRecentPlay) {
|
if (!recentPlay || !lastRecentPlay || recentPlay <= lastRecentPlay) {
|
||||||
if (recentPlay) {
|
if (recentPlay) {
|
||||||
lastRecentPlay = recentPlay;
|
lastRecentPlay = recentPlay;
|
||||||
playerForLastRecentPlay = playerId;
|
playerForLastRecentPlay = playerId;
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastRecentPlay = recentPlay;
|
lastRecentPlay = recentPlay;
|
||||||
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 =
|
||||||
? DEFAULT_RECENT_PLAY_REFRESH_INTERVAL
|
!lastRecentPlay || lastRecentPlay >= addToDate(-30 * MINUTE, new Date())
|
||||||
: 15 * MINUTE;
|
? DEFAULT_RECENT_PLAY_REFRESH_INTERVAL
|
||||||
|
: 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,
|
||||||
subscribe,
|
subscribe,
|
||||||
fetch,
|
fetch,
|
||||||
refresh,
|
refresh,
|
||||||
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,34 +1,48 @@
|
|||||||
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();
|
||||||
|
|
||||||
const httpStore = createHttpStore(
|
const httpStore = createHttpStore(
|
||||||
provider,
|
provider,
|
||||||
{type, page},
|
{ type, page },
|
||||||
initialState,
|
initialState,
|
||||||
{
|
{
|
||||||
onInitialized: onNewData,
|
onInitialized: onNewData,
|
||||||
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,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user