add and run prettier
This commit is contained in:
parent
47a23f0484
commit
f6f56aa09c
194
LICENSE.md
194
LICENSE.md
@ -1,16 +1,16 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
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.
|
||||
|
||||
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,
|
||||
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
|
||||
@ -19,35 +19,35 @@ 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
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
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
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
@ -58,49 +58,49 @@ products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
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
|
||||
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
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
"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
|
||||
"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
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
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.
|
||||
|
||||
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
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
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
|
||||
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
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
@ -109,18 +109,18 @@ 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
|
||||
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
|
||||
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
|
||||
interfaces specified for a particular programming language, one that
|
||||
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
|
||||
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
|
||||
@ -131,7 +131,7 @@ implementation is available to the public in source code form. A
|
||||
(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.
|
||||
|
||||
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
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
@ -144,16 +144,16 @@ linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
@ -161,7 +161,7 @@ covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
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
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
@ -172,19 +172,19 @@ for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
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
|
||||
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
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
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
|
||||
is effected by exercising rights under this License with respect to
|
||||
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
|
||||
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
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
@ -232,7 +232,7 @@ terms of section 4, provided that you also meet all of these conditions:
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
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,
|
||||
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
|
||||
@ -242,9 +242,9 @@ beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
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
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
@ -290,11 +290,11 @@ in one of these ways:
|
||||
Source of the work are being offered to the general public at no
|
||||
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
|
||||
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,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
@ -307,7 +307,7 @@ is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
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
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
@ -315,7 +315,7 @@ suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
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
|
||||
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
|
||||
@ -326,7 +326,7 @@ if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
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
|
||||
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
|
||||
@ -334,15 +334,15 @@ network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
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
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
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.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
@ -351,14 +351,14 @@ apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
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
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
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
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
@ -385,7 +385,7 @@ that material) supplement the terms of this License with terms:
|
||||
any liability that these contractual assumptions directly impose on
|
||||
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
|
||||
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
|
||||
@ -395,46 +395,46 @@ License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
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
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
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;
|
||||
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
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
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)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
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
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
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
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
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
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
@ -443,14 +443,14 @@ modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
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
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
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 merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
@ -460,7 +460,7 @@ give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
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
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
@ -468,13 +468,13 @@ rights granted under this License, and you may not initiate litigation
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
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
|
||||
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
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
@ -484,19 +484,19 @@ purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
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
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
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
|
||||
(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
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
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
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
@ -510,7 +510,7 @@ 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
|
||||
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
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
@ -518,7 +518,7 @@ or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
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
|
||||
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
|
||||
@ -533,13 +533,13 @@ for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
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
|
||||
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
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
@ -549,9 +549,9 @@ 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
|
||||
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
|
||||
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
|
||||
@ -560,14 +560,14 @@ but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
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
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
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
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
@ -576,19 +576,19 @@ Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
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
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
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
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
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
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
@ -597,9 +597,9 @@ 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
|
||||
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
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
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
|
||||
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,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
@ -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.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
@ -661,12 +661,12 @@ The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
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".
|
||||
|
||||
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.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<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
|
||||
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
|
||||
|
@ -24,6 +24,7 @@
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"luxon": "^2.0.2",
|
||||
"p-queue": "^7.1.0",
|
||||
"prettier": "3.0.3",
|
||||
"rollup": "^2.3.4",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,7 @@ html {
|
||||
|
||||
body {
|
||||
color: var(--textColor);
|
||||
background-color: var(--background)!important;
|
||||
background-color: var(--background) !important;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
min-height: 100vh;
|
||||
@ -54,7 +54,9 @@ select {
|
||||
color: var(--decrease);
|
||||
}
|
||||
|
||||
*[title]:not([title=""]):not(.clickable) {cursor: help;}
|
||||
*[title]:not([title=""]):not(.clickable) {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.scoresaber-icon {
|
||||
width: 100%;
|
||||
@ -69,8 +71,7 @@ select {
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='72.723976mm' height='63.291668mm' viewBox='0 0 72.723976 63.291668' version='1.1' id='svg3827' sodipodi:docname='bsicon_ter.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cdefs id='defs3821' /%3E%3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='1.4' inkscape:cx='40.905424' inkscape:cy='61.353566' inkscape:document-units='mm' inkscape:current-layer='layer1' showgrid='false' inkscape:window-width='1920' inkscape:window-height='1017' inkscape:window-x='-8' inkscape:window-y='-8' inkscape:window-maximized='1' /%3E%3Cmetadata id='metadata3824'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3Cdc:title%3E%3C/dc:title%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg inkscape:label='Calque 1' inkscape:groupmode='layer' id='layer1' style='opacity:1' transform='translate(-0.72553574,-0.71711111)'%3E%3Crect style='fill:%23000200;fill-opacity:1;stroke:%23000000;stroke-width:1.98928511;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4531' width='63.5' height='10.583332' x='5.4550524' y='10.242103' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Crect style='fill:%23ffffff;fill-opacity:1;stroke:none;stroke-width:3.35483217;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4533' width='6.6145835' height='6.6145835' x='25.298788' y='11.565023' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Crect style='fill:%23ffffff;fill-opacity:1;stroke:none;stroke-width:3.92078424;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4533-5' width='6.6145835' height='6.6145835' x='42.496708' y='11.565023' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Cg id='g4614' transform='rotate(-23.417079,-23.695385,307.31208)' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757'%3E%3Crect y='86.127083' x='125.67709' height='1.3229166' width='50.270832' id='rect4610' style='fill:%230000ff;fill-opacity:1;stroke:%230000ff;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Crect y='85.333328' x='109.80208' height='2.6458333' width='15.875' id='rect4605' style='fill:%23000000;fill-opacity:1;stroke:%23000000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3C/g%3E%3Cg transform='rotate(-156.98422,82.908484,73.919009)' id='g4614-3' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757'%3E%3Crect y='86.127083' x='125.67709' height='1.3229166' width='50.270832' id='rect4610-6' style='fill:%23ff0000;fill-opacity:1;stroke:%23ff0000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Crect y='85.333328' x='109.80208' height='2.6458333' width='15.875' id='rect4605-7' style='fill:%23000000;fill-opacity:1;stroke:%23000000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A"
|
||||
);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='72.723976mm' height='63.291668mm' viewBox='0 0 72.723976 63.291668' version='1.1' id='svg3827' sodipodi:docname='bsicon_ter.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cdefs id='defs3821' /%3E%3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='1.4' inkscape:cx='40.905424' inkscape:cy='61.353566' inkscape:document-units='mm' inkscape:current-layer='layer1' showgrid='false' inkscape:window-width='1920' inkscape:window-height='1017' inkscape:window-x='-8' inkscape:window-y='-8' inkscape:window-maximized='1' /%3E%3Cmetadata id='metadata3824'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3Cdc:title%3E%3C/dc:title%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg inkscape:label='Calque 1' inkscape:groupmode='layer' id='layer1' style='opacity:1' transform='translate(-0.72553574,-0.71711111)'%3E%3Crect style='fill:%23000200;fill-opacity:1;stroke:%23000000;stroke-width:1.98928511;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4531' width='63.5' height='10.583332' x='5.4550524' y='10.242103' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Crect style='fill:%23ffffff;fill-opacity:1;stroke:none;stroke-width:3.35483217;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4533' width='6.6145835' height='6.6145835' x='25.298788' y='11.565023' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Crect style='fill:%23ffffff;fill-opacity:1;stroke:none;stroke-width:3.92078424;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal' id='rect4533-5' width='6.6145835' height='6.6145835' x='42.496708' y='11.565023' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757' /%3E%3Cg id='g4614' transform='rotate(-23.417079,-23.695385,307.31208)' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757'%3E%3Crect y='86.127083' x='125.67709' height='1.3229166' width='50.270832' id='rect4610' style='fill:%230000ff;fill-opacity:1;stroke:%230000ff;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Crect y='85.333328' x='109.80208' height='2.6458333' width='15.875' id='rect4605' style='fill:%23000000;fill-opacity:1;stroke:%23000000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3C/g%3E%3Cg transform='rotate(-156.98422,82.908484,73.919009)' id='g4614-3' inkscape:export-xdpi='81.844757' inkscape:export-ydpi='81.844757'%3E%3Crect y='86.127083' x='125.67709' height='1.3229166' width='50.270832' id='rect4610-6' style='fill:%23ff0000;fill-opacity:1;stroke:%23ff0000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Crect y='85.333328' x='109.80208' height='2.6458333' width='15.875' id='rect4605-7' style='fill:%23000000;fill-opacity:1;stroke:%23000000;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.accsaber-icon {
|
||||
@ -161,7 +162,7 @@ select {
|
||||
}
|
||||
|
||||
.tablet-and-up {
|
||||
display: none!important;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +172,7 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width:768px) and (max-width: 1023px) {
|
||||
@media screen and (min-width: 768px) and (max-width: 1023px) {
|
||||
.tablet-only {
|
||||
display: block;
|
||||
}
|
||||
@ -183,7 +184,7 @@ select {
|
||||
}
|
||||
|
||||
.desktop-and-up {
|
||||
display: none!important;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
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>
|
||||
* @license MIT
|
||||
*/
|
||||
!function(t,e){"use strict";"function"!=typeof t.CustomEvent&&(t.CustomEvent=function(t,n){n=n||{bubbles:!1,cancelable:!1,detail:void 0};var a=e.createEvent("CustomEvent");return a.initCustomEvent(t,n.bubbles,n.cancelable,n.detail),a},t.CustomEvent.prototype=t.Event.prototype),e.addEventListener("touchstart",function(t){if("true"===t.target.getAttribute("data-swipe-ignore"))return;s=t.target,r=Date.now(),n=t.touches[0].clientX,a=t.touches[0].clientY,u=0,i=0},!1),e.addEventListener("touchmove",function(t){if(!n||!a)return;var e=t.touches[0].clientX,r=t.touches[0].clientY;u=n-e,i=a-r},!1),e.addEventListener("touchend",function(t){if(s!==t.target)return;var e=parseInt(l(s,"data-swipe-threshold","20"),10),o=parseInt(l(s,"data-swipe-timeout","500"),10),c=Date.now()-r,d="",p=t.changedTouches||t.touches||[];Math.abs(u)>Math.abs(i)?Math.abs(u)>e&&c<o&&(d=u>0?"swiped-left":"swiped-right"):Math.abs(i)>e&&c<o&&(d=i>0?"swiped-up":"swiped-down");if(""!==d){var b={dir:d.replace(/swiped-/,""),xStart:parseInt(n,10),xEnd:parseInt((p[0]||{}).clientX||-1,10),yStart:parseInt(a,10),yEnd:parseInt((p[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent("swiped",{bubbles:!0,cancelable:!0,detail:b})),s.dispatchEvent(new CustomEvent(d,{bubbles:!0,cancelable:!0,detail:b}))}n=null,a=null,r=null},!1);var n=null,a=null,u=null,i=null,r=null,s=null;function l(t,n,a){for(;t&&t!==e.documentElement;){var u=t.getAttribute(n);if(u)return u;t=t.parentNode}return a}}(window,document);
|
||||
!(function (t, e) {
|
||||
"use strict";
|
||||
"function" != typeof t.CustomEvent &&
|
||||
((t.CustomEvent = function (t, n) {
|
||||
n = n || { bubbles: !1, cancelable: !1, detail: void 0 };
|
||||
var a = e.createEvent("CustomEvent");
|
||||
return a.initCustomEvent(t, n.bubbles, n.cancelable, n.detail), a;
|
||||
}),
|
||||
(t.CustomEvent.prototype = t.Event.prototype)),
|
||||
e.addEventListener(
|
||||
"touchstart",
|
||||
function (t) {
|
||||
if ("true" === t.target.getAttribute("data-swipe-ignore")) return;
|
||||
(s = t.target),
|
||||
(r = Date.now()),
|
||||
(n = t.touches[0].clientX),
|
||||
(a = t.touches[0].clientY),
|
||||
(u = 0),
|
||||
(i = 0);
|
||||
},
|
||||
!1,
|
||||
),
|
||||
e.addEventListener(
|
||||
"touchmove",
|
||||
function (t) {
|
||||
if (!n || !a) return;
|
||||
var e = t.touches[0].clientX,
|
||||
r = t.touches[0].clientY;
|
||||
(u = n - e), (i = a - r);
|
||||
},
|
||||
!1,
|
||||
),
|
||||
e.addEventListener(
|
||||
"touchend",
|
||||
function (t) {
|
||||
if (s !== t.target) return;
|
||||
var e = parseInt(l(s, "data-swipe-threshold", "20"), 10),
|
||||
o = parseInt(l(s, "data-swipe-timeout", "500"), 10),
|
||||
c = Date.now() - r,
|
||||
d = "",
|
||||
p = t.changedTouches || t.touches || [];
|
||||
Math.abs(u) > Math.abs(i)
|
||||
? Math.abs(u) > e &&
|
||||
c < o &&
|
||||
(d = u > 0 ? "swiped-left" : "swiped-right")
|
||||
: Math.abs(i) > e &&
|
||||
c < o &&
|
||||
(d = i > 0 ? "swiped-up" : "swiped-down");
|
||||
if ("" !== d) {
|
||||
var b = {
|
||||
dir: d.replace(/swiped-/, ""),
|
||||
xStart: parseInt(n, 10),
|
||||
xEnd: parseInt((p[0] || {}).clientX || -1, 10),
|
||||
yStart: parseInt(a, 10),
|
||||
yEnd: parseInt((p[0] || {}).clientY || -1, 10),
|
||||
};
|
||||
s.dispatchEvent(
|
||||
new CustomEvent("swiped", {
|
||||
bubbles: !0,
|
||||
cancelable: !0,
|
||||
detail: b,
|
||||
}),
|
||||
),
|
||||
s.dispatchEvent(
|
||||
new CustomEvent(d, { bubbles: !0, cancelable: !0, detail: b }),
|
||||
);
|
||||
}
|
||||
(n = null), (a = null), (r = null);
|
||||
},
|
||||
!1,
|
||||
);
|
||||
var n = null,
|
||||
a = null,
|
||||
u = null,
|
||||
i = null,
|
||||
r = null,
|
||||
s = null;
|
||||
function l(t, n, a) {
|
||||
for (; t && t !== e.documentElement; ) {
|
||||
var u = t.getAttribute(n);
|
||||
if (u) return u;
|
||||
t = t.parentNode;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
})(window, document);
|
||||
|
@ -1,21 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
|
||||
<title>ScoreSaber Reloaded</title>
|
||||
|
||||
<link rel='icon' type='image/png' href='/assets/favicon.png' />
|
||||
<link rel='stylesheet' href='/assets/ss-bulma.css' />
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<link rel="stylesheet" href="/assets/ss-bulma.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://use.fontawesome.com/releases/v5.15.4/css/all.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="/assets/ssr.css?20210925" />
|
||||
<link rel='stylesheet' href='/build/bundle.css' />
|
||||
<link rel="stylesheet" href="/build/bundle.css" />
|
||||
|
||||
<script src="/assets/swiped-events.min.js"></script>
|
||||
<script defer src='/build/bundle.js'></script>
|
||||
</head>
|
||||
<script defer src="/build/bundle.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
<body></body>
|
||||
</html>
|
||||
|
@ -1,22 +1,27 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
import svg from 'rollup-plugin-svg';
|
||||
import svelte from "rollup-plugin-svelte";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import livereload from "rollup-plugin-livereload";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
import css from "rollup-plugin-css-only";
|
||||
import svg from "rollup-plugin-svg";
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
const buildVersion = execSync("git rev-parse --short HEAD").toString();
|
||||
fs.writeFileSync('build-info.js', 'export default ' + JSON.stringify({
|
||||
buildDate: (new Date()).toISOString().substr(0, 19).replace('T', ' ') + ' UTC',
|
||||
buildVersion
|
||||
}))
|
||||
fs.writeFileSync(
|
||||
"build-info.js",
|
||||
"export default " +
|
||||
JSON.stringify({
|
||||
buildDate:
|
||||
new Date().toISOString().substr(0, 19).replace("T", " ") + " UTC",
|
||||
buildVersion,
|
||||
}),
|
||||
);
|
||||
|
||||
function serve() {
|
||||
let server;
|
||||
@ -28,29 +33,33 @@ function serve() {
|
||||
return {
|
||||
writeBundle() {
|
||||
if (server) return;
|
||||
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
shell: true
|
||||
});
|
||||
server = require("child_process").spawn(
|
||||
"npm",
|
||||
["run", "start", "--", "--dev"],
|
||||
{
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
shell: true,
|
||||
},
|
||||
);
|
||||
|
||||
process.on('SIGTERM', toExit);
|
||||
process.on('exit', toExit);
|
||||
}
|
||||
process.on("SIGTERM", toExit);
|
||||
process.on("exit", toExit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/main.js',
|
||||
input: "src/main.js",
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js',
|
||||
format: "iife",
|
||||
name: "app",
|
||||
file: "public/build/bundle.js",
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
preprocess: sveltePreprocess({sourceMap: !production}),
|
||||
preprocess: sveltePreprocess({ sourceMap: !production }),
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
@ -58,7 +67,7 @@ export default [
|
||||
}),
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({output: 'bundle.css'}),
|
||||
css({ output: "bundle.css" }),
|
||||
|
||||
svg(),
|
||||
|
||||
@ -69,7 +78,7 @@ export default [
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ['svelte'],
|
||||
dedupe: ["svelte"],
|
||||
}),
|
||||
commonjs(),
|
||||
|
||||
@ -79,23 +88,23 @@ export default [
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload('public'),
|
||||
!production && livereload("public"),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser(),
|
||||
|
||||
{
|
||||
name: 'copy-comlink',
|
||||
name: "copy-comlink",
|
||||
generateBundle() {
|
||||
const buildDir = './public/build'
|
||||
if (!fs.existsSync(buildDir)){
|
||||
const buildDir = "./public/build";
|
||||
if (!fs.existsSync(buildDir)) {
|
||||
fs.mkdirSync(buildDir);
|
||||
}
|
||||
|
||||
fs.copyFileSync(
|
||||
path.resolve('./node_modules/comlink/dist/umd/comlink.min.js'),
|
||||
path.resolve('./public/build/comlink.min.js'),
|
||||
path.resolve("./node_modules/comlink/dist/umd/comlink.min.js"),
|
||||
path.resolve("./public/build/comlink.min.js"),
|
||||
);
|
||||
},
|
||||
},
|
||||
@ -106,12 +115,12 @@ export default [
|
||||
},
|
||||
|
||||
{
|
||||
input: 'src/workers/stats-worker.js',
|
||||
input: "src/workers/stats-worker.js",
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/stats-worker.js',
|
||||
format: "iife",
|
||||
name: "app",
|
||||
file: "public/build/stats-worker.js",
|
||||
},
|
||||
plugins: [
|
||||
// If you have external dependencies installed from
|
||||
@ -121,7 +130,7 @@ export default [
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ['svelte'],
|
||||
dedupe: ["svelte"],
|
||||
}),
|
||||
commonjs(),
|
||||
|
||||
@ -129,19 +138,19 @@ export default [
|
||||
// instead of npm run dev), minify
|
||||
production && terser(),
|
||||
{
|
||||
name: 'copy-test-worker',
|
||||
name: "copy-test-worker",
|
||||
load() {
|
||||
this.addWatchFile(path.resolve('./src/workers/stats-worker.js'));
|
||||
this.addWatchFile(path.resolve("./src/workers/stats-worker.js"));
|
||||
},
|
||||
generateBundle() {
|
||||
const buildDir = './public/build'
|
||||
if (!fs.existsSync(buildDir)){
|
||||
const buildDir = "./public/build";
|
||||
if (!fs.existsSync(buildDir)) {
|
||||
fs.mkdirSync(buildDir);
|
||||
}
|
||||
|
||||
fs.copyFileSync(
|
||||
path.resolve('./src/workers/stats-worker.js'),
|
||||
path.resolve('./public/build/stats-worker.js'),
|
||||
path.resolve("./src/workers/stats-worker.js"),
|
||||
path.resolve("./public/build/stats-worker.js"),
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -5,7 +5,7 @@ export const onLegendClick = (event, legendItem, legend) => {
|
||||
const scales = legend?.chart?.config?.options?.scales;
|
||||
if (!scales) return;
|
||||
|
||||
const {x: xAxis, ...yAxes} = scales;
|
||||
const { x: xAxis, ...yAxes } = scales;
|
||||
|
||||
if (ci.isDatasetVisible(idx)) {
|
||||
ci.hide(idx);
|
||||
@ -17,15 +17,23 @@ export const onLegendClick = (event, legendItem, legend) => {
|
||||
|
||||
if (legend?.chart) {
|
||||
const yAxisIdsToShow = (legend?.legendItems ?? [])
|
||||
.sort((a,b) => (ci?.config?.data?.datasets?.[a?.datasetIndex]?.axisOrder ?? a?.datasetIndex) - (ci?.config?.data?.datasets?.[b?.datasetIndex]?.axisOrder ?? b?.datasetIndex))
|
||||
.reduce((cum, legendItem) => {
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(ci?.config?.data?.datasets?.[a?.datasetIndex]?.axisOrder ??
|
||||
a?.datasetIndex) -
|
||||
(ci?.config?.data?.datasets?.[b?.datasetIndex]?.axisOrder ??
|
||||
b?.datasetIndex),
|
||||
)
|
||||
.reduce(
|
||||
(cum, legendItem) => {
|
||||
// done
|
||||
if (cum.second) return cum;
|
||||
|
||||
// skip hidden legend items
|
||||
if (legendItem?.hidden) return cum;
|
||||
|
||||
const yAxisId = ci?.getDatasetMeta(legendItem?.datasetIndex)?.yAxisID ?? null;
|
||||
const yAxisId =
|
||||
ci?.getDatasetMeta(legendItem?.datasetIndex)?.yAxisID ?? null;
|
||||
if (!yAxisId) return cum;
|
||||
|
||||
if (!cum.first) {
|
||||
@ -35,20 +43,26 @@ export const onLegendClick = (event, legendItem, legend) => {
|
||||
}
|
||||
|
||||
return cum;
|
||||
}, {first: null, second: null});
|
||||
},
|
||||
{ first: null, second: null },
|
||||
);
|
||||
|
||||
Object.keys(yAxes).forEach(currentAxisKey => {
|
||||
if (![yAxisIdsToShow.first, yAxisIdsToShow.second].includes(currentAxisKey)) {
|
||||
Object.keys(yAxes).forEach((currentAxisKey) => {
|
||||
if (
|
||||
![yAxisIdsToShow.first, yAxisIdsToShow.second].includes(currentAxisKey)
|
||||
) {
|
||||
yAxes[currentAxisKey].display = false;
|
||||
return;
|
||||
}
|
||||
|
||||
yAxes[currentAxisKey].display = true;
|
||||
if (yAxisIdsToShow.first === currentAxisKey) yAxes[currentAxisKey].position = 'left';
|
||||
if (yAxisIdsToShow.second === currentAxisKey) yAxes[currentAxisKey].position = 'right';
|
||||
if (yAxisIdsToShow.first === currentAxisKey)
|
||||
yAxes[currentAxisKey].position = "left";
|
||||
if (yAxisIdsToShow.second === currentAxisKey)
|
||||
yAxes[currentAxisKey].position = "right";
|
||||
});
|
||||
|
||||
legend.chart.options.scales = {x: xAxis, ...yAxes}
|
||||
legend.chart.options.scales = { x: xAxis, ...yAxes };
|
||||
legend.chart.update();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,22 +1,29 @@
|
||||
export default {
|
||||
id: 'regions',
|
||||
id: "regions",
|
||||
beforeDraw(chart, args, options) {
|
||||
if (!options?.regions || !Array.isArray(options.regions)) return;
|
||||
|
||||
const {ctx, chartArea: {left, top, right, bottom}, scales: {y}} = chart;
|
||||
const {
|
||||
ctx,
|
||||
chartArea: { left, top, right, bottom },
|
||||
scales: { y },
|
||||
} = chart;
|
||||
const width = right - left;
|
||||
|
||||
let fontSize = parseInt(ctx.font,10);
|
||||
let fontSize = parseInt(ctx.font, 10);
|
||||
if (isNaN(fontSize)) fontSize = 12;
|
||||
|
||||
ctx.save();
|
||||
|
||||
options.regions.forEach(region => {
|
||||
options.regions.forEach((region) => {
|
||||
if (y.min <= region.max && y.max >= region.min) {
|
||||
const minY = Math.max(region.min, y.min);
|
||||
const maxY = Math.min(region.max, y.max);
|
||||
const top = y.getPixelForValue(maxY);
|
||||
const height = region.min === region.max ? 1 : y.getPixelForValue(minY) - y.getPixelForValue(maxY);
|
||||
const height =
|
||||
region.min === region.max
|
||||
? 1
|
||||
: y.getPixelForValue(minY) - y.getPixelForValue(maxY);
|
||||
|
||||
ctx.fillStyle = region.color;
|
||||
ctx.fillRect(left, top, width, height);
|
||||
@ -24,16 +31,20 @@ export default {
|
||||
if (region.label) {
|
||||
const labelWidth = ctx.measureText(region.label)?.width ?? 0;
|
||||
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.textBaseline = "top";
|
||||
ctx.fillText(
|
||||
region.label,
|
||||
region?.position?.horizontal === 'right' ? right - labelWidth - 3 : left + 3,
|
||||
region?.position?.vertical === 'bottom' ? top + 2 : top - fontSize - 1
|
||||
region?.position?.horizontal === "right"
|
||||
? right - labelWidth - 3
|
||||
: left + 3,
|
||||
region?.position?.vertical === "bottom"
|
||||
? top + 2
|
||||
: top - fontSize - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -70,7 +70,7 @@ function updateScoresStats(playerData, playerStats) {
|
||||
bgColor: "var(--ppColour)",
|
||||
},
|
||||
]
|
||||
: []
|
||||
: [],
|
||||
)
|
||||
.filter((s) => s && (!playerStats || s.label !== "Average"));
|
||||
}
|
||||
|
@ -2,27 +2,34 @@ export default () => {
|
||||
let currentService = null;
|
||||
let currentServiceParams = {};
|
||||
|
||||
const getAllServices = () => ['scoresaber', 'beatsavior', 'accsaber'];
|
||||
const getAllServices = () => ["scoresaber", "beatsavior", "accsaber"];
|
||||
|
||||
const get = () => ({service: currentService, params: currentServiceParams});
|
||||
const get = () => ({ service: currentService, params: currentServiceParams });
|
||||
|
||||
const getDefaultParams = service => {
|
||||
const getDefaultParams = (service) => {
|
||||
switch (service) {
|
||||
case 'beatsavior':
|
||||
return {sort: 'recent', order: 'desc', page: 1, filters: {}};
|
||||
case "beatsavior":
|
||||
return { sort: "recent", order: "desc", page: 1, filters: {} };
|
||||
|
||||
case 'accsaber':
|
||||
return {type: 'overall', order: 'desc', sort: 'ap', page: 1, filters: {}}
|
||||
case "accsaber":
|
||||
return {
|
||||
type: "overall",
|
||||
order: "desc",
|
||||
sort: "ap",
|
||||
page: 1,
|
||||
filters: {},
|
||||
};
|
||||
|
||||
case 'scoresaber':
|
||||
case "scoresaber":
|
||||
default:
|
||||
return {sort: 'recent', order: 'desc', page: 1, filters: {}}
|
||||
}
|
||||
return { sort: "recent", order: "desc", page: 1, filters: {} };
|
||||
}
|
||||
};
|
||||
|
||||
const update = (serviceParams = {}, service = currentService) => {
|
||||
const availableServices = getAllServices();
|
||||
if (!availableServices.includes(service)) service = availableServices?.[0] ?? 'scoresaber';
|
||||
if (!availableServices.includes(service))
|
||||
service = availableServices?.[0] ?? "scoresaber";
|
||||
|
||||
const defaultServiceParams = getDefaultParams(service);
|
||||
|
||||
@ -32,84 +39,103 @@ export default () => {
|
||||
}
|
||||
|
||||
// preserve old filters
|
||||
serviceParams = {...serviceParams}
|
||||
serviceParams = { ...serviceParams };
|
||||
serviceParams.filters = {
|
||||
...(currentServiceParams?.filters ?? {}),
|
||||
...(serviceParams?.filters ?? {}),
|
||||
}
|
||||
};
|
||||
|
||||
currentService = service;
|
||||
currentServiceParams = {...defaultServiceParams, ...currentServiceParams, ...serviceParams}
|
||||
currentServiceParams = {
|
||||
...defaultServiceParams,
|
||||
...currentServiceParams,
|
||||
...serviceParams,
|
||||
};
|
||||
|
||||
return get();
|
||||
}
|
||||
};
|
||||
|
||||
const clearServiceParams = () => currentServiceParams = {}
|
||||
const clearServiceParams = () => (currentServiceParams = {});
|
||||
|
||||
const initFromUrl = (url = null) => {
|
||||
const availableServices = getAllServices();
|
||||
const defaultService = availableServices?.[0] ?? 'scoresaber';
|
||||
const paramsArr = url ? url.split('/') : [defaultService];
|
||||
const defaultService = availableServices?.[0] ?? "scoresaber";
|
||||
const paramsArr = url ? url.split("/") : [defaultService];
|
||||
|
||||
const service = paramsArr[0] ?? 'scoresaber';
|
||||
const service = paramsArr[0] ?? "scoresaber";
|
||||
|
||||
const serviceDefaultParams = getDefaultParams(service);
|
||||
|
||||
switch (service) {
|
||||
case 'beatsavior':
|
||||
case "beatsavior":
|
||||
return update(
|
||||
{
|
||||
sort: paramsArr[1] ?? serviceDefaultParams?.sort,
|
||||
order: 'desc',
|
||||
order: "desc",
|
||||
page: paramsArr[2] ?? serviceDefaultParams?.page,
|
||||
},
|
||||
service,
|
||||
);
|
||||
|
||||
case 'accsaber':
|
||||
case "accsaber":
|
||||
return update(
|
||||
{
|
||||
type: paramsArr[1] ?? serviceDefaultParams?.type,
|
||||
sort: paramsArr[2] ?? serviceDefaultParams?.sort,
|
||||
order: (paramsArr[2] ?? serviceDefaultParams?.sort) === 'rank' ? 'asc' : 'desc',
|
||||
order:
|
||||
(paramsArr[2] ?? serviceDefaultParams?.sort) === "rank"
|
||||
? "asc"
|
||||
: "desc",
|
||||
page: paramsArr[3] ?? serviceDefaultParams?.page,
|
||||
},
|
||||
service,
|
||||
);
|
||||
|
||||
case 'scoresaber':
|
||||
case "scoresaber":
|
||||
default:
|
||||
return update(
|
||||
{
|
||||
sort: paramsArr[1] ?? serviceDefaultParams?.sort,
|
||||
order: (paramsArr[1] ?? serviceDefaultParams?.sort) === 'rank' ? 'asc' : 'desc',
|
||||
order:
|
||||
(paramsArr[1] ?? serviceDefaultParams?.sort) === "rank"
|
||||
? "asc"
|
||||
: "desc",
|
||||
page: paramsArr[2] ?? serviceDefaultParams?.page,
|
||||
},
|
||||
service,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getUrl = (service, params = {}, noPage = false) => {
|
||||
if (!service) return '';
|
||||
if (!service) return "";
|
||||
|
||||
const serviceDefaultParams = getDefaultParams(service);
|
||||
|
||||
switch (service) {
|
||||
case 'beatsavior':
|
||||
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
|
||||
case "beatsavior":
|
||||
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${
|
||||
noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`
|
||||
}`;
|
||||
|
||||
case 'accsaber':
|
||||
return `${service}/${params?.type ?? serviceDefaultParams?.type}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
|
||||
case "accsaber":
|
||||
return `${service}/${params?.type ?? serviceDefaultParams?.type}/${
|
||||
params?.sort ?? serviceDefaultParams?.sort
|
||||
}${noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`}`;
|
||||
|
||||
case 'scoresaber':
|
||||
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${noPage ? '' : `/${params?.page ?? serviceDefaultParams?.page}`}`;
|
||||
}
|
||||
case "scoresaber":
|
||||
return `${service}/${params?.sort ?? serviceDefaultParams?.sort}${
|
||||
noPage ? "" : `/${params?.page ?? serviceDefaultParams?.page}`
|
||||
}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentServiceUrl = () => getUrl(currentService, currentServiceParams);
|
||||
const getCurrentServiceUrlWithoutPage = () => getUrl(currentService, currentServiceParams, true);
|
||||
const getDefaultServiceUrl = (service = currentService) => getUrl(service, {});
|
||||
const getCurrentServiceUrl = () =>
|
||||
getUrl(currentService, currentServiceParams);
|
||||
const getCurrentServiceUrlWithoutPage = () =>
|
||||
getUrl(currentService, currentServiceParams, true);
|
||||
const getDefaultServiceUrl = (service = currentService) =>
|
||||
getUrl(service, {});
|
||||
|
||||
return {
|
||||
getAvailableServices: getAllServices,
|
||||
@ -122,5 +148,5 @@ export default () => {
|
||||
getParams: () => currentServiceParams,
|
||||
update,
|
||||
clearServiceParams,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,19 +1,29 @@
|
||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
||||
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||
|
||||
export default (name, getObjKey) => {
|
||||
let cache = {};
|
||||
|
||||
// update data cached on another node
|
||||
eventBus.on('cache-key-set-' + name, ({key, value}, isLocal) => !isLocal ? set(key, value, false) : null);
|
||||
eventBus.on('cache-all-set' + name, ({data}, isLocal) => !isLocal ? setAll(data, false) : null);
|
||||
eventBus.on('cache-merge-' + name, ({data}, isLocal) => !isLocal ? merge(data, false) : null);
|
||||
eventBus.on('cache-key-forget-' + name, ({key}, isLocal) => !isLocal ? forget(key, false) : null);
|
||||
eventBus.on('cache-flush-' + name, (_, isLocal) => !isLocal ? flush(false) : null);
|
||||
eventBus.on("cache-key-set-" + name, ({ key, value }, isLocal) =>
|
||||
!isLocal ? set(key, value, false) : null,
|
||||
);
|
||||
eventBus.on("cache-all-set" + name, ({ data }, isLocal) =>
|
||||
!isLocal ? setAll(data, false) : null,
|
||||
);
|
||||
eventBus.on("cache-merge-" + name, ({ data }, isLocal) =>
|
||||
!isLocal ? merge(data, false) : null,
|
||||
);
|
||||
eventBus.on("cache-key-forget-" + name, ({ key }, isLocal) =>
|
||||
!isLocal ? forget(key, false) : null,
|
||||
);
|
||||
eventBus.on("cache-flush-" + name, (_, isLocal) =>
|
||||
!isLocal ? flush(false) : null,
|
||||
);
|
||||
|
||||
const set = (key, value, emitEvent = true) => {
|
||||
cache[key] = value;
|
||||
|
||||
if (emitEvent) eventBus.publish('cache-key-set-' + name, {key, value});
|
||||
if (emitEvent) eventBus.publish("cache-key-set-" + name, { key, value });
|
||||
|
||||
return value;
|
||||
};
|
||||
@ -21,17 +31,17 @@ export default (name, getObjKey) => {
|
||||
const setAll = (data, emitEvent = true) => {
|
||||
cache = data;
|
||||
|
||||
if (emitEvent) eventBus.publish('cache-all-set-' + name, {data});
|
||||
if (emitEvent) eventBus.publish("cache-all-set-" + name, { data });
|
||||
|
||||
return cache;
|
||||
}
|
||||
};
|
||||
const merge = (data, emitEvent = true) => {
|
||||
cache = {...cache, ...data}
|
||||
cache = { ...cache, ...data };
|
||||
|
||||
if (emitEvent) eventBus.publish('cache-merge-' + name, {data});
|
||||
if (emitEvent) eventBus.publish("cache-merge-" + name, { data });
|
||||
|
||||
return cache;
|
||||
}
|
||||
};
|
||||
|
||||
const get = async (key, fetchFunc) => {
|
||||
if (cache.hasOwnProperty(key)) return cache[key];
|
||||
@ -53,42 +63,43 @@ export default (name, getObjKey) => {
|
||||
const key = getObjKey(value);
|
||||
|
||||
return set(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
const getAll = () => cache;
|
||||
|
||||
const has = key => cache[key] !== undefined;
|
||||
const has = (key) => cache[key] !== undefined;
|
||||
|
||||
const getKeys = () => Object.keys(cache);
|
||||
|
||||
const forget = (key, emitEvent = true) => {
|
||||
delete cache[key];
|
||||
|
||||
if (emitEvent) eventBus.publish('cache-key-forget-' + name, {key});
|
||||
if (emitEvent) eventBus.publish("cache-key-forget-" + name, { key });
|
||||
|
||||
return cache;
|
||||
}
|
||||
};
|
||||
|
||||
const forgetByFilter = (filterFunc, emitEvent = true) => {
|
||||
if (!filterFunc) return false;
|
||||
|
||||
Object.keys(cache).filter(key => filterFunc(cache[key]))
|
||||
.forEach(key => {
|
||||
delete cache[key]
|
||||
Object.keys(cache)
|
||||
.filter((key) => filterFunc(cache[key]))
|
||||
.forEach((key) => {
|
||||
delete cache[key];
|
||||
|
||||
if (emitEvent) eventBus.publish('cache-key-forget-' + name, {key});
|
||||
if (emitEvent) eventBus.publish("cache-key-forget-" + name, { key });
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const flush = (emitEvent = true) => {
|
||||
cache = {};
|
||||
|
||||
if (emitEvent) eventBus.publish('cache-flush-' + name, {});
|
||||
if (emitEvent) eventBus.publish("cache-flush-" + name, {});
|
||||
|
||||
return cache;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
has,
|
||||
@ -102,5 +113,5 @@ export default (name, getObjKey) => {
|
||||
forget,
|
||||
forgetByFilter,
|
||||
flush,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
306
src/db/db.js
306
src/db/db.js
@ -1,195 +1,279 @@
|
||||
import {openDB} from 'idb'
|
||||
import log from '../utils/logger'
|
||||
import {isDateObject} from '../utils/js'
|
||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
||||
import { openDB } from "idb";
|
||||
import log from "../utils/logger";
|
||||
import { isDateObject } from "../utils/js";
|
||||
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||
|
||||
const SSR_DB_VERSION = 12;
|
||||
export let db = null;
|
||||
|
||||
export default async () => {
|
||||
IDBKeyRange.prototype.toString = function () {
|
||||
return "IDBKeyRange-" + (isDateObject(this.lower) ? this.lower.getTime() : this.lower) + '-' + (isDateObject(this.upper) ? this.upper : this.upper);
|
||||
}
|
||||
return (
|
||||
"IDBKeyRange-" +
|
||||
(isDateObject(this.lower) ? this.lower.getTime() : this.lower) +
|
||||
"-" +
|
||||
(isDateObject(this.upper) ? this.upper : this.upper)
|
||||
);
|
||||
};
|
||||
|
||||
return await openDatabase();
|
||||
}
|
||||
};
|
||||
|
||||
async function openDatabase() {
|
||||
try {
|
||||
let dbNewVersion = 0, dbOldVersion = 0;
|
||||
let dbNewVersion = 0,
|
||||
dbOldVersion = 0;
|
||||
|
||||
db = await openDB('ssr', SSR_DB_VERSION, {
|
||||
db = await openDB("ssr", SSR_DB_VERSION, {
|
||||
async upgrade(db, oldVersion, newVersion, transaction) {
|
||||
log.info(`Converting database from version ${oldVersion} to version ${newVersion}`);
|
||||
log.info(
|
||||
`Converting database from version ${oldVersion} to version ${newVersion}`,
|
||||
);
|
||||
|
||||
dbNewVersion = newVersion;
|
||||
dbOldVersion = oldVersion;
|
||||
|
||||
switch (true) {
|
||||
case newVersion >= 1 && oldVersion <= 0:
|
||||
db.createObjectStore('players', {
|
||||
keyPath: 'id',
|
||||
db.createObjectStore("players", {
|
||||
keyPath: "id",
|
||||
autoIncrement: false,
|
||||
});
|
||||
|
||||
const playersHistory = db.createObjectStore('players-history', {
|
||||
keyPath: '_idbId',
|
||||
const playersHistory = db.createObjectStore("players-history", {
|
||||
keyPath: "_idbId",
|
||||
autoIncrement: true,
|
||||
});
|
||||
playersHistory.createIndex('players-history-playerId', 'playerId', {unique: false});
|
||||
playersHistory.createIndex('players-history-timestamp', 'timestamp', {unique: false});
|
||||
playersHistory.createIndex("players-history-playerId", "playerId", {
|
||||
unique: false,
|
||||
});
|
||||
playersHistory.createIndex(
|
||||
"players-history-timestamp",
|
||||
"timestamp",
|
||||
{ unique: false },
|
||||
);
|
||||
|
||||
const scoresStore = db.createObjectStore('scores', {
|
||||
keyPath: 'id',
|
||||
const scoresStore = db.createObjectStore("scores", {
|
||||
keyPath: "id",
|
||||
autoIncrement: false,
|
||||
});
|
||||
scoresStore.createIndex('scores-leaderboardId', 'leaderboardId', {unique: false});
|
||||
scoresStore.createIndex('scores-playerId', 'playerId', {unique: false});
|
||||
scoresStore.createIndex('scores-timeset', 'timeset', {unique: false});
|
||||
scoresStore.createIndex('scores-pp', 'pp', {unique: false});
|
||||
scoresStore.createIndex("scores-leaderboardId", "leaderboardId", {
|
||||
unique: false,
|
||||
});
|
||||
scoresStore.createIndex("scores-playerId", "playerId", {
|
||||
unique: false,
|
||||
});
|
||||
scoresStore.createIndex("scores-timeset", "timeset", {
|
||||
unique: false,
|
||||
});
|
||||
scoresStore.createIndex("scores-pp", "pp", { unique: false });
|
||||
|
||||
db.createObjectStore('rankeds', {
|
||||
keyPath: 'leaderboardId',
|
||||
db.createObjectStore("rankeds", {
|
||||
keyPath: "leaderboardId",
|
||||
autoIncrement: false,
|
||||
});
|
||||
|
||||
const songsStore = db.createObjectStore('songs', {
|
||||
keyPath: 'hash',
|
||||
const songsStore = db.createObjectStore("songs", {
|
||||
keyPath: "hash",
|
||||
autoIncrement: false,
|
||||
});
|
||||
songsStore.createIndex('songs-key', 'key', {unique: true});
|
||||
songsStore.createIndex("songs-key", "key", { unique: true });
|
||||
|
||||
db.createObjectStore('twitch', {
|
||||
keyPath: 'playerId',
|
||||
db.createObjectStore("twitch", {
|
||||
keyPath: "playerId",
|
||||
autoIncrement: false,
|
||||
});
|
||||
|
||||
const rankedsChangesStore = db.createObjectStore('rankeds-changes', {
|
||||
keyPath: '_idbId',
|
||||
const rankedsChangesStore = db.createObjectStore(
|
||||
"rankeds-changes",
|
||||
{
|
||||
keyPath: "_idbId",
|
||||
autoIncrement: true,
|
||||
});
|
||||
rankedsChangesStore.createIndex('rankeds-changes-timestamp', 'timestamp', {unique: false});
|
||||
rankedsChangesStore.createIndex('rankeds-changes-leaderboardId', 'leaderboardId', {unique: false});
|
||||
},
|
||||
);
|
||||
rankedsChangesStore.createIndex(
|
||||
"rankeds-changes-timestamp",
|
||||
"timestamp",
|
||||
{ unique: false },
|
||||
);
|
||||
rankedsChangesStore.createIndex(
|
||||
"rankeds-changes-leaderboardId",
|
||||
"leaderboardId",
|
||||
{ unique: false },
|
||||
);
|
||||
|
||||
// no autoIncrement, no keyPath - key must be provided
|
||||
db.createObjectStore('key-value');
|
||||
db.createObjectStore("key-value");
|
||||
|
||||
db.createObjectStore('cache');
|
||||
db.createObjectStore("cache");
|
||||
|
||||
const groups = db.createObjectStore('groups', {keyPath: '_idbId', autoIncrement: true});
|
||||
groups.createIndex('groups-name', 'name', {unique: false});
|
||||
groups.createIndex('groups-playerId', 'playerId', {unique: false});
|
||||
const groups = db.createObjectStore("groups", {
|
||||
keyPath: "_idbId",
|
||||
autoIncrement: true,
|
||||
});
|
||||
groups.createIndex("groups-name", "name", { unique: false });
|
||||
groups.createIndex("groups-playerId", "playerId", {
|
||||
unique: false,
|
||||
});
|
||||
|
||||
const beatSaviorFiles = db.createObjectStore('beat-savior-files', {
|
||||
keyPath: 'fileId',
|
||||
const beatSaviorFiles = db.createObjectStore("beat-savior-files", {
|
||||
keyPath: "fileId",
|
||||
autoIncrement: false,
|
||||
});
|
||||
|
||||
const beatSavior = db.createObjectStore('beat-savior', {
|
||||
keyPath: 'beatSaviorId',
|
||||
const beatSavior = db.createObjectStore("beat-savior", {
|
||||
keyPath: "beatSaviorId",
|
||||
autoIncrement: false,
|
||||
});
|
||||
beatSavior.createIndex('beat-savior-playerId', 'playerId', {unique: false});
|
||||
beatSavior.createIndex('beat-savior-songId', 'songId', {unique: false});
|
||||
beatSavior.createIndex('beat-savior-fileId', 'fileId', {unique: false});
|
||||
beatSavior.createIndex("beat-savior-playerId", "playerId", {
|
||||
unique: false,
|
||||
});
|
||||
beatSavior.createIndex("beat-savior-songId", "songId", {
|
||||
unique: false,
|
||||
});
|
||||
beatSavior.createIndex("beat-savior-fileId", "fileId", {
|
||||
unique: false,
|
||||
});
|
||||
|
||||
// NO break here!
|
||||
|
||||
case newVersion >=2 && oldVersion <= 1:
|
||||
db.createObjectStore('beat-savior-players', {
|
||||
keyPath: 'playerId',
|
||||
case newVersion >= 2 && oldVersion <= 1:
|
||||
db.createObjectStore("beat-savior-players", {
|
||||
keyPath: "playerId",
|
||||
autoIncrement: false,
|
||||
});
|
||||
|
||||
// NO break here!
|
||||
|
||||
case newVersion >= 3 && oldVersion <=2:
|
||||
db.deleteObjectStore('players');
|
||||
case newVersion >= 3 && oldVersion <= 2:
|
||||
db.deleteObjectStore("players");
|
||||
|
||||
db.createObjectStore('players', {
|
||||
keyPath: 'playerId',
|
||||
db.createObjectStore("players", {
|
||||
keyPath: "playerId",
|
||||
autoIncrement: false,
|
||||
});
|
||||
|
||||
const scoresStore4 = transaction.objectStore('scores');
|
||||
scoresStore4.deleteIndex('scores-timeset');
|
||||
scoresStore4.createIndex('scores-timeSet', 'timeSet', {unique: false});
|
||||
const scoresStore4 = transaction.objectStore("scores");
|
||||
scoresStore4.deleteIndex("scores-timeset");
|
||||
scoresStore4.createIndex("scores-timeSet", "timeSet", {
|
||||
unique: false,
|
||||
});
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 4 && oldVersion <=3:
|
||||
db.deleteObjectStore('beat-savior-files');
|
||||
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');
|
||||
const beatSaviorStore = transaction.objectStore("beat-savior");
|
||||
beatSaviorStore.deleteIndex("beat-savior-fileId");
|
||||
beatSaviorStore.deleteIndex("beat-savior-songId");
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 5 && oldVersion <=4:
|
||||
const songsBeatMapsStore = db.createObjectStore('songs-beatmaps', {
|
||||
keyPath: 'hash',
|
||||
case newVersion >= 5 && oldVersion <= 4:
|
||||
const songsBeatMapsStore = db.createObjectStore("songs-beatmaps", {
|
||||
keyPath: "hash",
|
||||
autoIncrement: false,
|
||||
});
|
||||
songsBeatMapsStore.createIndex('songs-beatmaps--key', 'key', {unique: true});
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 6 && oldVersion <=5:
|
||||
const songsBeatMapsStorev6 = transaction.objectStore('songs-beatmaps');
|
||||
songsBeatMapsStorev6.deleteIndex('songs-beatmaps--key');
|
||||
songsBeatMapsStorev6.createIndex('songs-beatmaps-key', 'key', {unique: true});
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 7 && oldVersion <=6:
|
||||
const scoresUpdateQueue = db.createObjectStore('scores-update-queue', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: false,
|
||||
songsBeatMapsStore.createIndex("songs-beatmaps--key", "key", {
|
||||
unique: true,
|
||||
});
|
||||
scoresUpdateQueue.createIndex('scores-update-queue-fetchedAt', 'fetchedAt', {unique: false});
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 6 && oldVersion <= 5:
|
||||
const songsBeatMapsStorev6 =
|
||||
transaction.objectStore("songs-beatmaps");
|
||||
songsBeatMapsStorev6.deleteIndex("songs-beatmaps--key");
|
||||
songsBeatMapsStorev6.createIndex("songs-beatmaps-key", "key", {
|
||||
unique: true,
|
||||
});
|
||||
|
||||
// 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:
|
||||
const beatSaviorStorev8 = transaction.objectStore('beat-savior');
|
||||
beatSaviorStorev8.createIndex('beat-savior-hash', 'hash', {unique: false});
|
||||
const beatSaviorStorev8 = transaction.objectStore("beat-savior");
|
||||
beatSaviorStorev8.createIndex("beat-savior-hash", "hash", {
|
||||
unique: false,
|
||||
});
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 9 && oldVersion <= 8:
|
||||
const playersHistoryStorev9 = transaction.objectStore('players-history');
|
||||
playersHistoryStorev9.deleteIndex('players-history-timestamp');
|
||||
playersHistoryStorev9.createIndex('players-history-playerIdSsTimestamp', 'playerIdSsTimestamp', {unique: true});
|
||||
const playersHistoryStorev9 =
|
||||
transaction.objectStore("players-history");
|
||||
playersHistoryStorev9.deleteIndex("players-history-timestamp");
|
||||
playersHistoryStorev9.createIndex(
|
||||
"players-history-playerIdSsTimestamp",
|
||||
"playerIdSsTimestamp",
|
||||
{ unique: true },
|
||||
);
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 10 && oldVersion <= 9:
|
||||
const songsBeatMapsStoreV10 = transaction.objectStore('songs-beatmaps');
|
||||
songsBeatMapsStoreV10.deleteIndex('songs-beatmaps-key');
|
||||
songsBeatMapsStoreV10.createIndex('songs-beatmaps-key', 'key', {unique: false});
|
||||
const songsBeatMapsStoreV10 =
|
||||
transaction.objectStore("songs-beatmaps");
|
||||
songsBeatMapsStoreV10.deleteIndex("songs-beatmaps-key");
|
||||
songsBeatMapsStoreV10.createIndex("songs-beatmaps-key", "key", {
|
||||
unique: false,
|
||||
});
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 11 && oldVersion <= 10:
|
||||
db.createObjectStore('accsaber-categories', {
|
||||
keyPath: 'name',
|
||||
db.createObjectStore("accsaber-categories", {
|
||||
keyPath: "name",
|
||||
autoIncrement: false,
|
||||
});
|
||||
|
||||
const accSaberPlayersStore = db.createObjectStore('accsaber-players', {
|
||||
keyPath: 'id',
|
||||
const accSaberPlayersStore = db.createObjectStore(
|
||||
"accsaber-players",
|
||||
{
|
||||
keyPath: "id",
|
||||
autoIncrement: false,
|
||||
});
|
||||
accSaberPlayersStore.createIndex('accsaber-players-playerId', 'playerId', {unique: false});
|
||||
accSaberPlayersStore.createIndex('accsaber-players-category', 'category', {unique: false});
|
||||
},
|
||||
);
|
||||
accSaberPlayersStore.createIndex(
|
||||
"accsaber-players-playerId",
|
||||
"playerId",
|
||||
{ unique: false },
|
||||
);
|
||||
accSaberPlayersStore.createIndex(
|
||||
"accsaber-players-category",
|
||||
"category",
|
||||
{ unique: false },
|
||||
);
|
||||
|
||||
// NO break here
|
||||
|
||||
case newVersion >= 12 && oldVersion <= 11:
|
||||
const accSaberPlayersHistoryStore = db.createObjectStore('accsaber-players-history', {
|
||||
keyPath: 'playerIdTimestamp',
|
||||
const accSaberPlayersHistoryStore = db.createObjectStore(
|
||||
"accsaber-players-history",
|
||||
{
|
||||
keyPath: "playerIdTimestamp",
|
||||
autoIncrement: false,
|
||||
});
|
||||
accSaberPlayersHistoryStore.createIndex('accsaber-players-history-playerId', 'playerId', {unique: false});
|
||||
},
|
||||
);
|
||||
accSaberPlayersHistoryStore.createIndex(
|
||||
"accsaber-players-history-playerId",
|
||||
"playerId",
|
||||
{ unique: false },
|
||||
);
|
||||
|
||||
// NO break here
|
||||
}
|
||||
@ -198,28 +282,33 @@ async function openDatabase() {
|
||||
},
|
||||
|
||||
blocked() {
|
||||
console.warn('DB blocked')
|
||||
console.warn("DB blocked");
|
||||
},
|
||||
blocking() {
|
||||
// other tab tries to open newer db version - close connection
|
||||
console.warn('DB blocking... will be closed')
|
||||
console.warn("DB blocking... will be closed");
|
||||
db.close();
|
||||
|
||||
eventBus.publish('dl-manager-pause-cmd');
|
||||
eventBus.publish("dl-manager-pause-cmd");
|
||||
|
||||
// TODO: should be reopened with new version: event.newVersion
|
||||
// TODO: or rather notify user / auto reload page
|
||||
},
|
||||
terminated() {
|
||||
console.warn('DB terminated');
|
||||
console.warn("DB terminated");
|
||||
|
||||
eventBus.publish('dl-manager-pause-cmd');
|
||||
eventBus.publish("dl-manager-pause-cmd");
|
||||
},
|
||||
});
|
||||
|
||||
// Closure code should awaits DB operations ONLY or fail
|
||||
// https://github.com/jakearchibald/idb#user-content-transaction-lifetime
|
||||
db.runInTransaction = async (objectStores, closure, mode = 'readwrite', options = {durability: 'strict'}) => {
|
||||
db.runInTransaction = async (
|
||||
objectStores,
|
||||
closure,
|
||||
mode = "readwrite",
|
||||
options = { durability: "strict" },
|
||||
) => {
|
||||
try {
|
||||
const tx = db.transaction(objectStores, mode, options);
|
||||
|
||||
@ -231,12 +320,11 @@ async function openDatabase() {
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return db;
|
||||
}
|
||||
catch(e) {
|
||||
log.error('Can not open DB.');
|
||||
} catch (e) {
|
||||
log.error("Can not open DB.");
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
@ -1,51 +1,61 @@
|
||||
import keyValueRepository from './repository/key-value';
|
||||
import createBeatMapsService from '../services/beatmaps'
|
||||
import log from '../utils/logger';
|
||||
import {db} from './db'
|
||||
import {isDateObject} from '../utils/js'
|
||||
import twitchRepository from './repository/twitch'
|
||||
import {correctOldSsDate} from '../utils/date'
|
||||
import keyValueRepository from "./repository/key-value";
|
||||
import createBeatMapsService from "../services/beatmaps";
|
||||
import log from "../utils/logger";
|
||||
import { db } from "./db";
|
||||
import { isDateObject } from "../utils/js";
|
||||
import twitchRepository from "./repository/twitch";
|
||||
import { correctOldSsDate } from "../utils/date";
|
||||
|
||||
const FIXES_KEY = 'data-fix';
|
||||
const FIXES_KEY = "data-fix";
|
||||
|
||||
const getAppliedFixes = async () => keyValueRepository().get(FIXES_KEY, true);
|
||||
const setAppliedFixes = async fixes => keyValueRepository().set(fixes, FIXES_KEY);
|
||||
const addAppliedFix = async fixName => {
|
||||
const setAppliedFixes = async (fixes) =>
|
||||
keyValueRepository().set(fixes, FIXES_KEY);
|
||||
const addAppliedFix = async (fixName) => {
|
||||
let allAppliedFixes = await getAppliedFixes();
|
||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
||||
allAppliedFixes =
|
||||
allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
||||
allAppliedFixes.push(fixName);
|
||||
await setAppliedFixes(allAppliedFixes);
|
||||
}
|
||||
};
|
||||
|
||||
const allFixes = {
|
||||
'rankeds-20210725': {
|
||||
apply: async fixName => {
|
||||
log.info('Apply rankeds refresh fix (20210725)')
|
||||
"rankeds-20210725": {
|
||||
apply: async (fixName) => {
|
||||
log.info("Apply rankeds refresh fix (20210725)");
|
||||
|
||||
return db.runInTransaction(['rankeds-changes', 'rankeds', 'key-value'], async tx => {
|
||||
await tx.objectStore('rankeds-changes').clear();
|
||||
await tx.objectStore('rankeds').clear();
|
||||
return db.runInTransaction(
|
||||
["rankeds-changes", "rankeds", "key-value"],
|
||||
async (tx) => {
|
||||
await tx.objectStore("rankeds-changes").clear();
|
||||
await tx.objectStore("rankeds").clear();
|
||||
|
||||
const keyValueStore = tx.objectStore('key-value')
|
||||
const keyValueStore = tx.objectStore("key-value");
|
||||
|
||||
keyValueStore.delete('rankedsLastUpdated');
|
||||
keyValueStore.delete("rankedsLastUpdated");
|
||||
|
||||
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
||||
allAppliedFixes =
|
||||
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||
? allAppliedFixes
|
||||
: [];
|
||||
allAppliedFixes.push(fixName);
|
||||
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
'beatsaver-20210804': {
|
||||
apply: async fixName => {
|
||||
log.info('Converting BeatSaver songs to a new format...', 'DBFix')
|
||||
"beatsaver-20210804": {
|
||||
apply: async (fixName) => {
|
||||
log.info("Converting BeatSaver songs to a new format...", "DBFix");
|
||||
|
||||
return db.runInTransaction(['songs', 'songs-beatmaps', 'key-value'], async tx => {
|
||||
const songsBeatMapsStore = tx.objectStore('songs-beatmaps');
|
||||
return db.runInTransaction(
|
||||
["songs", "songs-beatmaps", "key-value"],
|
||||
async (tx) => {
|
||||
const songsBeatMapsStore = tx.objectStore("songs-beatmaps");
|
||||
|
||||
let cursor = await tx.objectStore('songs').openCursor();
|
||||
let cursor = await tx.objectStore("songs").openCursor();
|
||||
|
||||
let songCount = 0;
|
||||
|
||||
@ -55,90 +65,109 @@ const allFixes = {
|
||||
const beatSaverSong = cursor.value;
|
||||
|
||||
if (beatSaverSong?.metadata?.characteristics) {
|
||||
const beatMapsSong = beatmapsService.convertOldBeatSaverToBeatMaps(beatSaverSong);
|
||||
const beatMapsSong =
|
||||
beatmapsService.convertOldBeatSaverToBeatMaps(beatSaverSong);
|
||||
if (beatMapsSong) {
|
||||
songsBeatMapsStore.put(beatMapsSong)
|
||||
songsBeatMapsStore.put(beatMapsSong);
|
||||
|
||||
songCount++;
|
||||
} else {
|
||||
log.info(`Unable to convert, deleting a song`, 'DBFix', beatSaverSong);
|
||||
log.info(
|
||||
`Unable to convert, deleting a song`,
|
||||
"DBFix",
|
||||
beatSaverSong,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.info(`No metadata characteristics, skipping a song`, 'DBFix', beatSaverSong);
|
||||
log.info(
|
||||
`No metadata characteristics, skipping a song`,
|
||||
"DBFix",
|
||||
beatSaverSong,
|
||||
);
|
||||
}
|
||||
|
||||
cursor = await cursor.continue();
|
||||
}
|
||||
|
||||
const keyValueStore = tx.objectStore('key-value')
|
||||
const keyValueStore = tx.objectStore("key-value");
|
||||
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
||||
allAppliedFixes =
|
||||
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||
? allAppliedFixes
|
||||
: [];
|
||||
allAppliedFixes.push(fixName);
|
||||
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
||||
|
||||
log.info(`${songCount} BeatSaver song(s) converted`, 'DBFix')
|
||||
});
|
||||
}
|
||||
log.info(`${songCount} BeatSaver song(s) converted`, "DBFix");
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
'twitch-20210808': {
|
||||
apply: async fixName => {
|
||||
"twitch-20210808": {
|
||||
apply: async (fixName) => {
|
||||
const predefinedProfiles = {
|
||||
'76561198059659922': 'patian25',
|
||||
'1994101560659098': 'xoxobluff',
|
||||
'76561198138327464': 'altrowilddog',
|
||||
'76561198855288628': 'inbourne',
|
||||
'76561198136177445': 'riviengt',
|
||||
'76561199004224834': 'nyaanos',
|
||||
'76561198023909718': 'danielduel',
|
||||
'76561198212019365': 'fnyt',
|
||||
'76561197966674102': 'maciekvr',
|
||||
'76561198025451538': 'drakonno',
|
||||
'76561197994110158': 'sanorek',
|
||||
'76561198034203862': 'vr_agent',
|
||||
'3702342373170767': 'xjedam',
|
||||
'76561197995161445': 'mediekore',
|
||||
'76561198087710981': 'shreddyfreddy',
|
||||
'76561198999385463': 'woltixo',
|
||||
'76561198035381239': 'motzel',
|
||||
'76561198178407566' : 'acetari',
|
||||
'76561198045386379': 'duhhello',
|
||||
'76561198835772160': 'tornadoef6',
|
||||
'76561198187936410': 'garsh_',
|
||||
'76561198362923485': 'tseska_',
|
||||
'76561198154190170': 'tieeli',
|
||||
'76561198333869741': 'cerret07',
|
||||
'76561197995162898': 'electrostats',
|
||||
'76561198166289091': 'rocker1904',
|
||||
'2538637699496776': 'astrella_',
|
||||
'76561198171842815': 'coolpickb',
|
||||
'76561198145281261': 'harbgy'
|
||||
}
|
||||
"76561198059659922": "patian25",
|
||||
1994101560659098: "xoxobluff",
|
||||
"76561198138327464": "altrowilddog",
|
||||
"76561198855288628": "inbourne",
|
||||
"76561198136177445": "riviengt",
|
||||
"76561199004224834": "nyaanos",
|
||||
"76561198023909718": "danielduel",
|
||||
"76561198212019365": "fnyt",
|
||||
"76561197966674102": "maciekvr",
|
||||
"76561198025451538": "drakonno",
|
||||
"76561197994110158": "sanorek",
|
||||
"76561198034203862": "vr_agent",
|
||||
3702342373170767: "xjedam",
|
||||
"76561197995161445": "mediekore",
|
||||
"76561198087710981": "shreddyfreddy",
|
||||
"76561198999385463": "woltixo",
|
||||
"76561198035381239": "motzel",
|
||||
"76561198178407566": "acetari",
|
||||
"76561198045386379": "duhhello",
|
||||
76561198835772160: "tornadoef6",
|
||||
"76561198187936410": "garsh_",
|
||||
"76561198362923485": "tseska_",
|
||||
"76561198154190170": "tieeli",
|
||||
"76561198333869741": "cerret07",
|
||||
"76561197995162898": "electrostats",
|
||||
"76561198166289091": "rocker1904",
|
||||
2538637699496776: "astrella_",
|
||||
"76561198171842815": "coolpickb",
|
||||
"76561198145281261": "harbgy",
|
||||
};
|
||||
|
||||
log.info('Adding predefined Twitch profiles...', 'DBFix')
|
||||
log.info("Adding predefined Twitch profiles...", "DBFix");
|
||||
|
||||
const updatePlayerTwitchProfile = async (twitchProfile) => twitchRepository().set(twitchProfile);
|
||||
const updatePlayerTwitchProfile = async (twitchProfile) =>
|
||||
twitchRepository().set(twitchProfile);
|
||||
|
||||
await Promise.all(Object.entries(predefinedProfiles).map(async ([playerId, twitchLogin]) => updatePlayerTwitchProfile(
|
||||
{
|
||||
await Promise.all(
|
||||
Object.entries(predefinedProfiles).map(
|
||||
async ([playerId, twitchLogin]) =>
|
||||
updatePlayerTwitchProfile({
|
||||
lastUpdated: null,
|
||||
login: twitchLogin,
|
||||
playerId
|
||||
}
|
||||
)))
|
||||
playerId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await addAppliedFix(fixName);
|
||||
|
||||
log.info('Twitch profiles added.', 'DBFix')
|
||||
}
|
||||
log.info("Twitch profiles added.", "DBFix");
|
||||
},
|
||||
},
|
||||
|
||||
'player-history-20211022': {
|
||||
apply: async fixName => {
|
||||
log.info('Apply player ss history fix (20211022)')
|
||||
"player-history-20211022": {
|
||||
apply: async (fixName) => {
|
||||
log.info("Apply player ss history fix (20211022)");
|
||||
|
||||
return db.runInTransaction(['players-history', 'key-value'], async tx => {
|
||||
const playersHistoryStore = tx.objectStore('players-history');
|
||||
return db.runInTransaction(
|
||||
["players-history", "key-value"],
|
||||
async (tx) => {
|
||||
const playersHistoryStore = tx.objectStore("players-history");
|
||||
|
||||
let cursor = await playersHistoryStore.openCursor();
|
||||
|
||||
@ -157,33 +186,46 @@ const allFixes = {
|
||||
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
|
||||
|
||||
await cursor.delete();
|
||||
playersHistoryStore.put({...history, ssDate, playerIdSsTimestamp});
|
||||
playersHistoryStore.put({
|
||||
...history,
|
||||
ssDate,
|
||||
playerIdSsTimestamp,
|
||||
});
|
||||
|
||||
cursor = await cursor.continue();
|
||||
}
|
||||
|
||||
const keyValueStore = tx.objectStore('key-value')
|
||||
const keyValueStore = tx.objectStore("key-value");
|
||||
let allAppliedFixes = await keyValueStore.get(FIXES_KEY);
|
||||
allAppliedFixes = allAppliedFixes && Array.isArray(allAppliedFixes) ? allAppliedFixes : [];
|
||||
allAppliedFixes =
|
||||
allAppliedFixes && Array.isArray(allAppliedFixes)
|
||||
? allAppliedFixes
|
||||
: [];
|
||||
allAppliedFixes.push(fixName);
|
||||
await keyValueStore.put(allAppliedFixes, FIXES_KEY);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
let appliedDbFixes = await getAppliedFixes();
|
||||
const appliedFixes = appliedDbFixes && Array.isArray(appliedDbFixes) ? appliedDbFixes : [];
|
||||
const neededFixes = Object.keys(allFixes).filter(f => !appliedFixes.includes(f) && (!allFixes[f].validTo || allFixes[f].validTo > new Date()));
|
||||
const appliedFixes =
|
||||
appliedDbFixes && Array.isArray(appliedDbFixes) ? appliedDbFixes : [];
|
||||
const neededFixes = Object.keys(allFixes).filter(
|
||||
(f) =>
|
||||
!appliedFixes.includes(f) &&
|
||||
(!allFixes[f].validTo || allFixes[f].validTo > new Date()),
|
||||
);
|
||||
|
||||
if (!neededFixes.length) return;
|
||||
|
||||
document.body.innerHTML = '<p>Database conversion. Please wait...</p>';
|
||||
document.body.innerHTML = "<p>Database conversion. Please wait...</p>";
|
||||
|
||||
for (let key of neededFixes) {
|
||||
await allFixes[key].apply(key);
|
||||
}
|
||||
|
||||
document.body.innerHTML = '';
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
};
|
||||
|
@ -1,19 +1,29 @@
|
||||
import cacheRepository from './repository/cache';
|
||||
import groupsRepository from './repository/groups';
|
||||
import keyValueRepository from './repository/key-value';
|
||||
import playersRepository from './repository/players';
|
||||
import playersHistoryRepository from './repository/players-history';
|
||||
import rankedsRepository from './repository/rankeds';
|
||||
import rankedsChangesRepository from './repository/rankeds-changes';
|
||||
import scoresRepository from './repository/scores';
|
||||
import songsRepository from './repository/songs';
|
||||
import twitchRepository from './repository/twitch';
|
||||
import log from '../utils/logger';
|
||||
|
||||
import cacheRepository from "./repository/cache";
|
||||
import groupsRepository from "./repository/groups";
|
||||
import keyValueRepository from "./repository/key-value";
|
||||
import playersRepository from "./repository/players";
|
||||
import playersHistoryRepository from "./repository/players-history";
|
||||
import rankedsRepository from "./repository/rankeds";
|
||||
import rankedsChangesRepository from "./repository/rankeds-changes";
|
||||
import scoresRepository from "./repository/scores";
|
||||
import songsRepository from "./repository/songs";
|
||||
import twitchRepository from "./repository/twitch";
|
||||
import log from "../utils/logger";
|
||||
|
||||
export default () => {
|
||||
log.debug('Initialize DB repositories');
|
||||
log.debug("Initialize DB repositories");
|
||||
|
||||
// initialize all repositories in order to create cache to sync
|
||||
[cacheRepository, groupsRepository, keyValueRepository, playersRepository, playersHistoryRepository, rankedsRepository, rankedsChangesRepository, scoresRepository, songsRepository, twitchRepository].map(repository => repository());
|
||||
}
|
||||
[
|
||||
cacheRepository,
|
||||
groupsRepository,
|
||||
keyValueRepository,
|
||||
playersRepository,
|
||||
playersHistoryRepository,
|
||||
rankedsRepository,
|
||||
rankedsChangesRepository,
|
||||
scoresRepository,
|
||||
songsRepository,
|
||||
twitchRepository,
|
||||
].map((repository) => repository());
|
||||
};
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('accsaber-categories', 'name');
|
||||
export default () => createRepository("accsaber-categories", "name");
|
||||
|
@ -1,6 +1,7 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('accsaber-players-history', 'playerIdTimestamp', {
|
||||
'accsaber-players-history-playerId': 'playerId',
|
||||
'accsaber-players-history-playerIdTimestamp': 'playerIdTimestamp'
|
||||
});
|
||||
export default () =>
|
||||
createRepository("accsaber-players-history", "playerIdTimestamp", {
|
||||
"accsaber-players-history-playerId": "playerId",
|
||||
"accsaber-players-history-playerIdTimestamp": "playerIdTimestamp",
|
||||
});
|
||||
|
@ -1,10 +1,7 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository(
|
||||
'accsaber-players',
|
||||
'id',
|
||||
{
|
||||
'accsaber-players-playerId': 'playerId',
|
||||
'accsaber-players-category': 'category',
|
||||
},
|
||||
);
|
||||
export default () =>
|
||||
createRepository("accsaber-players", "id", {
|
||||
"accsaber-players-playerId": "playerId",
|
||||
"accsaber-players-category": "category",
|
||||
});
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('beat-savior-files', 'fileId');
|
||||
export default () => createRepository("beat-savior-files", "fileId");
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('beat-savior-players', 'playerId');
|
||||
export default () => createRepository("beat-savior-players", "playerId");
|
||||
|
@ -1,6 +1,7 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('beat-savior', 'beatSaviorId', {
|
||||
'beat-savior-playerId': 'playerId',
|
||||
'beat-savior-hash': 'hash',
|
||||
});
|
||||
export default () =>
|
||||
createRepository("beat-savior", "beatSaviorId", {
|
||||
"beat-savior-playerId": "playerId",
|
||||
"beat-savior-hash": "hash",
|
||||
});
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('cache');
|
||||
export default () => createRepository("cache");
|
||||
|
@ -1,11 +1,11 @@
|
||||
import cache from '../cache';
|
||||
import {db} from '../db';
|
||||
import {convertArrayToObjectByKey} from '../../utils/js'
|
||||
import makePendingPromisePool from '../../utils/pending-promises'
|
||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
||||
import cache from "../cache";
|
||||
import { db } from "../db";
|
||||
import { convertArrayToObjectByKey } from "../../utils/js";
|
||||
import makePendingPromisePool from "../../utils/pending-promises";
|
||||
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||
|
||||
export const ALL_KEY = '__ALL';
|
||||
const NONE_KEY = '__NONE';
|
||||
export const ALL_KEY = "__ALL";
|
||||
const NONE_KEY = "__NONE";
|
||||
|
||||
let repositories = {};
|
||||
|
||||
@ -20,46 +20,52 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
||||
const getKeyName = () => inlineKeyName;
|
||||
const hasOutOfLineKey = () => getKeyName() === undefined;
|
||||
const getObjKey = (obj, outOfLineKey = undefined) => {
|
||||
const key = hasOutOfLineKey() ? outOfLineKey : obj[inlineKeyName]
|
||||
const key = hasOutOfLineKey() ? outOfLineKey : obj[inlineKeyName];
|
||||
return key ? key : outOfLineKey;
|
||||
}
|
||||
};
|
||||
|
||||
let repositoryCache = cache(repositoryName, getObjKey);
|
||||
|
||||
const getCacheKeyFor = (query, indexName) => (indexName ? indexName : ALL_KEY) + '-' + (query ? query : NONE_KEY);
|
||||
const getCacheKeyFor = (query, indexName) =>
|
||||
(indexName ? indexName : ALL_KEY) + "-" + (query ? query : NONE_KEY);
|
||||
|
||||
const getFieldForIndexName = indexName => indexesKeyNames[indexName];
|
||||
const isFieldForIndexDefined = indexName => !!getFieldForIndexName(indexName);
|
||||
const getFieldForIndexName = (indexName) => indexesKeyNames[indexName];
|
||||
const isFieldForIndexDefined = (indexName) =>
|
||||
!!getFieldForIndexName(indexName);
|
||||
|
||||
const setDataAvailabilityStatus = cacheKey => dataAvailableFor[cacheKey] = true;
|
||||
const setAllDataAvailabilityStatus = () => setDataAvailabilityStatus(getCacheKeyFor());
|
||||
const removeDataAvailabilityStatus = cacheKey => {
|
||||
const setDataAvailabilityStatus = (cacheKey) =>
|
||||
(dataAvailableFor[cacheKey] = true);
|
||||
const setAllDataAvailabilityStatus = () =>
|
||||
setDataAvailabilityStatus(getCacheKeyFor());
|
||||
const removeDataAvailabilityStatus = (cacheKey) => {
|
||||
delete dataAvailableFor[cacheKey];
|
||||
delete dataAvailableFor[getCacheKeyFor()];
|
||||
}
|
||||
const flushDataAvailabilityStatus = () => dataAvailableFor = {};
|
||||
const isIndexDataAvailable = cacheKey => !!dataAvailableFor[cacheKey];
|
||||
};
|
||||
const flushDataAvailabilityStatus = () => (dataAvailableFor = {});
|
||||
const isIndexDataAvailable = (cacheKey) => !!dataAvailableFor[cacheKey];
|
||||
const isAllDataAvailable = () => isIndexDataAvailable(getCacheKeyFor());
|
||||
|
||||
const flushCache = () => {
|
||||
repositoryCache.flush();
|
||||
flushDataAvailabilityStatus();
|
||||
}
|
||||
};
|
||||
|
||||
const forgetCacheKey = key => repositoryCache.forget(key);
|
||||
const forgetCacheKey = (key) => repositoryCache.forget(key);
|
||||
|
||||
const forgetObject = async obj => {
|
||||
if (hasOutOfLineKey()) throw 'forgetObject function is not available in repositories with out-of-line keys';
|
||||
const forgetObject = async (obj) => {
|
||||
if (hasOutOfLineKey())
|
||||
throw "forgetObject function is not available in repositories with out-of-line keys";
|
||||
|
||||
const key = getObjKey(obj);
|
||||
if (!key) throw `Object does not contain ${inlineKeyName} field which is repository key`;
|
||||
if (!key)
|
||||
throw `Object does not contain ${inlineKeyName} field which is repository key`;
|
||||
|
||||
forgetCacheKey(key);
|
||||
}
|
||||
};
|
||||
|
||||
const getStoreName = () => storeName;
|
||||
|
||||
const getCachedKeys = _ => repositoryCache.getKeys();
|
||||
const getCachedKeys = (_) => repositoryCache.getKeys();
|
||||
|
||||
const getAllKeys = async () => db.getAllKeys(storeName);
|
||||
|
||||
@ -68,16 +74,23 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
||||
|
||||
const cacheKey = getCacheKeyFor(key);
|
||||
|
||||
return repositoryCache.get(key, () => resolvePromiseOrWaitForPending(cacheKey, () => db.get(storeName, key)));
|
||||
return repositoryCache.get(key, () =>
|
||||
resolvePromiseOrWaitForPending(cacheKey, () => db.get(storeName, key)),
|
||||
);
|
||||
};
|
||||
|
||||
const getFromIndex = async (indexName, query, refreshCache = false) => {
|
||||
if (hasOutOfLineKey()) throw `getFromIndex() is not available for stores with out-of-line key`;
|
||||
if (!isFieldForIndexDefined(indexName)) throw `Index ${indexName} has no field set`;
|
||||
if (hasOutOfLineKey())
|
||||
throw `getFromIndex() is not available for stores with out-of-line key`;
|
||||
if (!isFieldForIndexDefined(indexName))
|
||||
throw `Index ${indexName} has no field set`;
|
||||
|
||||
const cacheKey = getCacheKeyFor(query, indexName + '-single');
|
||||
const cacheKey = getCacheKeyFor(query, indexName + "-single");
|
||||
|
||||
const getFromDb = () => resolvePromiseOrWaitForPending(cacheKey, () => db.getFromIndex(storeName, indexName, query));
|
||||
const getFromDb = () =>
|
||||
resolvePromiseOrWaitForPending(cacheKey, () =>
|
||||
db.getFromIndex(storeName, indexName, query),
|
||||
);
|
||||
|
||||
if (query && query instanceof IDBKeyRange) return getFromDb();
|
||||
|
||||
@ -85,7 +98,8 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
||||
|
||||
const fullIndexCacheKey = getCacheKeyFor(query, indexName);
|
||||
|
||||
const filterItems = item => item !== undefined && (!query || item[field] === query);
|
||||
const filterItems = (item) =>
|
||||
item !== undefined && (!query || item[field] === query);
|
||||
|
||||
if (refreshCache) {
|
||||
removeDataAvailabilityStatus(cacheKey);
|
||||
@ -94,24 +108,34 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
||||
repositoryCache.forgetByFilter(filterItems);
|
||||
}
|
||||
|
||||
return repositoryCache.getByFilter(getFromDb, isAllDataAvailable() || isIndexDataAvailable(cacheKey) || isIndexDataAvailable(fullIndexCacheKey) ? filterItems : null);
|
||||
return repositoryCache.getByFilter(
|
||||
getFromDb,
|
||||
isAllDataAvailable() ||
|
||||
isIndexDataAvailable(cacheKey) ||
|
||||
isIndexDataAvailable(fullIndexCacheKey)
|
||||
? filterItems
|
||||
: null,
|
||||
);
|
||||
};
|
||||
|
||||
const getAll = async(refreshCache = false) => {
|
||||
const getAll = async (refreshCache = false) => {
|
||||
const cacheKey = getCacheKeyFor();
|
||||
|
||||
const getFromDb = () => resolvePromiseOrWaitForPending(cacheKey, () => db.getAll(storeName))
|
||||
const getFromDb = () =>
|
||||
resolvePromiseOrWaitForPending(cacheKey, () => db.getAll(storeName));
|
||||
|
||||
if (hasOutOfLineKey()) return getFromDb();
|
||||
|
||||
if (refreshCache) flushCache();
|
||||
|
||||
const filterUndefined = item => item !== undefined;
|
||||
const filterUndefined = (item) => item !== undefined;
|
||||
|
||||
if (!isAllDataAvailable()) {
|
||||
const data = convertArrayToObjectByKey(await getFromDb(), inlineKeyName);
|
||||
|
||||
const ret = Object.values(repositoryCache.setAll(data)).filter(filterUndefined);
|
||||
const ret = Object.values(repositoryCache.setAll(data)).filter(
|
||||
filterUndefined,
|
||||
);
|
||||
|
||||
setAllDataAvailabilityStatus();
|
||||
|
||||
@ -119,28 +143,39 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
||||
}
|
||||
|
||||
return Object.values(repositoryCache.getAll()).filter(filterUndefined);
|
||||
}
|
||||
};
|
||||
|
||||
const getAllFromIndex = async(indexName, query = undefined, refreshCache = false) => {
|
||||
if (hasOutOfLineKey()) throw `getAllFromIndex() is not available for stores with out-of-line key`;
|
||||
if (!isFieldForIndexDefined(indexName)) throw `Index ${indexName} has no field set`;
|
||||
const getAllFromIndex = async (
|
||||
indexName,
|
||||
query = undefined,
|
||||
refreshCache = false,
|
||||
) => {
|
||||
if (hasOutOfLineKey())
|
||||
throw `getAllFromIndex() is not available for stores with out-of-line key`;
|
||||
if (!isFieldForIndexDefined(indexName))
|
||||
throw `Index ${indexName} has no field set`;
|
||||
|
||||
const cacheKey = getCacheKeyFor(query, indexName);
|
||||
|
||||
const getFromDb = async () => resolvePromiseOrWaitForPending(cacheKey, () => db.getAllFromIndex(storeName, indexName, query));
|
||||
const getFromDb = async () =>
|
||||
resolvePromiseOrWaitForPending(cacheKey, () =>
|
||||
db.getAllFromIndex(storeName, indexName, query),
|
||||
);
|
||||
|
||||
if (query && query instanceof IDBKeyRange) return getFromDb();
|
||||
|
||||
const field = getFieldForIndexName(indexName);
|
||||
|
||||
const filterItems = item => item !== undefined && (!query || item[field] === query);
|
||||
const filterItems = (item) =>
|
||||
item !== undefined && (!query || item[field] === query);
|
||||
|
||||
if (refreshCache) {
|
||||
removeDataAvailabilityStatus(cacheKey);
|
||||
repositoryCache.forgetByFilter(filterItems);
|
||||
}
|
||||
|
||||
const getFromDbAndUpdateCache = async () => resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => {
|
||||
const getFromDbAndUpdateCache = async () =>
|
||||
resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => {
|
||||
const data = await getFromDb();
|
||||
|
||||
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
|
||||
@ -148,63 +183,71 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
||||
setDataAvailabilityStatus(cacheKey);
|
||||
|
||||
return data;
|
||||
})
|
||||
});
|
||||
|
||||
if (!isAllDataAvailable() && !isIndexDataAvailable(cacheKey)) return await getFromDbAndUpdateCache();
|
||||
if (!isAllDataAvailable() && !isIndexDataAvailable(cacheKey))
|
||||
return await getFromDbAndUpdateCache();
|
||||
|
||||
return Object.values(repositoryCache.getAll()).filter(filterItems);
|
||||
}
|
||||
};
|
||||
|
||||
const set = async (value, key = undefined, tx = null) => {
|
||||
const txStores = tx ? [...tx.objectStoreNames] : null;
|
||||
|
||||
let putKey;
|
||||
if (tx && txStores.includes(storeName)) {
|
||||
putKey = await tx.objectStore(storeName).put(value, inlineKeyName ? undefined : key);
|
||||
putKey = await tx
|
||||
.objectStore(storeName)
|
||||
.put(value, inlineKeyName ? undefined : key);
|
||||
} else {
|
||||
putKey = await db.put(storeName, value, inlineKeyName ? undefined : key)
|
||||
putKey = await db.put(storeName, value, inlineKeyName ? undefined : key);
|
||||
}
|
||||
|
||||
if (!hasOutOfLineKey() && !getObjKey(value)) value[inlineKeyName] = putKey;
|
||||
|
||||
return repositoryCache.set(getObjKey(value, key), value);
|
||||
}
|
||||
};
|
||||
|
||||
const del = async key => {
|
||||
const del = async (key) => {
|
||||
await db.delete(storeName, key);
|
||||
|
||||
return repositoryCache.forget(key);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteObject = async obj => {
|
||||
if (hasOutOfLineKey()) throw 'deleteObject function is not available in repositories with out-of-line keys';
|
||||
const deleteObject = async (obj) => {
|
||||
if (hasOutOfLineKey())
|
||||
throw "deleteObject function is not available in repositories with out-of-line keys";
|
||||
|
||||
const key = getObjKey(obj);
|
||||
if (!key) throw `Object does not contain ${inlineKeyName} field which is repository key`;
|
||||
if (!key)
|
||||
throw `Object does not contain ${inlineKeyName} field which is repository key`;
|
||||
|
||||
return del(key);
|
||||
}
|
||||
};
|
||||
|
||||
const openCursor = async (mode = 'readonly') => db.transaction(storeName, mode).store.openCursor();
|
||||
const openCursor = async (mode = "readonly") =>
|
||||
db.transaction(storeName, mode).store.openCursor();
|
||||
|
||||
const setCache = (value, key) => {
|
||||
if (hasOutOfLineKey()) {
|
||||
if (!key) throw `setCache() needs a key for stores (${storeName}) with out-of-line keys`;
|
||||
if (!key)
|
||||
throw `setCache() needs a key for stores (${storeName}) with out-of-line keys`;
|
||||
} else {
|
||||
key = getObjKey(value, key);
|
||||
}
|
||||
|
||||
repositoryCache.set(key, value);
|
||||
}
|
||||
const addToCache = data => {
|
||||
if (hasOutOfLineKey()) throw `addToCache() is not available for stores (${storeName}) with out-of-line key`;
|
||||
};
|
||||
const addToCache = (data) => {
|
||||
if (hasOutOfLineKey())
|
||||
throw `addToCache() is not available for stores (${storeName}) with out-of-line key`;
|
||||
|
||||
repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName));
|
||||
}
|
||||
};
|
||||
|
||||
const getCache = () => repositoryCache;
|
||||
|
||||
return repositories[repositoryName] = {
|
||||
return (repositories[repositoryName] = {
|
||||
getStoreName,
|
||||
hasOutOfLineKey,
|
||||
getAllKeys,
|
||||
@ -224,5 +267,5 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => {
|
||||
setCache,
|
||||
addToCache,
|
||||
getCache,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -1,5 +1,11 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
let repository;
|
||||
|
||||
export default () => repository ? repository : repository = createRepository('groups', '_idbId', {'groups-name': 'name', 'groups-playerId': 'playerId'});
|
||||
export default () =>
|
||||
repository
|
||||
? repository
|
||||
: (repository = createRepository("groups", "_idbId", {
|
||||
"groups-name": "name",
|
||||
"groups-playerId": "playerId",
|
||||
}));
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('key-value');
|
||||
export default () => createRepository("key-value");
|
||||
|
@ -1,6 +1,7 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('players-history', '_idbId', {
|
||||
'players-history-playerId': 'playerId',
|
||||
'players-history-playerIdSsTimestamp': 'playerIdSsTimestamp'
|
||||
});
|
||||
export default () =>
|
||||
createRepository("players-history", "_idbId", {
|
||||
"players-history-playerId": "playerId",
|
||||
"players-history-playerIdSsTimestamp": "playerIdSsTimestamp",
|
||||
});
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('players', 'playerId');
|
||||
export default () => createRepository("players", "playerId");
|
||||
|
@ -1,3 +1,7 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('rankeds-changes', '_idbId', {'rankeds-changes-timestamp': 'timestamp', 'rankeds-changes-leaderboardId': 'leaderboardId'});
|
||||
export default () =>
|
||||
createRepository("rankeds-changes", "_idbId", {
|
||||
"rankeds-changes-timestamp": "timestamp",
|
||||
"rankeds-changes-leaderboardId": "leaderboardId",
|
||||
});
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('rankeds', 'leaderboardId');
|
||||
export default () => createRepository("rankeds", "leaderboardId");
|
||||
|
@ -1,9 +1,6 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository(
|
||||
'scores-update-queue',
|
||||
'id',
|
||||
{
|
||||
'scores-update-queue-fetchedAt': 'fetchedAt',
|
||||
},
|
||||
)
|
||||
export default () =>
|
||||
createRepository("scores-update-queue", "id", {
|
||||
"scores-update-queue-fetchedAt": "fetchedAt",
|
||||
});
|
||||
|
@ -1,12 +1,9 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository(
|
||||
'scores',
|
||||
'id',
|
||||
{
|
||||
'scores-timeset': 'timeset',
|
||||
'scores-leaderboardId': 'leaderboardId',
|
||||
'scores-playerId': 'playerId',
|
||||
'scores-pp': 'pp',
|
||||
},
|
||||
)
|
||||
export default () =>
|
||||
createRepository("scores", "id", {
|
||||
"scores-timeset": "timeset",
|
||||
"scores-leaderboardId": "leaderboardId",
|
||||
"scores-playerId": "playerId",
|
||||
"scores-pp": "pp",
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('songs-beatmaps', 'hash', {'songs-beatmaps-key': 'key'});
|
||||
export default () =>
|
||||
createRepository("songs-beatmaps", "hash", { "songs-beatmaps-key": "key" });
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('songs', 'hash', {'songs-key': 'key'});
|
||||
export default () => createRepository("songs", "hash", { "songs-key": "key" });
|
||||
|
@ -1,3 +1,3 @@
|
||||
import createRepository from './generic';
|
||||
import createRepository from "./generic";
|
||||
|
||||
export default () => createRepository('twitch', 'playerId');
|
||||
export default () => createRepository("twitch", "playerId");
|
||||
|
53
src/main.js
53
src/main.js
@ -1,35 +1,35 @@
|
||||
import App from './App.svelte';
|
||||
import log from './utils/logger'
|
||||
import initDb from './db/db'
|
||||
import initializeRepositories from './db/repositories-init';
|
||||
import setupDataFixes from './db/fix-data'
|
||||
import createConfigStore from './stores/config'
|
||||
import createPlayerService from './services/scoresaber/player'
|
||||
import createBeatSaviorService from './services/beatsavior'
|
||||
import createRankedsStore from './stores/scoresaber/rankeds'
|
||||
import initDownloadManager from './network/download-manager'
|
||||
import initCommandProcessor from './network/command-processor'
|
||||
import {enablePatches, setAutoFreeze} from 'immer'
|
||||
import {initCompareEnhancer} from './stores/http/enhancers/scores/compare'
|
||||
import ErrorComponent from './components/Common/Error.svelte'
|
||||
import initializeWorkers from './utils/worker-wrappers'
|
||||
import App from "./App.svelte";
|
||||
import log from "./utils/logger";
|
||||
import initDb from "./db/db";
|
||||
import initializeRepositories from "./db/repositories-init";
|
||||
import setupDataFixes from "./db/fix-data";
|
||||
import createConfigStore from "./stores/config";
|
||||
import createPlayerService from "./services/scoresaber/player";
|
||||
import createBeatSaviorService from "./services/beatsavior";
|
||||
import createRankedsStore from "./stores/scoresaber/rankeds";
|
||||
import initDownloadManager from "./network/download-manager";
|
||||
import initCommandProcessor from "./network/command-processor";
|
||||
import { enablePatches, setAutoFreeze } from "immer";
|
||||
import { initCompareEnhancer } from "./stores/http/enhancers/scores/compare";
|
||||
import ErrorComponent from "./components/Common/Error.svelte";
|
||||
import initializeWorkers from "./utils/worker-wrappers";
|
||||
|
||||
let app = null;
|
||||
|
||||
(async() => {
|
||||
(async () => {
|
||||
try {
|
||||
// TODO: remove level setting
|
||||
// log.setLevel(log.TRACE);
|
||||
// log.logOnly(['AccSaberService']);
|
||||
|
||||
log.info('Starting up...', 'Main')
|
||||
log.info("Starting up...", "Main");
|
||||
|
||||
await initDb();
|
||||
await initializeRepositories();
|
||||
await setupDataFixes();
|
||||
|
||||
// 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
|
||||
enablePatches();
|
||||
@ -47,24 +47,29 @@ let app = null;
|
||||
|
||||
initCommandProcessor(await initDownloadManager());
|
||||
|
||||
log.info('Site initialized', 'Main')
|
||||
log.info("Site initialized", "Main");
|
||||
|
||||
app = new App({
|
||||
target: document.body,
|
||||
props: {},
|
||||
});
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
if (error instanceof DOMException && error.toString() === 'InvalidStateError: A mutation operation was attempted on a database that did not allow mutations.')
|
||||
error = new Error('Firefox in private mode does not support the database. Please run the site in normal mode.')
|
||||
if (
|
||||
error instanceof DOMException &&
|
||||
error.toString() ===
|
||||
"InvalidStateError: A mutation operation was attempted on a database that did not allow mutations."
|
||||
)
|
||||
error = new Error(
|
||||
"Firefox in private mode does not support the database. Please run the site in normal mode.",
|
||||
);
|
||||
|
||||
app = new ErrorComponent({
|
||||
target: document.body,
|
||||
props: {error, withTrace: true},
|
||||
props: { error, withTrace: true },
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
export default app;
|
@ -1,5 +1,5 @@
|
||||
// import eventBus from '../utils/broadcast-channel-pubsub'
|
||||
import {addToDate, MINUTE} from '../utils/date'
|
||||
import { addToDate, MINUTE } from "../utils/date";
|
||||
|
||||
const DEFAULT_CACHE_SIZE = 100;
|
||||
|
||||
@ -7,20 +7,25 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
||||
let cache = {};
|
||||
let cacheSize = size;
|
||||
|
||||
const isWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope
|
||||
const isWorker =
|
||||
typeof WorkerGlobalScope !== "undefined" &&
|
||||
self instanceof WorkerGlobalScope;
|
||||
|
||||
const defaultExpiryIn = expiryIn;
|
||||
|
||||
const packValue = value => {
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
const packValue = (value) => {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
|
||||
const newValue = {...value};
|
||||
const newValue = { ...value };
|
||||
|
||||
if (value.headers && value.headers instanceof Headers) {
|
||||
newValue.headers = [...value.headers.entries()].reduce((cum, [key, value]) => {
|
||||
newValue.headers = [...value.headers.entries()].reduce(
|
||||
(cum, [key, value]) => {
|
||||
cum[key] = value;
|
||||
return cum;
|
||||
}, {})
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
if (value.body && value.body instanceof Document) {
|
||||
@ -28,25 +33,29 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
};
|
||||
|
||||
const unpackValue = value => {
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
const unpackValue = (value) => {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
|
||||
const newValue = {...value};
|
||||
const newValue = { ...value };
|
||||
|
||||
if (value.headers) {
|
||||
const headers = new Headers();
|
||||
Object.keys(value.headers).map(k => headers.append(k, value.headers[k]));
|
||||
Object.keys(value.headers).map((k) =>
|
||||
headers.append(k, value.headers[k]),
|
||||
);
|
||||
newValue.headers = headers;
|
||||
}
|
||||
|
||||
if (value.body) {
|
||||
newValue.body = !isWorker ? new DOMParser().parseFromString(value.body, 'text/html') : value.body;
|
||||
newValue.body = !isWorker
|
||||
? new DOMParser().parseFromString(value.body, "text/html")
|
||||
: value.body;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
};
|
||||
|
||||
// update data cached on another node
|
||||
// const setUnsubscribe = eventBus.on('net-cache-key-set', ({key, value, expiryIn}, isLocal) => !isLocal ? set(key, unpackValue(value), expiryIn, false) : null);
|
||||
@ -54,14 +63,25 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
||||
// const flushUnsubscribe = eventBus.on('net-cache-flush', (_, isLocal) => !isLocal ? flush(false) : null);
|
||||
|
||||
const has = (key, maxAge = null, withExpired = false) =>
|
||||
cache.hasOwnProperty(key) && cache[key] &&
|
||||
(withExpired || !cache[key].expiryAt || cache[key].expiryAt >= new Date()) &&
|
||||
(!Number.isFinite(maxAge) || !cache[key].cachedAt || addToDate(maxAge, cache[key].cachedAt) >= new Date());
|
||||
cache.hasOwnProperty(key) &&
|
||||
cache[key] &&
|
||||
(withExpired ||
|
||||
!cache[key].expiryAt ||
|
||||
cache[key].expiryAt >= new Date()) &&
|
||||
(!Number.isFinite(maxAge) ||
|
||||
!cache[key].cachedAt ||
|
||||
addToDate(maxAge, cache[key].cachedAt) >= new Date());
|
||||
|
||||
const set = (key, value, expiryIn = null, emitEvent = true) => {
|
||||
expiryIn = expiryIn ? expiryIn : defaultExpiryIn;
|
||||
|
||||
cache[key] = {key, cachedAt: new Date(), expiryIn, expiryAt: addToDate(expiryIn, new Date()), value};
|
||||
cache[key] = {
|
||||
key,
|
||||
cachedAt: new Date(),
|
||||
expiryIn,
|
||||
expiryAt: addToDate(expiryIn, new Date()),
|
||||
value,
|
||||
};
|
||||
|
||||
// if (emitEvent) eventBus.publish('net-cache-key-set', {key, value: packValue(value), expiryIn});
|
||||
|
||||
@ -70,7 +90,12 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const get = (key, maxAge = null, withExpired = false, valueOnly = true) => has(key, maxAge, withExpired) ? (valueOnly ? cache[key].value : cache[key]) : undefined;
|
||||
const get = (key, maxAge = null, withExpired = false, valueOnly = true) =>
|
||||
has(key, maxAge, withExpired)
|
||||
? valueOnly
|
||||
? cache[key].value
|
||||
: cache[key]
|
||||
: undefined;
|
||||
|
||||
const getAll = () => cache;
|
||||
|
||||
@ -82,7 +107,7 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
||||
// if (emitEvent) eventBus.publish('net-cache-key-forget', {key});
|
||||
|
||||
return cache;
|
||||
}
|
||||
};
|
||||
|
||||
const flush = (emitEvent = true) => {
|
||||
cache = {};
|
||||
@ -90,23 +115,26 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
||||
// if (emitEvent) eventBus.publish('net-cache-flush', {});
|
||||
|
||||
return cache;
|
||||
}
|
||||
};
|
||||
|
||||
const garbageCollect = (size = cacheSize) => {
|
||||
const values = Object.values(cache);
|
||||
if (values.length < size) return;
|
||||
|
||||
cache = values
|
||||
.sort((a,b) => b.expiryAt - a.expiryAt)
|
||||
.sort((a, b) => b.expiryAt - a.expiryAt)
|
||||
.slice(0, size)
|
||||
.reduce((cum, item) => {cum[item.key] = item; return cum;}, {});
|
||||
}
|
||||
.reduce((cum, item) => {
|
||||
cum[item.key] = item;
|
||||
return cum;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
// setUnsubscribe();
|
||||
// forgetUnsubscribe();
|
||||
// flushUnsubscribe();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
has,
|
||||
@ -117,5 +145,5 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => {
|
||||
forget,
|
||||
flush,
|
||||
destroy,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,18 +1,21 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
|
||||
const process = response => {
|
||||
const process = (response) => {
|
||||
if (!response || !Array.isArray(response)) return [];
|
||||
|
||||
return response.map(c => ({
|
||||
return response.map((c) => ({
|
||||
name: c.categoryName,
|
||||
displayName: c.categoryDisplayName,
|
||||
countsTowardsOverall: c.countsTowardsOverall,
|
||||
description: c.description
|
||||
description: c.description,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const get = async ({priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.ACCSABER.categories(priority, queueOptions);
|
||||
const get = async ({
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.ACCSABER.categories(priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import {dateFromString, formatDateRelative} from '../../../utils/date'
|
||||
import {LEADERBOARD_SCORES_PER_PAGE} from '../../../utils/accsaber/consts'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
import { dateFromString, formatDateRelative } from "../../../utils/date";
|
||||
import { LEADERBOARD_SCORES_PER_PAGE } from "../../../utils/accsaber/consts";
|
||||
|
||||
const process = response => {
|
||||
if (!response || !Array.isArray(response.responses) || response.responses.length !== 2 || !Array.isArray(response.responses[0])) return [];
|
||||
const process = (response) => {
|
||||
if (
|
||||
!response ||
|
||||
!Array.isArray(response.responses) ||
|
||||
response.responses.length !== 2 ||
|
||||
!Array.isArray(response.responses[0])
|
||||
)
|
||||
return [];
|
||||
|
||||
const page = response?.fetchOptions.page ?? 1;
|
||||
const totalItems = response.responses[0].length;
|
||||
@ -25,16 +31,32 @@ const process = response => {
|
||||
difficulty,
|
||||
} = mapInfo;
|
||||
|
||||
const song = {hash, name, subName, authorName, levelAuthorName, beatsaverKey};
|
||||
const diffInfo = {type: 'Standard', diff: difficulty?.toLowerCase()?.replace('plus', 'Plus')}
|
||||
const leaderboard = {leaderboardId, song, diffInfo, complexity, categoryDisplayName};
|
||||
const song = {
|
||||
hash,
|
||||
name,
|
||||
subName,
|
||||
authorName,
|
||||
levelAuthorName,
|
||||
beatsaverKey,
|
||||
};
|
||||
const diffInfo = {
|
||||
type: "Standard",
|
||||
diff: difficulty?.toLowerCase()?.replace("plus", "Plus"),
|
||||
};
|
||||
const leaderboard = {
|
||||
leaderboardId,
|
||||
song,
|
||||
diffInfo,
|
||||
complexity,
|
||||
categoryDisplayName,
|
||||
};
|
||||
|
||||
return {
|
||||
page,
|
||||
pageQty,
|
||||
totalItems,
|
||||
leaderboard,
|
||||
scores: response.responses[0].map(s => {
|
||||
scores: response.responses[0].map((s) => {
|
||||
let {
|
||||
accuracy: acc,
|
||||
ap,
|
||||
@ -48,14 +70,16 @@ const process = response => {
|
||||
|
||||
if (acc && Number.isFinite(acc)) acc *= 100;
|
||||
|
||||
timeSet = dateFromString(timeSet)
|
||||
timeSet = dateFromString(timeSet);
|
||||
const timeSetString = formatDateRelative(timeSet);
|
||||
|
||||
return {
|
||||
player: {
|
||||
name,
|
||||
playerId,
|
||||
playerInfo: {avatar: `https://cdn.accsaber.com/avatars/${playerId}.jpg`},
|
||||
playerInfo: {
|
||||
avatar: `https://cdn.accsaber.com/avatars/${playerId}.jpg`,
|
||||
},
|
||||
},
|
||||
score: {
|
||||
acc,
|
||||
@ -67,19 +91,30 @@ const process = response => {
|
||||
timeSetString,
|
||||
},
|
||||
other: rest,
|
||||
}
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const get = async ({leaderboardId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
|
||||
const get = async ({
|
||||
leaderboardId,
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => {
|
||||
const responses = await Promise.all([
|
||||
queue.ACCSABER.leaderboard(leaderboardId, page, priority, queueOptions),
|
||||
queue.ACCSABER.leaderboardInfo(leaderboardId, priority, queueOptions)
|
||||
queue.ACCSABER.leaderboardInfo(leaderboardId, priority, queueOptions),
|
||||
]);
|
||||
|
||||
return {...responses[0], body: {responses: responses.map(r => r.body), fetchOptions: {leaderboardId, page}}}
|
||||
}
|
||||
return {
|
||||
...responses[0],
|
||||
body: {
|
||||
responses: responses.map((r) => r.body),
|
||||
fetchOptions: { leaderboardId, page },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,28 +1,43 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import {fromAccSaberDateString} from '../../../utils/date'
|
||||
import {isDateObject} from '../../../utils/js'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
import { fromAccSaberDateString } from "../../../utils/date";
|
||||
import { isDateObject } from "../../../utils/js";
|
||||
|
||||
const process = response => {
|
||||
const process = (response) => {
|
||||
const playerId = response?.fetchOptions?.playerId ?? null;
|
||||
if (!response?.response || !Object.keys(response.response)?.length || !playerId) return [];
|
||||
|
||||
if (
|
||||
!response?.response ||
|
||||
!Object.keys(response.response)?.length ||
|
||||
!playerId
|
||||
)
|
||||
return [];
|
||||
|
||||
return {
|
||||
playerId,
|
||||
history: Object.entries(response.response)
|
||||
.map(([date, rank]) => ({date: fromAccSaberDateString(date), rank}))
|
||||
.filter(obj => isDateObject(obj?.date))
|
||||
.sort((a,b) => a.date.getTime() - b.date.getTime())
|
||||
,
|
||||
}
|
||||
}
|
||||
.map(([date, rank]) => ({ date: fromAccSaberDateString(date), rank }))
|
||||
.filter((obj) => isDateObject(obj?.date))
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime()),
|
||||
};
|
||||
};
|
||||
|
||||
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
|
||||
const response = await queue.ACCSABER.playerRankHistory(playerId, priority, queueOptions);
|
||||
const get = async ({
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => {
|
||||
const response = await queue.ACCSABER.playerRankHistory(
|
||||
playerId,
|
||||
priority,
|
||||
queueOptions,
|
||||
);
|
||||
|
||||
return {...response, body: {response: response.body, fetchOptions: {playerId}}}
|
||||
}
|
||||
return {
|
||||
...response,
|
||||
body: { response: response.body, fetchOptions: { playerId } },
|
||||
};
|
||||
};
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,23 +1,36 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
|
||||
const process = response => {
|
||||
const category = response?.fetchOptions?.category ?? 'overall';
|
||||
const process = (response) => {
|
||||
const category = response?.fetchOptions?.category ?? "overall";
|
||||
if (!response?.response || !Array.isArray(response.response)) return [];
|
||||
|
||||
return response.response.map(p => ({
|
||||
return response.response.map((p) => ({
|
||||
...p,
|
||||
id: `${p.playerId}-${category}`,
|
||||
category,
|
||||
lastUpdated: new Date(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const get = async ({category = 'overall', page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
|
||||
const response = await queue.ACCSABER.ranking(category, page, priority, queueOptions);
|
||||
const get = async ({
|
||||
category = "overall",
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => {
|
||||
const response = await queue.ACCSABER.ranking(
|
||||
category,
|
||||
page,
|
||||
priority,
|
||||
queueOptions,
|
||||
);
|
||||
|
||||
return {...response, body: {response: response.body, fetchOptions: {category}}}
|
||||
}
|
||||
return {
|
||||
...response,
|
||||
body: { response: response.body, fetchOptions: { category } },
|
||||
};
|
||||
};
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import {dateFromString} from '../../../utils/date'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
import { dateFromString } from "../../../utils/date";
|
||||
|
||||
const process = response => {
|
||||
const process = (response) => {
|
||||
const playerId = response?.fetchOptions?.playerId ?? null;
|
||||
if (!response?.response || !Array.isArray(response.response) || !playerId) return [];
|
||||
if (!response?.response || !Array.isArray(response.response) || !playerId)
|
||||
return [];
|
||||
|
||||
return response.response.map(s => {
|
||||
return response.response.map((s) => {
|
||||
let {
|
||||
songHash: hash,
|
||||
songName: name,
|
||||
@ -28,11 +29,27 @@ const process = response => {
|
||||
leaderboardId = parseInt(leaderboardId, 10);
|
||||
if (isNaN(leaderboardId)) leaderboardId = null;
|
||||
|
||||
const song = {hash, name, subName: '', authorName, levelAuthorName, beatsaverKey};
|
||||
const diffInfo = {type: 'Standard', diff: difficulty?.toLowerCase()?.replace('plus', 'Plus')}
|
||||
const leaderboard = {leaderboardId, song, diffInfo, complexity, categoryDisplayName};
|
||||
const song = {
|
||||
hash,
|
||||
name,
|
||||
subName: "",
|
||||
authorName,
|
||||
levelAuthorName,
|
||||
beatsaverKey,
|
||||
};
|
||||
const diffInfo = {
|
||||
type: "Standard",
|
||||
diff: difficulty?.toLowerCase()?.replace("plus", "Plus"),
|
||||
};
|
||||
const leaderboard = {
|
||||
leaderboardId,
|
||||
song,
|
||||
diffInfo,
|
||||
complexity,
|
||||
categoryDisplayName,
|
||||
};
|
||||
|
||||
const timeSet = dateFromString(s.timeSet)
|
||||
const timeSet = dateFromString(s.timeSet);
|
||||
return {
|
||||
id: `${playerId}-${s.leaderboardId}`,
|
||||
playerId,
|
||||
@ -41,18 +58,41 @@ const process = response => {
|
||||
ap,
|
||||
acc,
|
||||
leaderboard,
|
||||
score: {...originalScore, ap, unmodifiedScore: score, score, mods: null, timeSet, acc, percentage: acc, weightedAp},
|
||||
score: {
|
||||
...originalScore,
|
||||
ap,
|
||||
unmodifiedScore: score,
|
||||
score,
|
||||
mods: null,
|
||||
timeSet,
|
||||
acc,
|
||||
percentage: acc,
|
||||
weightedAp,
|
||||
},
|
||||
fetchedAt: new Date(),
|
||||
lastUpdated: new Date(),
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => {
|
||||
const response = await queue.ACCSABER.scores(playerId, page, priority, queueOptions);
|
||||
const get = async ({
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => {
|
||||
const response = await queue.ACCSABER.scores(
|
||||
playerId,
|
||||
page,
|
||||
priority,
|
||||
queueOptions,
|
||||
);
|
||||
|
||||
return {...response, body: {response: response.body, fetchOptions: {playerId, page}}}
|
||||
}
|
||||
return {
|
||||
...response,
|
||||
body: { response: response.body, fetchOptions: { playerId, page } },
|
||||
};
|
||||
};
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import process from './utils/process'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
import process from "./utils/process";
|
||||
|
||||
const get = async ({hash, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATMAPS.byHash(hash, priority, queueOptions);
|
||||
const get = async ({
|
||||
hash,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.BEATMAPS.byHash(hash, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import process from './utils/process'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
import process from "./utils/process";
|
||||
|
||||
const get = async ({key, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATMAPS.byKey(key, priority, queueOptions);
|
||||
const get = async ({
|
||||
key,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.BEATMAPS.byKey(key, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import {opt} from '../../../../utils/js'
|
||||
import { opt } from "../../../../utils/js";
|
||||
|
||||
export default response => {
|
||||
const versions = opt(response, 'versions');
|
||||
export default (response) => {
|
||||
const versions = opt(response, "versions");
|
||||
if (!versions || !Array.isArray(versions) || !versions.length) return null;
|
||||
|
||||
const lastIdx = versions.length - 1;
|
||||
|
||||
const hash = opt(versions, `${lastIdx}.hash`);
|
||||
const key = opt(response, 'id');
|
||||
const key = opt(response, "id");
|
||||
|
||||
if (!hash || !key || !hash.toLowerCase) return null;
|
||||
|
||||
return {...response, hash: hash.toLowerCase(), key}
|
||||
}
|
||||
return { ...response, hash: hash.toLowerCase(), key };
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import queue from '../../queues/queues'
|
||||
import {dateFromString} from '../../../utils/date'
|
||||
import createClient from '../generic'
|
||||
import queue from "../../queues/queues";
|
||||
import { dateFromString } from "../../../utils/date";
|
||||
import createClient from "../generic";
|
||||
|
||||
const SONG_DATA_TYPES = {
|
||||
None: 0,
|
||||
@ -8,14 +8,14 @@ const SONG_DATA_TYPES = {
|
||||
Fail: 2,
|
||||
Practice: 3,
|
||||
Replay: 4,
|
||||
Campaign: 5
|
||||
}
|
||||
Campaign: 5,
|
||||
};
|
||||
|
||||
const process = response => {
|
||||
const process = (response) => {
|
||||
if (!response || !Array.isArray(response)) return null;
|
||||
|
||||
return response
|
||||
.map(s => {
|
||||
.map((s) => {
|
||||
let {
|
||||
_id: beatSaviorId,
|
||||
playerID: playerId,
|
||||
@ -29,28 +29,65 @@ const process = response => {
|
||||
timeSet,
|
||||
trackers,
|
||||
trackers: {
|
||||
accuracyTracker: {accLeft, accRight, leftAverageCut, rightAverageCut, leftTimeDependence, rightTimeDependence, leftPreswing, leftPostswing, rightPreswing, rightPostswing},
|
||||
winTracker: {won, nbOfPause: pauses, rank},
|
||||
hitTracker: {bombHit, miss, missedNotes, badCuts, nbOfWallHit: wallHit, maxCombo},
|
||||
scoreTracker: {score},
|
||||
accuracyTracker: {
|
||||
accLeft,
|
||||
accRight,
|
||||
leftAverageCut,
|
||||
rightAverageCut,
|
||||
leftTimeDependence,
|
||||
rightTimeDependence,
|
||||
leftPreswing,
|
||||
leftPostswing,
|
||||
rightPreswing,
|
||||
rightPostswing,
|
||||
},
|
||||
winTracker: { won, nbOfPause: pauses, rank },
|
||||
hitTracker: {
|
||||
bombHit,
|
||||
miss,
|
||||
missedNotes,
|
||||
badCuts,
|
||||
nbOfWallHit: wallHit,
|
||||
maxCombo,
|
||||
},
|
||||
scoreTracker: { score },
|
||||
},
|
||||
} = s;
|
||||
|
||||
if (![SONG_DATA_TYPES.Pass, SONG_DATA_TYPES.Fail, SONG_DATA_TYPES.Campaign].includes(type)) return null;
|
||||
if (
|
||||
![
|
||||
SONG_DATA_TYPES.Pass,
|
||||
SONG_DATA_TYPES.Fail,
|
||||
SONG_DATA_TYPES.Campaign,
|
||||
].includes(type)
|
||||
)
|
||||
return null;
|
||||
|
||||
const leaderboardId = null;
|
||||
|
||||
hash = hash ? hash.toLowerCase() : null;
|
||||
|
||||
if (!playerId || !playerId.length || !hash || !hash.length || !diff || !diff.length || !score) return null;
|
||||
if (
|
||||
!playerId ||
|
||||
!playerId.length ||
|
||||
!hash ||
|
||||
!hash.length ||
|
||||
!diff ||
|
||||
!diff.length ||
|
||||
!score
|
||||
)
|
||||
return null;
|
||||
|
||||
const song = {hash, name, subName: '', authorName, levelAuthorName};
|
||||
const song = { hash, name, subName: "", authorName, levelAuthorName };
|
||||
const leaderboard = {
|
||||
leaderboardId,
|
||||
difficulty,
|
||||
diffInfo: {diff: diff === 'expertplus' ? 'expertPlus' : diff, type: 'Standard'},
|
||||
diffInfo: {
|
||||
diff: diff === "expertplus" ? "expertPlus" : diff,
|
||||
type: "Standard",
|
||||
},
|
||||
song,
|
||||
}
|
||||
};
|
||||
|
||||
const stats = {
|
||||
won,
|
||||
@ -62,9 +99,17 @@ const process = response => {
|
||||
bombHit,
|
||||
wallHit,
|
||||
maxCombo,
|
||||
accLeft, accRight, leftAverageCut, rightAverageCut, leftTimeDependence, rightTimeDependence,
|
||||
leftPreswing, leftPostswing, rightPreswing, rightPostswing,
|
||||
}
|
||||
accLeft,
|
||||
accRight,
|
||||
leftAverageCut,
|
||||
rightAverageCut,
|
||||
leftTimeDependence,
|
||||
rightTimeDependence,
|
||||
leftPreswing,
|
||||
leftPostswing,
|
||||
rightPreswing,
|
||||
rightPostswing,
|
||||
};
|
||||
|
||||
return {
|
||||
beatSaviorId,
|
||||
@ -72,24 +117,27 @@ const process = response => {
|
||||
leaderboardId,
|
||||
scoreId: null,
|
||||
hash,
|
||||
diff: diff === 'expertplus' ? 'expertPlus' : diff,
|
||||
diff: diff === "expertplus" ? "expertPlus" : diff,
|
||||
score,
|
||||
type,
|
||||
leaderboard,
|
||||
timeSet: dateFromString(timeSet),
|
||||
stats,
|
||||
trackers,
|
||||
}
|
||||
|
||||
};
|
||||
})
|
||||
.filter(s => s);
|
||||
.filter((s) => s);
|
||||
};
|
||||
|
||||
const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATSAVIOR.player(playerId, priority, queueOptions);
|
||||
const get = async ({
|
||||
playerId,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.BEATSAVIOR.player(playerId, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
export default {
|
||||
...client,
|
||||
SONG_DATA_TYPES
|
||||
SONG_DATA_TYPES,
|
||||
};
|
@ -1,19 +1,35 @@
|
||||
import queue, {getResponseBody, isResponseCached, updateResponseBody} from '../queues/queues'
|
||||
import queue, {
|
||||
getResponseBody,
|
||||
isResponseCached,
|
||||
updateResponseBody,
|
||||
} from "../queues/queues";
|
||||
|
||||
export default (get, process) => {
|
||||
const clientGet = async ({priority = queue.PRIORITY.FG_LOW, fullResponse = false, ...getOptions} = {}) => {
|
||||
const response = await get({...getOptions, priority});
|
||||
const clientGet = async ({
|
||||
priority = queue.PRIORITY.FG_LOW,
|
||||
fullResponse = false,
|
||||
...getOptions
|
||||
} = {}) => {
|
||||
const response = await get({ ...getOptions, priority });
|
||||
|
||||
return fullResponse ? response : getResponseBody(response);
|
||||
}
|
||||
};
|
||||
|
||||
const clientGetProcessed = async ({priority = queue.PRIORITY.FG_LOW, fullResponse = false, ...getOptions} = {}) => {
|
||||
const response = await clientGet({...getOptions, priority, fullResponse});
|
||||
const clientGetProcessed = async ({
|
||||
priority = queue.PRIORITY.FG_LOW,
|
||||
fullResponse = false,
|
||||
...getOptions
|
||||
} = {}) => {
|
||||
const response = await clientGet({ ...getOptions, priority, fullResponse });
|
||||
|
||||
const processedResponse = process(fullResponse ? getResponseBody(response) : response);
|
||||
const processedResponse = process(
|
||||
fullResponse ? getResponseBody(response) : response,
|
||||
);
|
||||
|
||||
return fullResponse ? updateResponseBody(response, processedResponse) : processedResponse;
|
||||
}
|
||||
return fullResponse
|
||||
? updateResponseBody(response, processedResponse)
|
||||
: processedResponse;
|
||||
};
|
||||
|
||||
return {
|
||||
get: clientGet,
|
||||
@ -21,5 +37,5 @@ export default (get, process) => {
|
||||
getProcessed: clientGetProcessed,
|
||||
getDataFromResponse: getResponseBody,
|
||||
isResponseCached,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,34 +1,63 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import createClient from '../../generic'
|
||||
import queue from "../../../queues/queues";
|
||||
import { opt } from "../../../../utils/js";
|
||||
import createClient from "../../generic";
|
||||
|
||||
const process = response => {
|
||||
if (!opt(response, 'scores') || !Array.isArray(response.scores)) return null;
|
||||
const process = (response) => {
|
||||
if (!opt(response, "scores") || !Array.isArray(response.scores)) return null;
|
||||
|
||||
const scores = response.scores.map(s => {
|
||||
let {unmodififiedScore: unmodifiedScore, mods, ...score} = s.score;
|
||||
const scores = response.scores.map((s) => {
|
||||
let { unmodififiedScore: unmodifiedScore, mods, ...score } = s.score;
|
||||
|
||||
if (mods && typeof mods === 'string') mods = mods.split(',').map(m => m.trim().toUpperCase()).filter(m => m.length);
|
||||
if (mods && typeof mods === "string")
|
||||
mods = mods
|
||||
.split(",")
|
||||
.map((m) => m.trim().toUpperCase())
|
||||
.filter((m) => m.length);
|
||||
else if (!mods) mods = null;
|
||||
|
||||
const acc = unmodifiedScore && opt(score, 'maxScore') ? unmodifiedScore / score.maxScore * 100 : opt(score, 'acc', null);
|
||||
const percentage = opt(score, 'score') && opt(score, 'maxScore') ? score.score / score.maxScore * 100 : opt(score, 'percentage', null);
|
||||
const acc =
|
||||
unmodifiedScore && opt(score, "maxScore")
|
||||
? (unmodifiedScore / score.maxScore) * 100
|
||||
: opt(score, "acc", null);
|
||||
const percentage =
|
||||
opt(score, "score") && opt(score, "maxScore")
|
||||
? (score.score / score.maxScore) * 100
|
||||
: opt(score, "percentage", null);
|
||||
|
||||
const ppWeighted = opt(score, 'pp') && opt(score, 'weight') ? score.pp * score.weight : null;
|
||||
const ppWeighted =
|
||||
opt(score, "pp") && opt(score, "weight") ? score.pp * score.weight : null;
|
||||
|
||||
return {
|
||||
...s,
|
||||
score: {...score, unmodifiedScore: unmodifiedScore || null, mods, acc, percentage, ppWeighted},
|
||||
score: {
|
||||
...score,
|
||||
unmodifiedScore: unmodifiedScore || null,
|
||||
mods,
|
||||
acc,
|
||||
percentage,
|
||||
ppWeighted,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...response,
|
||||
scores
|
||||
}
|
||||
}
|
||||
scores,
|
||||
};
|
||||
};
|
||||
|
||||
const get = async ({leaderboardId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.leaderboard(leaderboardId, page, priority, queueOptions);
|
||||
const get = async ({
|
||||
leaderboardId,
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) =>
|
||||
queue.SCORESABER_PAGE.leaderboard(
|
||||
leaderboardId,
|
||||
page,
|
||||
priority,
|
||||
queueOptions,
|
||||
);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,34 +1,56 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import createClient from '../../generic'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import queue from "../../../queues/queues";
|
||||
import createClient from "../../generic";
|
||||
import { opt } from "../../../../utils/js";
|
||||
|
||||
const process = response => {
|
||||
if (!opt(response, 'playerInfo')) return null;
|
||||
const process = (response) => {
|
||||
if (!opt(response, "playerInfo")) return null;
|
||||
|
||||
const {playerInfo: info, scoreStats} = response;
|
||||
const {playerId, playerName: name, country, countryRank, avatar, permissions, ...playerInfo} = info;
|
||||
const { playerInfo: info, scoreStats } = response;
|
||||
const {
|
||||
playerId,
|
||||
playerName: name,
|
||||
country,
|
||||
countryRank,
|
||||
avatar,
|
||||
permissions,
|
||||
...playerInfo
|
||||
} = info;
|
||||
|
||||
if (avatar) {
|
||||
if (!avatar.startsWith('http'))
|
||||
playerInfo.avatar = `${queue.SCORESABER_API.SS_API_HOST}${!avatar.startsWith('/') ? '/' : ''}${avatar}`;
|
||||
else
|
||||
playerInfo.avatar = avatar;
|
||||
if (!avatar.startsWith("http"))
|
||||
playerInfo.avatar = `${queue.SCORESABER_API.SS_API_HOST}${
|
||||
!avatar.startsWith("/") ? "/" : ""
|
||||
}${avatar}`;
|
||||
else playerInfo.avatar = avatar;
|
||||
}
|
||||
|
||||
playerInfo.banned = !!playerInfo.banned;
|
||||
playerInfo.inactive = !!playerInfo.inactive;
|
||||
playerInfo.rankHistory = playerInfo.history && playerInfo.history.length
|
||||
? playerInfo.history.split(',').map(r => parseInt(r, 10)).filter(r => !isNaN(r))
|
||||
playerInfo.rankHistory =
|
||||
playerInfo.history && playerInfo.history.length
|
||||
? playerInfo.history
|
||||
.split(",")
|
||||
.map((r) => parseInt(r, 10))
|
||||
.filter((r) => !isNaN(r))
|
||||
: [];
|
||||
delete playerInfo.history;
|
||||
|
||||
playerInfo.externalProfileUrl = null;
|
||||
playerInfo.countries = [{country, rank: countryRank}];
|
||||
playerInfo.countries = [{ country, rank: countryRank }];
|
||||
|
||||
return {playerId, name, playerInfo, scoreStats: scoreStats ? scoreStats : null};
|
||||
return {
|
||||
playerId,
|
||||
name,
|
||||
playerInfo,
|
||||
scoreStats: scoreStats ? scoreStats : null,
|
||||
};
|
||||
};
|
||||
|
||||
const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.player(playerId, priority, queueOptions);
|
||||
const get = async ({
|
||||
playerId,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.SCORESABER_API.player(playerId, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,21 +1,27 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import api from './api'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import createClient from '../../generic'
|
||||
import queue from "../../../queues/queues";
|
||||
import api from "./api";
|
||||
import { opt } from "../../../../utils/js";
|
||||
import createClient from "../../generic";
|
||||
|
||||
const process = response => {
|
||||
const apiProcessedResponse = api.process(response && response.player ? response.player : null);
|
||||
const process = (response) => {
|
||||
const apiProcessedResponse = api.process(
|
||||
response && response.player ? response.player : null,
|
||||
);
|
||||
|
||||
if (!opt(apiProcessedResponse, 'player.playerInfo')) return null;
|
||||
if (!opt(apiProcessedResponse, "player.playerInfo")) return null;
|
||||
|
||||
const recentPlay = opt(response, 'player.recentPlay');
|
||||
const recentPlayLastUpdated = opt(response, 'player.recentPlayLastUpdated');
|
||||
const recentPlay = opt(response, "player.recentPlay");
|
||||
const recentPlayLastUpdated = opt(response, "player.recentPlayLastUpdated");
|
||||
if (recentPlay && recentPlayLastUpdated) {
|
||||
apiProcessedResponse.playerInfo.recentPlay = recentPlay;
|
||||
apiProcessedResponse.playerInfo.recentPlayLastUpdated = recentPlayLastUpdated;
|
||||
apiProcessedResponse.playerInfo.recentPlayLastUpdated =
|
||||
recentPlayLastUpdated;
|
||||
}
|
||||
|
||||
const externalProfileUrl = opt(response, 'player.playerInfo.externalProfileUrl');
|
||||
const externalProfileUrl = opt(
|
||||
response,
|
||||
"player.playerInfo.externalProfileUrl",
|
||||
);
|
||||
if (externalProfileUrl) {
|
||||
apiProcessedResponse.playerInfo.externalProfileUrl = externalProfileUrl;
|
||||
}
|
||||
@ -23,7 +29,11 @@ const process = response => {
|
||||
return apiProcessedResponse;
|
||||
};
|
||||
|
||||
const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.player(playerId, priority, queueOptions);
|
||||
const get = async ({
|
||||
playerId,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.SCORESABER_PAGE.player(playerId, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import process from './utils/process'
|
||||
import createClient from '../../generic'
|
||||
import queue from "../../../queues/queues";
|
||||
import process from "./utils/process";
|
||||
import createClient from "../../generic";
|
||||
|
||||
const get = async ({query, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.findPlayer(query, priority, queueOptions);
|
||||
const get = async ({
|
||||
query,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.SCORESABER_API.findPlayer(query, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import createClient from '../../generic'
|
||||
import queue from "../../../queues/queues";
|
||||
import { opt } from "../../../../utils/js";
|
||||
import createClient from "../../generic";
|
||||
|
||||
const process = response => opt(response, 'pages', null)
|
||||
const process = (response) => opt(response, "pages", null);
|
||||
|
||||
const get = async ({priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.rankingGlobalPages(priority, queueOptions);
|
||||
const get = async ({
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.SCORESABER_API.rankingGlobalPages(priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import process from './utils/process'
|
||||
import createClient from '../../generic'
|
||||
import queue from "../../../queues/queues";
|
||||
import process from "./utils/process";
|
||||
import createClient from "../../generic";
|
||||
|
||||
const get = async ({page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.rankingGlobal(page, priority, queueOptions);
|
||||
const get = async ({
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.SCORESABER_API.rankingGlobal(page, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,17 +1,23 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import api from './api-ranking-global'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import createClient from '../../generic'
|
||||
import queue from "../../../queues/queues";
|
||||
import api from "./api-ranking-global";
|
||||
import { opt } from "../../../../utils/js";
|
||||
import createClient from "../../generic";
|
||||
|
||||
const process = response => {
|
||||
const process = (response) => {
|
||||
const apiProcessedResponse = api.process(response);
|
||||
|
||||
if (!opt(response, 'players')) return null;
|
||||
if (!opt(response, "players")) return null;
|
||||
|
||||
return apiProcessedResponse;
|
||||
}
|
||||
};
|
||||
|
||||
const get = async ({country, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.countryRanking(country, page, priority, queueOptions);
|
||||
const get = async ({
|
||||
country,
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) =>
|
||||
queue.SCORESABER_PAGE.countryRanking(country, page, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,17 +1,28 @@
|
||||
import {opt} from '../../../../../utils/js'
|
||||
import queue from '../../../../queues/queues'
|
||||
import { opt } from "../../../../../utils/js";
|
||||
import queue from "../../../../queues/queues";
|
||||
|
||||
export default response => {
|
||||
if (!opt(response, 'players')) return null;
|
||||
export default (response) => {
|
||||
if (!opt(response, "players")) return null;
|
||||
|
||||
if (!Array.isArray(response.players)) return null;
|
||||
|
||||
return response.players.map(player => {
|
||||
let {avatar, country, difference, history, playerId, playerName: name, pp, rank} = player;
|
||||
return response.players.map((player) => {
|
||||
let {
|
||||
avatar,
|
||||
country,
|
||||
difference,
|
||||
history,
|
||||
playerId,
|
||||
playerName: name,
|
||||
pp,
|
||||
rank,
|
||||
} = player;
|
||||
|
||||
if (avatar) {
|
||||
if (!avatar.startsWith('http'))
|
||||
avatar = `${queue.SCORESABER_API.SS_API_HOST}${!avatar.startsWith('/') ? '/' : ''}${avatar}`;
|
||||
if (!avatar.startsWith("http"))
|
||||
avatar = `${queue.SCORESABER_API.SS_API_HOST}${
|
||||
!avatar.startsWith("/") ? "/" : ""
|
||||
}${avatar}`;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -19,16 +30,20 @@ export default response => {
|
||||
name,
|
||||
playerInfo: {
|
||||
avatar,
|
||||
countries: [{country, rank: null}],
|
||||
countries: [{ country, rank: null }],
|
||||
pp,
|
||||
rank,
|
||||
rankHistory: history && history.length
|
||||
? history.split(',').map(r => parseInt(r, 10)).filter(r => !isNaN(r))
|
||||
rankHistory:
|
||||
history && history.length
|
||||
? history
|
||||
.split(",")
|
||||
.map((r) => parseInt(r, 10))
|
||||
.filter((r) => !isNaN(r))
|
||||
: [],
|
||||
},
|
||||
others: {
|
||||
difference,
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
@ -1,9 +1,13 @@
|
||||
import createClient from '../../generic'
|
||||
import queues from '../../../queues/queues'
|
||||
import createClient from "../../generic";
|
||||
import queues from "../../../queues/queues";
|
||||
|
||||
const process = response => response;
|
||||
const process = (response) => response;
|
||||
|
||||
const get = async ({page = 1, priority = queues.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queues.SCORESABER_PAGE.rankeds(page, priority, queueOptions)
|
||||
const get = async ({
|
||||
page = 1,
|
||||
priority = queues.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queues.SCORESABER_PAGE.rankeds(page, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,8 +1,14 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import process from './utils/process';
|
||||
import createClient from '../../generic'
|
||||
import queue from "../../../queues/queues";
|
||||
import process from "./utils/process";
|
||||
import createClient from "../../generic";
|
||||
|
||||
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.recentScores(playerId, page, priority, queueOptions);
|
||||
const get = async ({
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) =>
|
||||
queue.SCORESABER_API.recentScores(playerId, page, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,8 +1,14 @@
|
||||
import queue from '../../../queues/queues'
|
||||
import createClient from '../../generic'
|
||||
import process from './utils/process'
|
||||
import queue from "../../../queues/queues";
|
||||
import createClient from "../../generic";
|
||||
import process from "./utils/process";
|
||||
|
||||
const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.topScores(playerId, page, priority, queueOptions);
|
||||
const get = async ({
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) =>
|
||||
queue.SCORESABER_API.topScores(playerId, page, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
|
@ -1,11 +1,16 @@
|
||||
import {dateFromString} from '../../../../../utils/date'
|
||||
import {extractDiffAndType} from '../../../../../utils/scoresaber/format'
|
||||
import {opt} from '../../../../../utils/js'
|
||||
import { dateFromString } from "../../../../../utils/date";
|
||||
import { extractDiffAndType } from "../../../../../utils/scoresaber/format";
|
||||
import { opt } from "../../../../../utils/js";
|
||||
|
||||
export default response => {
|
||||
if (!opt(response, 'scores') || !Array.isArray(response.scores) || !opt(response, 'scores.0.scoreId')) return [];
|
||||
export default (response) => {
|
||||
if (
|
||||
!opt(response, "scores") ||
|
||||
!Array.isArray(response.scores) ||
|
||||
!opt(response, "scores.0.scoreId")
|
||||
)
|
||||
return [];
|
||||
|
||||
return response.scores.map(s => {
|
||||
return response.scores.map((s) => {
|
||||
const {
|
||||
songHash: hash,
|
||||
songName: name,
|
||||
@ -18,25 +23,44 @@ export default response => {
|
||||
...originalScore
|
||||
} = s;
|
||||
|
||||
const song = {hash, name, subName, authorName, levelAuthorName};
|
||||
const song = { hash, name, subName, authorName, levelAuthorName };
|
||||
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;
|
||||
|
||||
const acc = unmodifiedScore && opt(score, 'maxScore') ? unmodifiedScore / score.maxScore * 100 : null;
|
||||
const percentage = opt(score, 'score') && opt(score, 'maxScore') ? score.score / score.maxScore * 100 : null;
|
||||
const acc =
|
||||
unmodifiedScore && opt(score, "maxScore")
|
||||
? (unmodifiedScore / score.maxScore) * 100
|
||||
: null;
|
||||
const percentage =
|
||||
opt(score, "score") && opt(score, "maxScore")
|
||||
? (score.score / score.maxScore) * 100
|
||||
: null;
|
||||
|
||||
const ppWeighted = opt(score, 'pp') && opt(score, 'weight') ? score.pp * score.weight : null;
|
||||
const ppWeighted =
|
||||
opt(score, "pp") && opt(score, "weight") ? score.pp * score.weight : null;
|
||||
|
||||
return {
|
||||
leaderboard,
|
||||
score: {...score, unmodifiedScore, mods, timeSet: dateFromString(score.timeSet), acc, percentage, ppWeighted},
|
||||
score: {
|
||||
...score,
|
||||
unmodifiedScore,
|
||||
mods,
|
||||
timeSet: dateFromString(score.timeSet),
|
||||
acc,
|
||||
percentage,
|
||||
ppWeighted,
|
||||
},
|
||||
fetchedAt: new Date(),
|
||||
lastUpdated: new Date(),
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,17 +1,22 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import {opt} from '../../../utils/js'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
import { opt } from "../../../utils/js";
|
||||
|
||||
const process = response => {
|
||||
if (!opt(response, 'data.0')) return null;
|
||||
const process = (response) => {
|
||||
if (!opt(response, "data.0")) return null;
|
||||
|
||||
return {...response.data[0], profileLastUpdated: new Date()};
|
||||
return { ...response.data[0], profileLastUpdated: new Date() };
|
||||
};
|
||||
|
||||
const get = async ({accessToken, login, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.TWITCH.profile(accessToken, login, priority, queueOptions);
|
||||
const get = async ({
|
||||
accessToken,
|
||||
login,
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.TWITCH.profile(accessToken, login, priority, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
export default {
|
||||
...client
|
||||
}
|
||||
...client,
|
||||
};
|
||||
|
@ -1,16 +1,22 @@
|
||||
import queue from '../../queues/queues'
|
||||
import createClient from '../generic'
|
||||
import queue from "../../queues/queues";
|
||||
import createClient from "../generic";
|
||||
|
||||
const process = response => {
|
||||
const process = (response) => {
|
||||
if (!response || !response.data || !Array.isArray(response.data)) return null;
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const get = async ({accessToken, userId, type = 'archive', priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.TWITCH.videos(accessToken, userId, type, queueOptions);
|
||||
const get = async ({
|
||||
accessToken,
|
||||
userId,
|
||||
type = "archive",
|
||||
priority = queue.PRIORITY.FG_HIGH,
|
||||
...queueOptions
|
||||
} = {}) => queue.TWITCH.videos(accessToken, userId, type, queueOptions);
|
||||
|
||||
const client = createClient(get, process);
|
||||
|
||||
export default {
|
||||
...client,
|
||||
}
|
||||
};
|
||||
|
@ -1,45 +1,48 @@
|
||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
||||
import createPlayerService from '../services/scoresaber/player'
|
||||
import log from '../utils/logger'
|
||||
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||
import createPlayerService from "../services/scoresaber/player";
|
||||
import log from "../utils/logger";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export default (dlManager) => {
|
||||
if (initialized) {
|
||||
log.debug(`Command processor already initialized.`, 'CmdProcessor');
|
||||
log.debug(`Command processor already initialized.`, "CmdProcessor");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const playerService = createPlayerService();
|
||||
|
||||
eventBus.on('data-imported', () => {
|
||||
if (window) window.location.reload()
|
||||
eventBus.on("data-imported", () => {
|
||||
if (window) window.location.reload();
|
||||
});
|
||||
|
||||
eventBus.on('player-add-cmd', async ({playerId}) => {
|
||||
eventBus.on("player-add-cmd", async ({ playerId }) => {
|
||||
await dlManager.enqueuePlayer(playerId);
|
||||
});
|
||||
|
||||
eventBus.on('player-remove-cmd', async ({playerId, purgeScores = false}) => {
|
||||
eventBus.on(
|
||||
"player-remove-cmd",
|
||||
async ({ playerId, purgeScores = false }) => {
|
||||
if (!playerId) return;
|
||||
|
||||
await playerService.remove(playerId, purgeScores);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
eventBus.on('dl-manager-pause-cmd', () => {
|
||||
log.debug('Pause Dl Manager', 'CmdProcessor');
|
||||
eventBus.on("dl-manager-pause-cmd", () => {
|
||||
log.debug("Pause Dl Manager", "CmdProcessor");
|
||||
|
||||
dlManager.pause();
|
||||
});
|
||||
|
||||
eventBus.on('dl-manager-unpause-cmd', () => {
|
||||
log.debug('Unpause Dl Manager', 'CmdProcessor');
|
||||
eventBus.on("dl-manager-unpause-cmd", () => {
|
||||
log.debug("Unpause Dl Manager", "CmdProcessor");
|
||||
|
||||
dlManager.start();
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
|
||||
log.info(`Command processor initialized`, 'CmdProcessor');
|
||||
}
|
||||
log.info(`Command processor initialized`, "CmdProcessor");
|
||||
};
|
||||
|
@ -1,15 +1,15 @@
|
||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
||||
import log from '../utils/logger'
|
||||
import createQueue, {PRIORITY} from '../utils/queue'
|
||||
import {configStore} from '../stores/config'
|
||||
import createRankedsStore from '../stores/scoresaber/rankeds'
|
||||
import createPlayerService from '../services/scoresaber/player'
|
||||
import createScoresService from '../services/scoresaber/scores'
|
||||
import createBeatSaviorService from '../services/beatsavior'
|
||||
import createAccSaberService from '../services/accsaber'
|
||||
import {PRIORITY as HTTP_QUEUE_PRIORITY} from './queues/http-queue'
|
||||
import {HOUR, MINUTE} from '../utils/date'
|
||||
import {opt} from '../utils/js'
|
||||
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||
import log from "../utils/logger";
|
||||
import createQueue, { PRIORITY } from "../utils/queue";
|
||||
import { configStore } from "../stores/config";
|
||||
import createRankedsStore from "../stores/scoresaber/rankeds";
|
||||
import createPlayerService from "../services/scoresaber/player";
|
||||
import createScoresService from "../services/scoresaber/scores";
|
||||
import createBeatSaviorService from "../services/beatsavior";
|
||||
import createAccSaberService from "../services/accsaber";
|
||||
import { PRIORITY as HTTP_QUEUE_PRIORITY } from "./queues/http-queue";
|
||||
import { HOUR, MINUTE } from "../utils/date";
|
||||
import { opt } from "../utils/js";
|
||||
|
||||
const INTERVAL_TICK = MINUTE;
|
||||
|
||||
@ -22,110 +22,198 @@ let beatSaviorService = null;
|
||||
let accSaberService = null;
|
||||
|
||||
const TYPES = {
|
||||
BEATSAVIOR: {name: 'BEATSAVIOR', priority: PRIORITY.LOW},
|
||||
RANKEDS: {name: 'RANKEDS', priority: PRIORITY.LOW},
|
||||
ACCSABER: {name: 'ACCSABER', priority: PRIORITY.NORMAL},
|
||||
PLAYER_SCORES: {name: 'PLAYER-SCORES', priority: PRIORITY.NORMAL},
|
||||
PLAYER_SCORES_UPDATE_QUEUE: {name: 'PLAYER_SCORES_UPDATE_QUEUE', priority: PRIORITY.LOWEST},
|
||||
ACTIVE_PLAYERS: {name: 'ACTIVE-PLAYERS', priority: PRIORITY.HIGH},
|
||||
MAIN_PLAYER: {name: 'MAIN-PLAYER', priority: PRIORITY.HIGHEST},
|
||||
}
|
||||
BEATSAVIOR: { name: "BEATSAVIOR", priority: PRIORITY.LOW },
|
||||
RANKEDS: { name: "RANKEDS", priority: PRIORITY.LOW },
|
||||
ACCSABER: { name: "ACCSABER", priority: PRIORITY.NORMAL },
|
||||
PLAYER_SCORES: { name: "PLAYER-SCORES", priority: PRIORITY.NORMAL },
|
||||
PLAYER_SCORES_UPDATE_QUEUE: {
|
||||
name: "PLAYER_SCORES_UPDATE_QUEUE",
|
||||
priority: PRIORITY.LOWEST,
|
||||
},
|
||||
ACTIVE_PLAYERS: { name: "ACTIVE-PLAYERS", priority: PRIORITY.HIGH },
|
||||
MAIN_PLAYER: { name: "MAIN-PLAYER", priority: PRIORITY.HIGHEST },
|
||||
};
|
||||
|
||||
const enqueue = async (queue, type, force = false, data = null, then = null) => {
|
||||
const enqueue = async (
|
||||
queue,
|
||||
type,
|
||||
force = false,
|
||||
data = null,
|
||||
then = null,
|
||||
) => {
|
||||
if (!type || !type.name || !Number.isFinite(type.priority)) {
|
||||
log.warn(`Unknown type enqueued.`, 'DlManager', type);
|
||||
log.warn(`Unknown type enqueued.`, "DlManager", type);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Try to enqueue type ${type.name}. Forced: ${force}, data: ${JSON.stringify(data)}`, 'DlManager');
|
||||
log.debug(
|
||||
`Try to enqueue type ${type.name}. Forced: ${force}, data: ${JSON.stringify(
|
||||
data,
|
||||
)}`,
|
||||
"DlManager",
|
||||
);
|
||||
|
||||
const priority = force ? PRIORITY.HIGHEST : type.priority;
|
||||
const networkPriority = priority === PRIORITY.HIGHEST ? HTTP_QUEUE_PRIORITY.BG_HIGH : HTTP_QUEUE_PRIORITY.BG_NORMAL;
|
||||
const networkPriority =
|
||||
priority === PRIORITY.HIGHEST
|
||||
? HTTP_QUEUE_PRIORITY.BG_HIGH
|
||||
: HTTP_QUEUE_PRIORITY.BG_NORMAL;
|
||||
|
||||
const processThen = async (promise, then = null) => {
|
||||
promise.then(result => {
|
||||
if(then) log.debug('Processing then command...', 'DlManager');
|
||||
promise.then((result) => {
|
||||
if (then) log.debug("Processing then command...", "DlManager");
|
||||
|
||||
return then ? {result, thenResult: then()} : result;
|
||||
})
|
||||
}
|
||||
return then ? { result, thenResult: then() } : result;
|
||||
});
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case TYPES.MAIN_PLAYER:
|
||||
if (mainPlayerId) {
|
||||
log.debug(`Enqueue main player`, 'DlManager');
|
||||
log.debug(`Enqueue main player`, "DlManager");
|
||||
|
||||
await Promise.all([
|
||||
enqueue(queue, {...TYPES.ACTIVE_PLAYERS, priority: PRIORITY.HIGHEST}, force, {playerId: mainPlayerId}),
|
||||
enqueue(queue, {...TYPES.PLAYER_SCORES, priority: PRIORITY.HIGHEST}, force, {playerId: mainPlayerId}),
|
||||
enqueue(
|
||||
queue,
|
||||
{ ...TYPES.ACTIVE_PLAYERS, priority: PRIORITY.HIGHEST },
|
||||
force,
|
||||
{ playerId: mainPlayerId },
|
||||
),
|
||||
enqueue(
|
||||
queue,
|
||||
{ ...TYPES.PLAYER_SCORES, priority: PRIORITY.HIGHEST },
|
||||
force,
|
||||
{ playerId: mainPlayerId },
|
||||
),
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
case TYPES.RANKEDS:
|
||||
log.debug(`Enqueue rankeds`, 'DlManager');
|
||||
log.debug(`Enqueue rankeds`, "DlManager");
|
||||
|
||||
if (!rankedsStore) rankedsStore = await createRankedsStore();
|
||||
|
||||
processThen(queue.add(async () => rankedsStore.refresh(force, networkPriority), priority), then)
|
||||
.then(_ => log.debug('Enqueued rankeds processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(
|
||||
async () => rankedsStore.refresh(force, networkPriority),
|
||||
priority,
|
||||
),
|
||||
then,
|
||||
).then((_) => log.debug("Enqueued rankeds processed.", "DlManager"));
|
||||
break;
|
||||
|
||||
case TYPES.ACTIVE_PLAYERS:
|
||||
log.debug(`Enqueue active players`, 'DlManager');
|
||||
log.debug(`Enqueue active players`, "DlManager");
|
||||
|
||||
if (data && data.playerId) {
|
||||
if (data.add)
|
||||
processThen(queue.add(async () => playerService.add(data.playerId, networkPriority), priority), then)
|
||||
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(
|
||||
async () => playerService.add(data.playerId, networkPriority),
|
||||
priority,
|
||||
),
|
||||
then,
|
||||
).then((_) =>
|
||||
log.debug("Enqueued active players processed.", "DlManager"),
|
||||
);
|
||||
else
|
||||
processThen(queue.add(async () => playerService.refresh(data.playerId, force, networkPriority), priority), then)
|
||||
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(
|
||||
async () =>
|
||||
playerService.refresh(data.playerId, force, networkPriority),
|
||||
priority,
|
||||
),
|
||||
then,
|
||||
).then((_) =>
|
||||
log.debug("Enqueued active players processed.", "DlManager"),
|
||||
);
|
||||
} else
|
||||
processThen(queue.add(async () => playerService.refreshAll(force, networkPriority), priority), then)
|
||||
.then(_ => log.debug('Enqueued active players processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(
|
||||
async () => playerService.refreshAll(force, networkPriority),
|
||||
priority,
|
||||
),
|
||||
then,
|
||||
).then((_) =>
|
||||
log.debug("Enqueued active players processed.", "DlManager"),
|
||||
);
|
||||
break;
|
||||
|
||||
case TYPES.PLAYER_SCORES:
|
||||
log.debug(`Enqueue players scores`, 'DlManager');
|
||||
log.debug(`Enqueue players scores`, "DlManager");
|
||||
|
||||
if (data && data.playerId)
|
||||
processThen(queue.add(async () => scoresService.refresh(data.playerId, force, networkPriority), priority), then)
|
||||
.then(_ => log.debug('Enqueued players scores processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(
|
||||
async () =>
|
||||
scoresService.refresh(data.playerId, force, networkPriority),
|
||||
priority,
|
||||
),
|
||||
then,
|
||||
).then((_) =>
|
||||
log.debug("Enqueued players scores processed.", "DlManager"),
|
||||
);
|
||||
else
|
||||
processThen(queue.add(async () => scoresService.refreshAll(force, networkPriority), priority), then)
|
||||
.then(_ => log.debug('Enqueued players scores processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(
|
||||
async () => scoresService.refreshAll(force, networkPriority),
|
||||
priority,
|
||||
),
|
||||
then,
|
||||
).then((_) =>
|
||||
log.debug("Enqueued players scores processed.", "DlManager"),
|
||||
);
|
||||
break;
|
||||
|
||||
case TYPES.BEATSAVIOR:
|
||||
log.debug(`Enqueue Beat Savior`, 'DlManager');
|
||||
log.debug(`Enqueue Beat Savior`, "DlManager");
|
||||
|
||||
processThen(queue.add(async () => beatSaviorService.refreshAll(force, networkPriority), priority), then)
|
||||
.then(_ => log.debug('Enqueued Beat Savior processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(
|
||||
async () => beatSaviorService.refreshAll(force, networkPriority),
|
||||
priority,
|
||||
),
|
||||
then,
|
||||
).then((_) => log.debug("Enqueued Beat Savior processed.", "DlManager"));
|
||||
|
||||
break;
|
||||
|
||||
case TYPES.PLAYER_SCORES_UPDATE_QUEUE:
|
||||
log.debug(`Enqueue player scores rank && pp updates`, 'DlManager');
|
||||
log.debug(`Enqueue player scores rank && pp updates`, "DlManager");
|
||||
|
||||
processThen(queue.add(async () => scoresService.updateRankAndPpFromTheQueue(), priority), then)
|
||||
.then(_ => log.debug('Enqueued player scores rank & pp updates processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(
|
||||
async () => scoresService.updateRankAndPpFromTheQueue(),
|
||||
priority,
|
||||
),
|
||||
then,
|
||||
).then((_) =>
|
||||
log.debug(
|
||||
"Enqueued player scores rank & pp updates processed.",
|
||||
"DlManager",
|
||||
),
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case TYPES.ACCSABER:
|
||||
log.debug(`Enqueue AccSaber updates`, 'DlManager');
|
||||
log.debug(`Enqueue AccSaber updates`, "DlManager");
|
||||
|
||||
processThen(queue.add(async () => accSaberService.refreshAll(), priority), then)
|
||||
.then(_ => log.debug('Enqueued AccSaber updates processed.', 'DlManager'));
|
||||
processThen(
|
||||
queue.add(async () => accSaberService.refreshAll(), priority),
|
||||
then,
|
||||
).then((_) =>
|
||||
log.debug("Enqueued AccSaber updates processed.", "DlManager"),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const enqueueAllJobs = async queue => {
|
||||
log.debug(`Try to enqueue & process queue.`, 'DlManager');
|
||||
const enqueueAllJobs = async (queue) => {
|
||||
log.debug(`Try to enqueue & process queue.`, "DlManager");
|
||||
|
||||
await Promise.all([
|
||||
enqueue(queue, TYPES.MAIN_PLAYER),
|
||||
@ -137,18 +225,18 @@ const enqueueAllJobs = async queue => {
|
||||
|
||||
// it should be at the end of the queue
|
||||
enqueue(queue, TYPES.PLAYER_SCORES_UPDATE_QUEUE),
|
||||
])
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
let intervalId;
|
||||
const startSyncing = async queue => {
|
||||
const startSyncing = async (queue) => {
|
||||
await enqueueAllJobs(queue);
|
||||
intervalId = setInterval(() => enqueueAllJobs(queue), INTERVAL_TICK);
|
||||
}
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
if (initialized) {
|
||||
log.debug(`Download manager already initialized.`, 'DlManager');
|
||||
log.debug(`Download manager already initialized.`, "DlManager");
|
||||
|
||||
return;
|
||||
}
|
||||
@ -161,49 +249,54 @@ export default async () => {
|
||||
|
||||
mainPlayerId = configStore.getMainPlayerId();
|
||||
|
||||
configStore.subscribe(config => {
|
||||
const newMainPlayerId = opt(config, 'users.main')
|
||||
configStore.subscribe((config) => {
|
||||
const newMainPlayerId = opt(config, "users.main");
|
||||
if (mainPlayerId !== newMainPlayerId) {
|
||||
mainPlayerId = newMainPlayerId;
|
||||
|
||||
log.debug(`Main player changed to ${mainPlayerId}`, 'DlManager')
|
||||
log.debug(`Main player changed to ${mainPlayerId}`, "DlManager");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
playerService = createPlayerService();
|
||||
scoresService = createScoresService();
|
||||
beatSaviorService = createBeatSaviorService();
|
||||
accSaberService = createAccSaberService();
|
||||
|
||||
eventBus.leaderStore.subscribe(async isLeader => {
|
||||
eventBus.leaderStore.subscribe(async (isLeader) => {
|
||||
if (isLeader) {
|
||||
queue.clear();
|
||||
queue.start();
|
||||
|
||||
const nodeId = eventBus.getNodeId();
|
||||
log.info(`Node ${nodeId} is a leader, queue processing enabled`, 'DlManager')
|
||||
|
||||
await startSyncing(queue)
|
||||
}
|
||||
})
|
||||
|
||||
const enqueuePlayer = async playerId => {
|
||||
await enqueue(
|
||||
queue, TYPES.ACTIVE_PLAYERS, true,
|
||||
{playerId, add: true},
|
||||
async () => enqueue(queue, TYPES.PLAYER_SCORES, true, {playerId}),
|
||||
log.info(
|
||||
`Node ${nodeId} is a leader, queue processing enabled`,
|
||||
"DlManager",
|
||||
);
|
||||
|
||||
await startSyncing(queue);
|
||||
}
|
||||
});
|
||||
|
||||
const enqueuePlayer = async (playerId) => {
|
||||
await enqueue(
|
||||
queue,
|
||||
TYPES.ACTIVE_PLAYERS,
|
||||
true,
|
||||
{ playerId, add: true },
|
||||
async () => enqueue(queue, TYPES.PLAYER_SCORES, true, { playerId }),
|
||||
);
|
||||
};
|
||||
|
||||
const pause = () => {
|
||||
log.debug('Pause Dl Manager', 'DlManager');
|
||||
log.debug("Pause Dl Manager", "DlManager");
|
||||
|
||||
queue.clear();
|
||||
queue.pause();
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
log.debug('Unpause Dl Manager', 'DlManager');
|
||||
log.debug("Unpause Dl Manager", "DlManager");
|
||||
|
||||
queue.clear();
|
||||
queue.start();
|
||||
@ -213,11 +306,11 @@ export default async () => {
|
||||
|
||||
initialized = true;
|
||||
|
||||
log.info(`Download manager initialized`, 'DlManager');
|
||||
log.info(`Download manager initialized`, "DlManager");
|
||||
|
||||
return {
|
||||
start,
|
||||
pause,
|
||||
enqueuePlayer
|
||||
}
|
||||
}
|
||||
enqueuePlayer,
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {SsrError} from '../others/errors'
|
||||
import {delay} from '../utils/promise'
|
||||
import {parseRateLimitHeaders} from './utils'
|
||||
import { SsrError } from "../others/errors";
|
||||
import { delay } from "../utils/promise";
|
||||
import { parseRateLimitHeaders } from "./utils";
|
||||
|
||||
export class SsrNetworkError extends SsrError {
|
||||
constructor(message) {
|
||||
@ -20,7 +20,7 @@ export class SsrNetworkError extends SsrError {
|
||||
|
||||
export class SsrNetworkTimeoutError extends SsrNetworkError {
|
||||
constructor(timeout, message) {
|
||||
super(message && message.length ? message : `Timeout Error (${timeout}ms)`)
|
||||
super(message && message.length ? message : `Timeout Error (${timeout}ms)`);
|
||||
|
||||
this.name = "SsrNetworkTimeoutError";
|
||||
this.timeout = timeout;
|
||||
@ -29,12 +29,17 @@ export class SsrNetworkTimeoutError extends SsrNetworkError {
|
||||
|
||||
export class SsrHttpResponseError extends SsrNetworkError {
|
||||
constructor(response, ...args) {
|
||||
super(`HTTP Error Response: ${response && response.status ? response.status : 'None'} ${response && response.statusText ? response.statusText : ''}`, ...args);
|
||||
super(
|
||||
`HTTP Error Response: ${
|
||||
response && response.status ? response.status : "None"
|
||||
} ${response && response.statusText ? response.statusText : ""}`,
|
||||
...args,
|
||||
);
|
||||
|
||||
this.name = 'SsrHttpResponseError';
|
||||
this.name = "SsrHttpResponseError";
|
||||
this.response = response;
|
||||
|
||||
const {remaining, limit, resetAt} = parseRateLimitHeaders(response);
|
||||
const { remaining, limit, resetAt } = parseRateLimitHeaders(response);
|
||||
|
||||
this.remaining = remaining;
|
||||
this.limit = limit;
|
||||
@ -50,7 +55,7 @@ export class SsrHttpClientError extends SsrHttpResponseError {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.name = 'SsrHttpClientError';
|
||||
this.name = "SsrHttpClientError";
|
||||
}
|
||||
|
||||
shouldRetry() {
|
||||
@ -66,7 +71,7 @@ export class SsrHttpRateLimitError extends SsrHttpClientError {
|
||||
constructor(response, ...args) {
|
||||
super(response, ...args);
|
||||
|
||||
this.name = 'SsrHttpRateLimitError';
|
||||
this.name = "SsrHttpRateLimitError";
|
||||
}
|
||||
|
||||
shouldRetry() {
|
||||
@ -119,6 +124,6 @@ export class SsrHttpServerError extends SsrHttpResponseError {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.name = 'SsrHttpServerError';
|
||||
this.name = "SsrHttpServerError";
|
||||
}
|
||||
}
|
@ -97,12 +97,12 @@ export async function fetchUrl(url, options = {}, cors = true) {
|
||||
|
||||
export async function fetchJson(
|
||||
url,
|
||||
{ cacheTtl = null, maxAge = null, ...restOptions } = {}
|
||||
{ cacheTtl = null, maxAge = null, ...restOptions } = {},
|
||||
) {
|
||||
const options = getOptionsWithCacheKey(
|
||||
url,
|
||||
{ cacheTtl, maxAge, ...restOptions },
|
||||
"json"
|
||||
"json",
|
||||
);
|
||||
|
||||
const {
|
||||
@ -129,7 +129,7 @@ export async function fetchJson(
|
||||
body,
|
||||
},
|
||||
fetchCacheKey,
|
||||
fetchCacheTtl
|
||||
fetchCacheTtl,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -141,12 +141,12 @@ export async function fetchJson(
|
||||
|
||||
export async function fetchHtml(
|
||||
url,
|
||||
{ cacheTtl = null, maxAge = null, ...restOptions } = {}
|
||||
{ cacheTtl = null, maxAge = null, ...restOptions } = {},
|
||||
) {
|
||||
const options = getOptionsWithCacheKey(
|
||||
url,
|
||||
{ cacheTtl, maxAge, ...restOptions },
|
||||
"json"
|
||||
"json",
|
||||
);
|
||||
|
||||
const {
|
||||
@ -172,7 +172,7 @@ export async function fetchHtml(
|
||||
body: new DOMParser().parseFromString(body, "text/html"),
|
||||
},
|
||||
fetchCacheKey,
|
||||
fetchCacheTtl
|
||||
fetchCacheTtl,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -1,25 +1,75 @@
|
||||
import {default as createQueue, PRIORITY} from '../http-queue';
|
||||
import {substituteVars} from "../../../utils/format";
|
||||
import { default as createQueue, PRIORITY } from "../http-queue";
|
||||
import { substituteVars } from "../../../utils/format";
|
||||
|
||||
const ACCSABER_API_URL = 'https://api.accsaber.com';
|
||||
const CATEGORIES_URL = ACCSABER_API_URL + '/categories';
|
||||
const RANKING_URL = ACCSABER_API_URL + '/categories/${category}/standings';
|
||||
const PLAYER_SCORES_URL = ACCSABER_API_URL + '/players/${playerId}/scores';
|
||||
const PLAYER_RANK_HISTORY = ACCSABER_API_URL + '/players/${playerId}/recent-rank-history'
|
||||
const LEADERBOARD_URL = ACCSABER_API_URL + '/map-leaderboards/${leaderboardId}';
|
||||
const LEADERBOARD_INFO_URL = ACCSABER_API_URL + '/ranked-maps/${leaderboardId}';
|
||||
const ACCSABER_API_URL = "https://api.accsaber.com";
|
||||
const CATEGORIES_URL = ACCSABER_API_URL + "/categories";
|
||||
const RANKING_URL = ACCSABER_API_URL + "/categories/${category}/standings";
|
||||
const PLAYER_SCORES_URL = ACCSABER_API_URL + "/players/${playerId}/scores";
|
||||
const PLAYER_RANK_HISTORY =
|
||||
ACCSABER_API_URL + "/players/${playerId}/recent-rank-history";
|
||||
const LEADERBOARD_URL = ACCSABER_API_URL + "/map-leaderboards/${leaderboardId}";
|
||||
const LEADERBOARD_INFO_URL = ACCSABER_API_URL + "/ranked-maps/${leaderboardId}";
|
||||
|
||||
export default (options = {}) => {
|
||||
const queue = createQueue(options);
|
||||
|
||||
const {fetchJson, fetchHtml, ...queueToReturn} = queue;
|
||||
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
||||
|
||||
const categories = async (priority = PRIORITY.FG_LOW, options = {}) => fetchJson(CATEGORIES_URL, options, priority)
|
||||
const ranking = async (category = 'overall', page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(RANKING_URL, {category, page}), options, priority)
|
||||
const scores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(PLAYER_SCORES_URL, {playerId, page}), options, priority)
|
||||
const playerRankHistory = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(PLAYER_RANK_HISTORY, {playerId}), options, priority)
|
||||
const leaderboard = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(LEADERBOARD_URL, {leaderboardId, page}), options, priority)
|
||||
const leaderboardInfo = async (leaderboardId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(LEADERBOARD_INFO_URL, {leaderboardId}), options, priority)
|
||||
const categories = async (priority = PRIORITY.FG_LOW, options = {}) =>
|
||||
fetchJson(CATEGORIES_URL, options, priority);
|
||||
const ranking = async (
|
||||
category = "overall",
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(
|
||||
substituteVars(RANKING_URL, { category, page }),
|
||||
options,
|
||||
priority,
|
||||
);
|
||||
const scores = async (
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(
|
||||
substituteVars(PLAYER_SCORES_URL, { playerId, page }),
|
||||
options,
|
||||
priority,
|
||||
);
|
||||
const playerRankHistory = async (
|
||||
playerId,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(
|
||||
substituteVars(PLAYER_RANK_HISTORY, { playerId }),
|
||||
options,
|
||||
priority,
|
||||
);
|
||||
const leaderboard = async (
|
||||
leaderboardId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(
|
||||
substituteVars(LEADERBOARD_URL, { leaderboardId, page }),
|
||||
options,
|
||||
priority,
|
||||
);
|
||||
const leaderboardInfo = async (
|
||||
leaderboardId,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(
|
||||
substituteVars(LEADERBOARD_INFO_URL, { leaderboardId }),
|
||||
options,
|
||||
priority,
|
||||
);
|
||||
|
||||
return {
|
||||
categories,
|
||||
@ -29,5 +79,5 @@ export default (options = {}) => {
|
||||
leaderboard,
|
||||
leaderboardInfo,
|
||||
...queueToReturn,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,21 +1,23 @@
|
||||
import {default as createQueue, PRIORITY} from '../http-queue';
|
||||
import {substituteVars} from "../../../utils/format";
|
||||
import { default as createQueue, PRIORITY } from "../http-queue";
|
||||
import { substituteVars } from "../../../utils/format";
|
||||
|
||||
const BEATMAPS_API_URL = 'https://api.beatsaver.com/';
|
||||
const SONG_BY_HASH_URL = BEATMAPS_API_URL + '/maps/hash/${hash}';
|
||||
const SONG_BY_KEY_URL = BEATMAPS_API_URL + '/maps/id/${key}'
|
||||
const BEATMAPS_API_URL = "https://api.beatsaver.com/";
|
||||
const SONG_BY_HASH_URL = BEATMAPS_API_URL + "/maps/hash/${hash}";
|
||||
const SONG_BY_KEY_URL = BEATMAPS_API_URL + "/maps/id/${key}";
|
||||
|
||||
export default (options = {}) => {
|
||||
const queue = createQueue(options);
|
||||
|
||||
const {fetchJson, fetchHtml, ...queueToReturn} = queue;
|
||||
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
||||
|
||||
const byHash = async (hash, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SONG_BY_HASH_URL, {hash}), options, priority)
|
||||
const byKey = async (key, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SONG_BY_KEY_URL, {key}), options, priority)
|
||||
const byHash = async (hash, priority = PRIORITY.FG_LOW, options = {}) =>
|
||||
fetchJson(substituteVars(SONG_BY_HASH_URL, { hash }), options, priority);
|
||||
const byKey = async (key, priority = PRIORITY.FG_LOW, options = {}) =>
|
||||
fetchJson(substituteVars(SONG_BY_KEY_URL, { key }), options, priority);
|
||||
|
||||
return {
|
||||
byHash,
|
||||
byKey,
|
||||
...queueToReturn,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,9 +1,17 @@
|
||||
import {default as createQueue, PRIORITY as QUEUE_PRIORITY} from '../../utils/queue';
|
||||
import {SsrError, SsrTimeoutError} from '../../others/errors'
|
||||
import {SsrHttpRateLimitError, SsrHttpResponseError, SsrNetworkError, SsrNetworkTimeoutError} from '../errors'
|
||||
import {fetchHtml, fetchJson} from '../fetch';
|
||||
import makePendingPromisePool from '../../utils/pending-promises'
|
||||
import {AbortError} from '../../utils/promise'
|
||||
import {
|
||||
default as createQueue,
|
||||
PRIORITY as QUEUE_PRIORITY,
|
||||
} from "../../utils/queue";
|
||||
import { SsrError, SsrTimeoutError } from "../../others/errors";
|
||||
import {
|
||||
SsrHttpRateLimitError,
|
||||
SsrHttpResponseError,
|
||||
SsrNetworkError,
|
||||
SsrNetworkTimeoutError,
|
||||
} from "../errors";
|
||||
import { fetchHtml, fetchJson } from "../fetch";
|
||||
import makePendingPromisePool from "../../utils/pending-promises";
|
||||
import { AbortError } from "../../utils/promise";
|
||||
|
||||
const DEFAULT_RETRIES = 2;
|
||||
|
||||
@ -13,38 +21,65 @@ export const PRIORITY = {
|
||||
BG_HIGH: QUEUE_PRIORITY.NORMAL,
|
||||
BG_NORMAL: QUEUE_PRIORITY.LOW,
|
||||
BG_LOW: QUEUE_PRIORITY.LOWEST,
|
||||
}
|
||||
};
|
||||
|
||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||
|
||||
export default (options = {}) => {
|
||||
const {retries, rateLimitTick, ...queueOptions} = {retries: DEFAULT_RETRIES, rateLimitTick: 500, ...options};
|
||||
const { retries, rateLimitTick, ...queueOptions } = {
|
||||
retries: DEFAULT_RETRIES,
|
||||
rateLimitTick: 500,
|
||||
...options,
|
||||
};
|
||||
const queue = createQueue(queueOptions);
|
||||
|
||||
const {add, emitter, ...queueToReturn} = queue;
|
||||
const { add, emitter, ...queueToReturn } = queue;
|
||||
|
||||
let lastRateLimitError = null;
|
||||
let rateLimitTimerId = null;
|
||||
let currentRateLimit = {waiting: 0, remaining: null, limit: null, resetAt: null};
|
||||
let currentRateLimit = {
|
||||
waiting: 0,
|
||||
remaining: null,
|
||||
limit: null,
|
||||
resetAt: null,
|
||||
};
|
||||
|
||||
const rateLimitTicker = () => {
|
||||
const expiresInMs = lastRateLimitError && lastRateLimitError.resetAt ? lastRateLimitError.resetAt - new Date() + 1000 : 0;
|
||||
const expiresInMs =
|
||||
lastRateLimitError && lastRateLimitError.resetAt
|
||||
? lastRateLimitError.resetAt - new Date() + 1000
|
||||
: 0;
|
||||
if (expiresInMs <= 0) {
|
||||
emitter.emit('waiting', {waiting: 0, remaining: null, limit: null, resetAt: null});
|
||||
emitter.emit("waiting", {
|
||||
waiting: 0,
|
||||
remaining: null,
|
||||
limit: null,
|
||||
resetAt: null,
|
||||
});
|
||||
|
||||
if (rateLimitTimerId) clearTimeout(rateLimitTimerId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const {remaining, limit, resetAt} = lastRateLimitError;
|
||||
emitter.emit('waiting', {waiting: expiresInMs, remaining, limit, resetAt});
|
||||
const { remaining, limit, resetAt } = lastRateLimitError;
|
||||
emitter.emit("waiting", {
|
||||
waiting: expiresInMs,
|
||||
remaining,
|
||||
limit,
|
||||
resetAt,
|
||||
});
|
||||
|
||||
if (rateLimitTimerId) clearTimeout(rateLimitTimerId);
|
||||
rateLimitTimerId = setTimeout(rateLimitTicker, rateLimitTick);
|
||||
}
|
||||
};
|
||||
|
||||
const retriedFetch = async (fetchFunc, url, options, priority = PRIORITY.FG_LOW) => {
|
||||
const retriedFetch = async (
|
||||
fetchFunc,
|
||||
url,
|
||||
options,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
) => {
|
||||
for (let i = 0; i <= retries; i++) {
|
||||
try {
|
||||
return await add(async () => {
|
||||
@ -55,23 +90,22 @@ export default (options = {}) => {
|
||||
}
|
||||
|
||||
return fetchFunc(url, options)
|
||||
.then(response => {
|
||||
currentRateLimit = {...response.rateLimit, waiting: 0};
|
||||
.then((response) => {
|
||||
currentRateLimit = { ...response.rateLimit, waiting: 0 };
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch(err => {
|
||||
if (err instanceof SsrTimeoutError) throw new SsrNetworkTimeoutError(err.timeout);
|
||||
.catch((err) => {
|
||||
if (err instanceof SsrTimeoutError)
|
||||
throw new SsrNetworkTimeoutError(err.timeout);
|
||||
|
||||
throw err;
|
||||
})
|
||||
},
|
||||
priority,
|
||||
)
|
||||
});
|
||||
}, priority);
|
||||
} catch (err) {
|
||||
if (err instanceof SsrHttpResponseError) {
|
||||
const {remaining, limit, resetAt} = err;
|
||||
currentRateLimit = {waiting: 0, remaining, limit, resetAt};
|
||||
const { remaining, limit, resetAt } = err;
|
||||
currentRateLimit = { waiting: 0, remaining, limit, resetAt };
|
||||
}
|
||||
|
||||
if (err instanceof SsrNetworkError) {
|
||||
@ -79,7 +113,13 @@ export default (options = {}) => {
|
||||
if (!shouldRetry || i === retries) throw err;
|
||||
|
||||
if (err instanceof SsrHttpRateLimitError) {
|
||||
if (err.remaining <= 0 && err.resetAt && (!lastRateLimitError || !lastRateLimitError.resetAt || lastRateLimitError.resetAt < err.resetAt)) {
|
||||
if (
|
||||
err.remaining <= 0 &&
|
||||
err.resetAt &&
|
||||
(!lastRateLimitError ||
|
||||
!lastRateLimitError.resetAt ||
|
||||
lastRateLimitError.resetAt < err.resetAt)
|
||||
) {
|
||||
lastRateLimitError = err;
|
||||
|
||||
rateLimitTicker();
|
||||
@ -95,11 +135,17 @@ export default (options = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
throw new SsrError('Unknown error');
|
||||
}
|
||||
throw new SsrError("Unknown error");
|
||||
};
|
||||
|
||||
const queuedFetchJson = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchJson, url, options, priority));
|
||||
const queuedFetchHtml = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchHtml, url, options, priority));
|
||||
const queuedFetchJson = async (url, options, priority = PRIORITY.FG_LOW) =>
|
||||
resolvePromiseOrWaitForPending(url, () =>
|
||||
retriedFetch(fetchJson, url, options, priority),
|
||||
);
|
||||
const queuedFetchHtml = async (url, options, priority = PRIORITY.FG_LOW) =>
|
||||
resolvePromiseOrWaitForPending(url, () =>
|
||||
retriedFetch(fetchHtml, url, options, priority),
|
||||
);
|
||||
|
||||
const getRateLimit = () => currentRateLimit;
|
||||
|
||||
@ -108,5 +154,5 @@ export default (options = {}) => {
|
||||
fetchHtml: queuedFetchHtml,
|
||||
getRateLimit,
|
||||
...queueToReturn,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,55 +1,99 @@
|
||||
import {writable} from 'svelte/store'
|
||||
import {PRIORITY} from './http-queue'
|
||||
import createScoreSaberApiQueue from './scoresaber/api-queue'
|
||||
import createScoreSaberPageQueue from './scoresaber/page-queue'
|
||||
import createBeatMapsApiQueue from './beatmaps/api-queue'
|
||||
import createBeatSaviorApiQueue from './beatsavior/api-queue'
|
||||
import createTwitchApiQueue from './twitch/api-queue'
|
||||
import createAccSaberApiQueue from './accsaber/api-queue'
|
||||
import { writable } from "svelte/store";
|
||||
import { PRIORITY } from "./http-queue";
|
||||
import createScoreSaberApiQueue from "./scoresaber/api-queue";
|
||||
import createScoreSaberPageQueue from "./scoresaber/page-queue";
|
||||
import createBeatMapsApiQueue from "./beatmaps/api-queue";
|
||||
import createBeatSaviorApiQueue from "./beatsavior/api-queue";
|
||||
import createTwitchApiQueue from "./twitch/api-queue";
|
||||
import createAccSaberApiQueue from "./accsaber/api-queue";
|
||||
|
||||
export const getResponseBody = response => response ? response.body : null;
|
||||
export const isResponseCached = response => !!(response && response.cached)
|
||||
export const updateResponseBody = (response, body) => response ? {...response, body} : null;
|
||||
export const getResponseBody = (response) => (response ? response.body : null);
|
||||
export const isResponseCached = (response) => !!(response && response.cached);
|
||||
export const updateResponseBody = (response, body) =>
|
||||
response ? { ...response, body } : null;
|
||||
|
||||
const initQueue = queue => {
|
||||
const initQueue = (queue) => {
|
||||
let queueState = {
|
||||
size: 0,
|
||||
pending: 0,
|
||||
rateLimit: {waiting: 0, remaining: null, limit: null, resetAt: null},
|
||||
progress: {num: 0, count: 0, progress: 1},
|
||||
rateLimit: { waiting: 0, remaining: null, limit: null, resetAt: null },
|
||||
progress: { num: 0, count: 0, progress: 1 },
|
||||
};
|
||||
|
||||
const {subscribe, set} = writable(queueState);
|
||||
const { subscribe, set } = writable(queueState);
|
||||
|
||||
queue.on('change', ({size, pending}) => {
|
||||
const {rateLimit: {waiting}} = queueState;
|
||||
const {remaining, limit, resetAt} = queue.getRateLimit();
|
||||
queueState = {...queueState, size, pending, rateLimit: {waiting, remaining, limit, resetAt}};
|
||||
queue.on("change", ({ size, pending }) => {
|
||||
const {
|
||||
rateLimit: { waiting },
|
||||
} = queueState;
|
||||
const { remaining, limit, resetAt } = queue.getRateLimit();
|
||||
queueState = {
|
||||
...queueState,
|
||||
size,
|
||||
pending,
|
||||
rateLimit: { waiting, remaining, limit, resetAt },
|
||||
};
|
||||
set(queueState);
|
||||
});
|
||||
queue.on('progress', ({progress, num, count}) => {
|
||||
const {rateLimit: {waiting}} = queueState;
|
||||
const {remaining, limit, resetAt} = queue.getRateLimit();
|
||||
queueState = {...queueState, progress: {num, count, progress}, rateLimit: {waiting, remaining, limit, resetAt}}
|
||||
queue.on("progress", ({ progress, num, count }) => {
|
||||
const {
|
||||
rateLimit: { waiting },
|
||||
} = queueState;
|
||||
const { remaining, limit, resetAt } = queue.getRateLimit();
|
||||
queueState = {
|
||||
...queueState,
|
||||
progress: { num, count, progress },
|
||||
rateLimit: { waiting, remaining, limit, resetAt },
|
||||
};
|
||||
set(queueState);
|
||||
});
|
||||
queue.on('waiting', ({waiting, remaining, limit, resetAt}) => {
|
||||
queueState = {...queueState, rateLimit: {waiting, remaining, limit, resetAt}}
|
||||
queue.on("waiting", ({ waiting, remaining, limit, resetAt }) => {
|
||||
queueState = {
|
||||
...queueState,
|
||||
rateLimit: { waiting, remaining, limit, resetAt },
|
||||
};
|
||||
set(queueState);
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
...queue,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
SCORESABER_API: initQueue(createScoreSaberApiQueue({concurrency: 3, timeout: 95000})),
|
||||
SCORESABER_PAGE: initQueue(createScoreSaberPageQueue({concurrency: 3, timeout: 30000})),
|
||||
BEATMAPS: initQueue(createBeatMapsApiQueue({concurrency: 1, timeout: 10000, intervalCap: 10, interval: 1000})),
|
||||
BEATSAVIOR: initQueue(createBeatSaviorApiQueue({concurrency: 1, timeout: 10000, intervalCap: 60, interval: 60000})),
|
||||
TWITCH: initQueue(createTwitchApiQueue({concurrency: 8, timeout: 8000, intervalCap: 800, interval: 60000})),
|
||||
ACCSABER: initQueue(createAccSaberApiQueue({concurrency: 2, timeout: 10000})),
|
||||
SCORESABER_API: initQueue(
|
||||
createScoreSaberApiQueue({ concurrency: 3, timeout: 95000 }),
|
||||
),
|
||||
SCORESABER_PAGE: initQueue(
|
||||
createScoreSaberPageQueue({ concurrency: 3, timeout: 30000 }),
|
||||
),
|
||||
BEATMAPS: initQueue(
|
||||
createBeatMapsApiQueue({
|
||||
concurrency: 1,
|
||||
timeout: 10000,
|
||||
intervalCap: 10,
|
||||
interval: 1000,
|
||||
}),
|
||||
),
|
||||
BEATSAVIOR: initQueue(
|
||||
createBeatSaviorApiQueue({
|
||||
concurrency: 1,
|
||||
timeout: 10000,
|
||||
intervalCap: 60,
|
||||
interval: 60000,
|
||||
}),
|
||||
),
|
||||
TWITCH: initQueue(
|
||||
createTwitchApiQueue({
|
||||
concurrency: 8,
|
||||
timeout: 8000,
|
||||
intervalCap: 800,
|
||||
interval: 60000,
|
||||
}),
|
||||
),
|
||||
ACCSABER: initQueue(
|
||||
createAccSaberApiQueue({ concurrency: 2, timeout: 10000 }),
|
||||
),
|
||||
PRIORITY,
|
||||
}
|
||||
};
|
||||
|
@ -1,35 +1,79 @@
|
||||
import {default as createQueue, PRIORITY} from '../http-queue';
|
||||
import {substituteVars} from '../../../utils/format'
|
||||
import {PLAYER_SCORES_PER_PAGE, PLAYERS_PER_PAGE} from '../../../utils/scoresaber/consts'
|
||||
import { default as createQueue, PRIORITY } from "../http-queue";
|
||||
import { substituteVars } from "../../../utils/format";
|
||||
import {
|
||||
PLAYER_SCORES_PER_PAGE,
|
||||
PLAYERS_PER_PAGE,
|
||||
} from "../../../utils/scoresaber/consts";
|
||||
|
||||
export const SS_API_HOST = 'https://new.scoresaber.com';
|
||||
export const SS_API_HOST = "https://new.scoresaber.com";
|
||||
export const SS_API_URL = `${SS_API_HOST}/api`;
|
||||
|
||||
export const SS_API_PLAYER_INFO_URL = SS_API_URL + '/player/${playerId}/full';
|
||||
export const SS_API_RECENT_SCORES_URL = SS_API_URL + '/player/${playerId}/scores/recent/${page}';
|
||||
export const SS_API_TOP_SCORES_URL = SS_API_URL + '/player/${playerId}/scores/top/${page}';
|
||||
export const SS_API_FIND_PLAYER_URL = SS_API_URL + '/players/by-name/${query}'
|
||||
export const SS_API_RANKING_GLOBAL_URL = SS_API_URL + '/players/${page}'
|
||||
export const SS_API_RANKING_GLOBAL_PAGES_URL = SS_API_URL + '/players/pages'
|
||||
export const SS_API_PLAYER_INFO_URL = SS_API_URL + "/player/${playerId}/full";
|
||||
export const SS_API_RECENT_SCORES_URL =
|
||||
SS_API_URL + "/player/${playerId}/scores/recent/${page}";
|
||||
export const SS_API_TOP_SCORES_URL =
|
||||
SS_API_URL + "/player/${playerId}/scores/top/${page}";
|
||||
export const SS_API_FIND_PLAYER_URL = SS_API_URL + "/players/by-name/${query}";
|
||||
export const SS_API_RANKING_GLOBAL_URL = SS_API_URL + "/players/${page}";
|
||||
export const SS_API_RANKING_GLOBAL_PAGES_URL = SS_API_URL + "/players/pages";
|
||||
|
||||
export default (options = {}) => {
|
||||
const queue = createQueue(options);
|
||||
|
||||
const {fetchJson, fetchHtml, ...queueToReturn} = queue;
|
||||
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
||||
|
||||
const fetchScores = async (baseUrl, playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(baseUrl, {playerId, page}), options, priority);
|
||||
const fetchScores = async (
|
||||
baseUrl,
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(substituteVars(baseUrl, { playerId, page }), options, priority);
|
||||
|
||||
const player = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_PLAYER_INFO_URL, {playerId}), options, priority);
|
||||
const player = async (playerId, priority = PRIORITY.FG_LOW, options = {}) =>
|
||||
fetchJson(
|
||||
substituteVars(SS_API_PLAYER_INFO_URL, { playerId }),
|
||||
options,
|
||||
priority,
|
||||
);
|
||||
|
||||
const recentScores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchScores(SS_API_RECENT_SCORES_URL, playerId, page, priority, options);
|
||||
const recentScores = async (
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) => fetchScores(SS_API_RECENT_SCORES_URL, playerId, page, priority, options);
|
||||
|
||||
const topScores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchScores(SS_API_TOP_SCORES_URL, playerId, page, priority, options);
|
||||
const topScores = async (
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) => fetchScores(SS_API_TOP_SCORES_URL, playerId, page, priority, options);
|
||||
|
||||
const findPlayer = async (query, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_FIND_PLAYER_URL, {query: encodeURIComponent(query)}), options, priority);
|
||||
const findPlayer = async (query, priority = PRIORITY.FG_LOW, options = {}) =>
|
||||
fetchJson(
|
||||
substituteVars(SS_API_FIND_PLAYER_URL, {
|
||||
query: encodeURIComponent(query),
|
||||
}),
|
||||
options,
|
||||
priority,
|
||||
);
|
||||
|
||||
const rankingGlobal = async (page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_RANKING_GLOBAL_URL, {page}), options, priority);
|
||||
const rankingGlobal = async (
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(
|
||||
substituteVars(SS_API_RANKING_GLOBAL_URL, { page }),
|
||||
options,
|
||||
priority,
|
||||
);
|
||||
|
||||
const rankingGlobalPages = async (priority = PRIORITY.FG_LOW, options = {}) => fetchJson(SS_API_RANKING_GLOBAL_PAGES_URL, options, priority);
|
||||
const rankingGlobalPages = async (priority = PRIORITY.FG_LOW, options = {}) =>
|
||||
fetchJson(SS_API_RANKING_GLOBAL_PAGES_URL, options, priority);
|
||||
|
||||
return {
|
||||
player,
|
||||
@ -42,5 +86,5 @@ export default (options = {}) => {
|
||||
PLAYER_SCORES_PER_PAGE,
|
||||
PLAYERS_PER_PAGE,
|
||||
...queueToReturn,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ export const parseSsInt = (text) => {
|
||||
export const parseSsFloat = (text) =>
|
||||
text
|
||||
? parseFloat(
|
||||
getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, ""))
|
||||
getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, "")),
|
||||
)
|
||||
: null;
|
||||
|
||||
@ -78,32 +78,32 @@ export default (options = {}) => {
|
||||
const rankeds = async (
|
||||
page = 1,
|
||||
priority = PRIORITY.BG_NORMAL,
|
||||
options = {}
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(substituteVars(RANKEDS_URL, { page }), options, priority).then(
|
||||
(r) => {
|
||||
r.body = processRankeds(r.body);
|
||||
|
||||
return r;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const processPlayerProfile = (playerId, doc) => {
|
||||
cfDecryptEmail(doc);
|
||||
|
||||
let avatar = getImgUrl(
|
||||
opt(doc.querySelector(".column.avatar img"), "src", null)
|
||||
opt(doc.querySelector(".column.avatar img"), "src", null),
|
||||
);
|
||||
|
||||
let playerName = opt(
|
||||
doc.querySelector(".content .column:not(.avatar) .title a"),
|
||||
"innerText"
|
||||
"innerText",
|
||||
);
|
||||
playerName = playerName ? playerName.trim() : null;
|
||||
|
||||
let country = getFirstRegexpMatch(
|
||||
/^.*?\/flags\/([^.]+)\..*$/,
|
||||
opt(doc.querySelector(".content .column .title img"), "src")
|
||||
opt(doc.querySelector(".content .column .title img"), "src"),
|
||||
);
|
||||
country = country ? country.toUpperCase() : null;
|
||||
|
||||
@ -111,8 +111,8 @@ export default (options = {}) => {
|
||||
opt(
|
||||
doc.querySelector(".pagination .pagination-list li a.is-current"),
|
||||
"innerText",
|
||||
null
|
||||
)
|
||||
null,
|
||||
),
|
||||
);
|
||||
pageNum = !isNaN(pageNum) ? pageNum : null;
|
||||
|
||||
@ -120,8 +120,8 @@ export default (options = {}) => {
|
||||
opt(
|
||||
doc.querySelector(".pagination .pagination-list li:last-of-type"),
|
||||
"innerText",
|
||||
null
|
||||
)
|
||||
null,
|
||||
),
|
||||
);
|
||||
pageQty = !isNaN(pageQty) ? pageQty : null;
|
||||
|
||||
@ -130,31 +130,31 @@ export default (options = {}) => {
|
||||
/^\s*<strong>(?:[^:]+)\s*:?\s*<\/strong>\s*(.*)$/,
|
||||
opt(
|
||||
doc.querySelector(
|
||||
".columns .column:not(.is-narrow) ul li:nth-of-type(3)"
|
||||
".columns .column:not(.is-narrow) ul li:nth-of-type(3)",
|
||||
),
|
||||
"innerHTML",
|
||||
),
|
||||
),
|
||||
"innerHTML"
|
||||
)
|
||||
)
|
||||
);
|
||||
totalItems = !isNaN(totalItems) ? totalItems : 0;
|
||||
|
||||
let playerRank = parseSsInt(
|
||||
opt(
|
||||
doc.querySelector(
|
||||
".content .column ul li:first-of-type a:first-of-type"
|
||||
".content .column ul li:first-of-type a:first-of-type",
|
||||
),
|
||||
"innerText",
|
||||
),
|
||||
"innerText"
|
||||
)
|
||||
);
|
||||
playerRank = !isNaN(playerRank) ? playerRank : null;
|
||||
|
||||
let countryRank = parseSsInt(
|
||||
opt(
|
||||
doc.querySelector(
|
||||
'.content .column ul li:first-of-type a[href^="/global?country="]'
|
||||
'.content .column ul li:first-of-type a[href^="/global?country="]',
|
||||
),
|
||||
"innerText",
|
||||
),
|
||||
"innerText"
|
||||
)
|
||||
);
|
||||
countryRank = !isNaN(countryRank) ? countryRank : null;
|
||||
|
||||
@ -170,7 +170,7 @@ export default (options = {}) => {
|
||||
[...doc.querySelectorAll(".content .column ul li")]
|
||||
.map((li) => {
|
||||
const matches = li.innerHTML.match(
|
||||
/^\s*<strong>([^:]+)\s*:?\s*<\/strong>\s*(.*)$/
|
||||
/^\s*<strong>([^:]+)\s*:?\s*<\/strong>\s*(.*)$/,
|
||||
);
|
||||
if (!matches) return null;
|
||||
|
||||
@ -219,7 +219,7 @@ export default (options = {}) => {
|
||||
const item = mapping.find((m) => m.key === matches[1]);
|
||||
return item ? { ...item, value } : { label: matches[1], value };
|
||||
})
|
||||
.filter((s) => s)
|
||||
.filter((s) => s),
|
||||
)
|
||||
.reduce(
|
||||
(cum, item) => {
|
||||
@ -255,7 +255,7 @@ export default (options = {}) => {
|
||||
|
||||
return cum;
|
||||
},
|
||||
{ inactiveAccount: false, bannedAccount: false }
|
||||
{ inactiveAccount: false, bannedAccount: false },
|
||||
);
|
||||
|
||||
const scores = [...doc.querySelectorAll("table.ranking tbody tr")].map(
|
||||
@ -274,7 +274,7 @@ export default (options = {}) => {
|
||||
if (song) {
|
||||
const leaderboardId = parseInt(
|
||||
getFirstRegexpMatch(/leaderboard\/(\d+)/, song.href),
|
||||
10
|
||||
10,
|
||||
);
|
||||
ret.leaderboardId = leaderboardId ? leaderboardId : null;
|
||||
} else {
|
||||
@ -293,7 +293,7 @@ export default (options = {}) => {
|
||||
.replace(/&/g, "&")
|
||||
.replace(
|
||||
/<span class="__cf_email__" data-cfemail="[^"]+">\[email protected]<\/span>/g,
|
||||
""
|
||||
"",
|
||||
)
|
||||
.match(/^(.*?)\s*<span[^>]+>(.*?)<\/span>/)
|
||||
: null;
|
||||
@ -328,7 +328,7 @@ export default (options = {}) => {
|
||||
ret.timeSet = songDate ? dateFromString(songDate.title) : null;
|
||||
|
||||
const pp = parseSsFloat(
|
||||
opt(tr.querySelector("th.score .scoreTop.ppValue"), "innerText")
|
||||
opt(tr.querySelector("th.score .scoreTop.ppValue"), "innerText"),
|
||||
);
|
||||
ret.pp = !isNaN(pp) ? pp : null;
|
||||
|
||||
@ -337,9 +337,9 @@ export default (options = {}) => {
|
||||
/^\(([0-9.]+)pp\)$/,
|
||||
opt(
|
||||
tr.querySelector("th.score .scoreTop.ppWeightedValue"),
|
||||
"innerText"
|
||||
)
|
||||
)
|
||||
"innerText",
|
||||
),
|
||||
),
|
||||
);
|
||||
ret.ppWeighted = !isNaN(ppWeighted) ? ppWeighted : null;
|
||||
|
||||
@ -380,7 +380,7 @@ export default (options = {}) => {
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
);
|
||||
const recentPlay =
|
||||
scores && scores.length && scores[0].timeSet ? scores[0].timeSet : null;
|
||||
@ -394,18 +394,18 @@ export default (options = {}) => {
|
||||
externalProfileUrl: opt(
|
||||
doc.querySelector(".content .column:not(.avatar) .title a"),
|
||||
"href",
|
||||
null
|
||||
null,
|
||||
),
|
||||
history: getFirstRegexpMatch(
|
||||
/data:\s*\[([0-9,]+)\]/,
|
||||
doc.body.innerHTML
|
||||
doc.body.innerHTML,
|
||||
),
|
||||
country,
|
||||
badges: [...doc.querySelectorAll(".column.avatar center img")].map(
|
||||
(img) => ({
|
||||
image: getImgUrl(img.src),
|
||||
description: img.title,
|
||||
})
|
||||
}),
|
||||
),
|
||||
rank: stats.rank ? stats.rank : null,
|
||||
countryRank: stats.countryRank ? stats.countryRank : null,
|
||||
@ -435,7 +435,7 @@ export default (options = {}) => {
|
||||
fetchHtml(
|
||||
substituteVars(PLAYER_PROFILE_URL, { playerId }),
|
||||
options,
|
||||
priority
|
||||
priority,
|
||||
).then((r) => {
|
||||
r.body = processPlayerProfile(playerId, r.body);
|
||||
|
||||
@ -451,17 +451,17 @@ export default (options = {}) => {
|
||||
const id = getFirstRegexpMatch(/\/(\d+)$/, a.href);
|
||||
|
||||
const avatar = getImgUrl(
|
||||
opt(tr.querySelector("td.picture img"), "src", null)
|
||||
opt(tr.querySelector("td.picture img"), "src", null),
|
||||
);
|
||||
|
||||
let country = getFirstRegexpMatch(
|
||||
/^.*?\/flags\/([^.]+)\..*$/,
|
||||
opt(tr.querySelector("td.player img"), "src", null)
|
||||
opt(tr.querySelector("td.player img"), "src", null),
|
||||
);
|
||||
country = country ? country.toUpperCase() : null;
|
||||
|
||||
let difference = parseSsInt(
|
||||
opt(tr.querySelector("td.diff"), "innerText", null)
|
||||
opt(tr.querySelector("td.diff"), "innerText", null),
|
||||
);
|
||||
difference = !isNaN(difference) ? difference : null;
|
||||
|
||||
@ -469,15 +469,15 @@ export default (options = {}) => {
|
||||
playerName = playerName || playerName === "" ? playerName.trim() : null;
|
||||
|
||||
let pp = parseSsFloat(
|
||||
opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText")
|
||||
opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText"),
|
||||
);
|
||||
pp = !isNaN(pp) ? pp : null;
|
||||
|
||||
let rank = parseSsInt(
|
||||
getFirstRegexpMatch(
|
||||
/^\s*#(\d+)\s*$/,
|
||||
opt(tr.querySelector("td.rank"), "innerText", null)
|
||||
)
|
||||
opt(tr.querySelector("td.rank"), "innerText", null),
|
||||
),
|
||||
);
|
||||
rank = !isNaN(rank) ? rank : null;
|
||||
|
||||
@ -491,7 +491,7 @@ export default (options = {}) => {
|
||||
pp,
|
||||
rank,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return { players: data };
|
||||
@ -501,12 +501,12 @@ export default (options = {}) => {
|
||||
country,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {}
|
||||
options = {},
|
||||
) =>
|
||||
fetchHtml(
|
||||
substituteVars(COUNTRY_RANKING_URL, { country, page }),
|
||||
options,
|
||||
priority
|
||||
priority,
|
||||
).then((r) => {
|
||||
r.body = processCountryRanking(country, r.body);
|
||||
|
||||
@ -529,11 +529,11 @@ export default (options = {}) => {
|
||||
};
|
||||
|
||||
ret.player.playerInfo.avatar = getImgUrl(
|
||||
opt(tr.querySelector(".picture img"), "src", null)
|
||||
opt(tr.querySelector(".picture img"), "src", null),
|
||||
);
|
||||
|
||||
ret.score.rank = parseSsInt(
|
||||
opt(tr.querySelector("td.rank"), "innerText")
|
||||
opt(tr.querySelector("td.rank"), "innerText"),
|
||||
);
|
||||
if (isNaN(ret.score.rank)) ret.score.rank = null;
|
||||
|
||||
@ -541,7 +541,7 @@ export default (options = {}) => {
|
||||
if (player) {
|
||||
let country = getFirstRegexpMatch(
|
||||
/^.*?\/flags\/([^.]+)\..*$/,
|
||||
opt(player.querySelector("img"), "src", "")
|
||||
opt(player.querySelector("img"), "src", ""),
|
||||
);
|
||||
country = country ? country.toUpperCase() : null;
|
||||
if (country) {
|
||||
@ -551,14 +551,14 @@ export default (options = {}) => {
|
||||
|
||||
ret.player.name = opt(
|
||||
player.querySelector("span.songTop.pp"),
|
||||
"innerText"
|
||||
"innerText",
|
||||
);
|
||||
ret.player.name = ret.player.name
|
||||
? ret.player.name.trim().replace("'", "'")
|
||||
: null;
|
||||
ret.player.playerId = getFirstRegexpMatch(
|
||||
/\/u\/(\d+)((\?|&|#).*)?$/,
|
||||
opt(player, "href", "")
|
||||
opt(player, "href", ""),
|
||||
);
|
||||
ret.player.playerId = ret.player.playerId
|
||||
? ret.player.playerId.trim()
|
||||
@ -574,7 +574,7 @@ export default (options = {}) => {
|
||||
ret.score.timeSetString = opt(
|
||||
tr.querySelector("td.timeset"),
|
||||
"innerText",
|
||||
null
|
||||
null,
|
||||
);
|
||||
if (ret.score.timeSetString)
|
||||
ret.score.timeSetString = ret.score.timeSetString.trim();
|
||||
@ -602,7 +602,7 @@ export default (options = {}) => {
|
||||
const diffs = [...doc.querySelectorAll(".tabs ul li a")].map((a) => {
|
||||
let leaderboardId = parseInt(
|
||||
getFirstRegexpMatch(/leaderboard\/(\d+)$/, a.href),
|
||||
10
|
||||
10,
|
||||
);
|
||||
if (isNaN(leaderboardId)) leaderboardId = null;
|
||||
|
||||
@ -615,7 +615,7 @@ export default (options = {}) => {
|
||||
const currentDiffHuman = opt(
|
||||
doc.querySelector(".tabs li.is-active a span"),
|
||||
"innerText",
|
||||
null
|
||||
null,
|
||||
);
|
||||
|
||||
let diff = null;
|
||||
@ -628,20 +628,20 @@ export default (options = {}) => {
|
||||
|
||||
const songName = opt(
|
||||
doc.querySelector(
|
||||
".column.is-one-third-desktop .box:first-of-type .title a"
|
||||
".column.is-one-third-desktop .box:first-of-type .title a",
|
||||
),
|
||||
"innerText",
|
||||
null
|
||||
null,
|
||||
);
|
||||
|
||||
const imageUrl = getImgUrl(
|
||||
opt(
|
||||
doc.querySelector(
|
||||
".column.is-one-third-desktop .box:first-of-type .columns .column.is-one-quarter img"
|
||||
".column.is-one-third-desktop .box:first-of-type .columns .column.is-one-quarter img",
|
||||
),
|
||||
"src",
|
||||
null
|
||||
)
|
||||
null,
|
||||
),
|
||||
);
|
||||
|
||||
const songInfo = [
|
||||
@ -656,13 +656,13 @@ export default (options = {}) => {
|
||||
]
|
||||
.map((sid) => {
|
||||
let songInfoBox = doc.querySelector(
|
||||
".column.is-one-third-desktop .box:first-of-type"
|
||||
".column.is-one-third-desktop .box:first-of-type",
|
||||
);
|
||||
return {
|
||||
...sid,
|
||||
value: songInfoBox
|
||||
? songInfoBox.innerHTML.match(
|
||||
new RegExp(sid.label + ":\\s*<b>(.*?)</b>", "i")
|
||||
new RegExp(sid.label + ":\\s*<b>(.*?)</b>", "i"),
|
||||
)
|
||||
: null,
|
||||
};
|
||||
@ -708,7 +708,7 @@ export default (options = {}) => {
|
||||
|
||||
return cum;
|
||||
},
|
||||
{ imageUrl, stats: {} }
|
||||
{ imageUrl, stats: {} },
|
||||
);
|
||||
|
||||
const { stats, ...song } = songInfo;
|
||||
@ -718,9 +718,9 @@ export default (options = {}) => {
|
||||
opt(
|
||||
doc.querySelector(".pagination .pagination-list li:last-of-type"),
|
||||
"innerText",
|
||||
null
|
||||
null,
|
||||
),
|
||||
10
|
||||
10,
|
||||
);
|
||||
if (isNaN(pageQty)) pageQty = null;
|
||||
|
||||
@ -736,7 +736,7 @@ export default (options = {}) => {
|
||||
|
||||
let diffChartText = getFirstRegexpMatch(
|
||||
/'difficulty',\s*([0-9.,\s]+)\s*\]/,
|
||||
doc.body.innerHTML
|
||||
doc.body.innerHTML,
|
||||
);
|
||||
let diffChart = (diffChartText ? diffChartText : "")
|
||||
.split(",")
|
||||
@ -758,12 +758,12 @@ export default (options = {}) => {
|
||||
leaderboardId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {}
|
||||
options = {},
|
||||
) =>
|
||||
fetchHtml(
|
||||
substituteVars(LEADERBOARD_URL, { leaderboardId, page }),
|
||||
options,
|
||||
priority
|
||||
priority,
|
||||
).then((r) => {
|
||||
r.body = processLeaderboard(leaderboardId, page, r.body);
|
||||
|
||||
|
@ -1,44 +1,104 @@
|
||||
import {default as createQueue, PRIORITY} from '../http-queue';
|
||||
import ssrConfig from '../../../ssr-config'
|
||||
import {substituteVars} from "../../../utils/format";
|
||||
import { default as createQueue, PRIORITY } from "../http-queue";
|
||||
import ssrConfig from "../../../ssr-config";
|
||||
import { substituteVars } from "../../../utils/format";
|
||||
|
||||
const CLIENT_ID = 'u0swxz56n4iumc634at1osoqdk31qt';
|
||||
const CLIENT_ID = "u0swxz56n4iumc634at1osoqdk31qt";
|
||||
|
||||
const TWITCH_AUTH_URL = 'https://id.twitch.tv/oauth2'
|
||||
const AUTHORIZATION_URL = `${TWITCH_AUTH_URL}/authorize?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(ssrConfig.domain + '/twitch')}&response_type=token` + '&scope=${scopes}&state=${state}';
|
||||
const VALIDATE_URL = `${TWITCH_AUTH_URL}/validate`
|
||||
const TWITCH_AUTH_URL = "https://id.twitch.tv/oauth2";
|
||||
const AUTHORIZATION_URL =
|
||||
`${TWITCH_AUTH_URL}/authorize?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
|
||||
ssrConfig.domain + "/twitch",
|
||||
)}&response_type=token` + "&scope=${scopes}&state=${state}";
|
||||
const VALIDATE_URL = `${TWITCH_AUTH_URL}/validate`;
|
||||
|
||||
const TWITCH_API_URL = 'https://api.twitch.tv/helix';
|
||||
const PROFILE_URL = TWITCH_API_URL + '/users?login=${login}';
|
||||
const VIDEOS_URL = TWITCH_API_URL + '/videos?user_id=${userId}&type=${type}&first=100';
|
||||
const STREAMS_URL = TWITCH_API_URL + '/streams?user_id=${userId}';
|
||||
const TWITCH_API_URL = "https://api.twitch.tv/helix";
|
||||
const PROFILE_URL = TWITCH_API_URL + "/users?login=${login}";
|
||||
const VIDEOS_URL =
|
||||
TWITCH_API_URL + "/videos?user_id=${userId}&type=${type}&first=100";
|
||||
const STREAMS_URL = TWITCH_API_URL + "/streams?user_id=${userId}";
|
||||
|
||||
export default (options = {}) => {
|
||||
const queue = createQueue(options);
|
||||
|
||||
const {fetchJson, fetchHtml, ...queueToReturn} = queue;
|
||||
const { fetchJson, fetchHtml, ...queueToReturn } = queue;
|
||||
|
||||
const fetchApi = (url, accessToken, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(
|
||||
const fetchApi = (
|
||||
url,
|
||||
accessToken,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(
|
||||
url,
|
||||
{
|
||||
...options,
|
||||
headers: {
|
||||
'Client-ID': CLIENT_ID,
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
"Client-ID": CLIENT_ID,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
priority,
|
||||
)
|
||||
);
|
||||
|
||||
const getAuthUrl = (state = '', scopes = '') => substituteVars(AUTHORIZATION_URL, {state: encodeURIComponent(state), scopes: encodeURIComponent(scopes)});
|
||||
const getAuthUrl = (state = "", scopes = "") =>
|
||||
substituteVars(AUTHORIZATION_URL, {
|
||||
state: encodeURIComponent(state),
|
||||
scopes: encodeURIComponent(scopes),
|
||||
});
|
||||
|
||||
const validateToken = async (accessToken, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(VALIDATE_URL, {...options, headers: {'Authorization': `OAuth ${accessToken}`}}, priority)
|
||||
const validateToken = async (
|
||||
accessToken,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchJson(
|
||||
VALIDATE_URL,
|
||||
{ ...options, headers: { Authorization: `OAuth ${accessToken}` } },
|
||||
priority,
|
||||
);
|
||||
|
||||
const profile = async (accessToken, login, priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(PROFILE_URL, {login: encodeURIComponent(login)}), accessToken, priority, options)
|
||||
const profile = async (
|
||||
accessToken,
|
||||
login,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchApi(
|
||||
substituteVars(PROFILE_URL, { login: encodeURIComponent(login) }),
|
||||
accessToken,
|
||||
priority,
|
||||
options,
|
||||
);
|
||||
|
||||
const videos = async (accessToken, userId, type = 'archive', priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(VIDEOS_URL, {userId: encodeURIComponent(userId), type: encodeURIComponent(type)}), accessToken, priority, options)
|
||||
const videos = async (
|
||||
accessToken,
|
||||
userId,
|
||||
type = "archive",
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchApi(
|
||||
substituteVars(VIDEOS_URL, {
|
||||
userId: encodeURIComponent(userId),
|
||||
type: encodeURIComponent(type),
|
||||
}),
|
||||
accessToken,
|
||||
priority,
|
||||
options,
|
||||
);
|
||||
|
||||
const streams = async (accessToken, userId, priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(STREAMS_URL, {userId: encodeURIComponent(userId)}), accessToken, priority, options)
|
||||
const streams = async (
|
||||
accessToken,
|
||||
userId,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
options = {},
|
||||
) =>
|
||||
fetchApi(
|
||||
substituteVars(STREAMS_URL, { userId: encodeURIComponent(userId) }),
|
||||
accessToken,
|
||||
priority,
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
getAuthUrl,
|
||||
@ -47,5 +107,5 @@ export default (options = {}) => {
|
||||
videos,
|
||||
streams,
|
||||
...queueToReturn,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,13 +1,13 @@
|
||||
export const parseRateLimitHeaders = response => {
|
||||
export const parseRateLimitHeaders = (response) => {
|
||||
if (!response || !response.headers) return null;
|
||||
|
||||
const remaining = parseInt(response.headers.get('x-ratelimit-remaining'), 10);
|
||||
const limit = parseInt(response.headers.get('x-ratelimit-limit'), 10);
|
||||
const resetAt = parseInt(response.headers.get('x-ratelimit-reset'), 10);
|
||||
const remaining = parseInt(response.headers.get("x-ratelimit-remaining"), 10);
|
||||
const limit = parseInt(response.headers.get("x-ratelimit-limit"), 10);
|
||||
const resetAt = parseInt(response.headers.get("x-ratelimit-reset"), 10);
|
||||
|
||||
return {
|
||||
remaining: !isNaN(remaining) ? remaining : null,
|
||||
limit: !isNaN(limit) ? limit : null,
|
||||
resetAt: !isNaN(resetAt) ? new Date(resetAt * 1000) : null,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ export class SsrError extends Error {
|
||||
|
||||
export class SsrTimeoutError extends SsrError {
|
||||
constructor(timeout, message) {
|
||||
super(message && message.length ? message : `Timeout Error (${timeout}ms)`)
|
||||
super(message && message.length ? message : `Timeout Error (${timeout}ms)`);
|
||||
|
||||
this.name = "SsrTimeoutError";
|
||||
this.timeout = timeout;
|
||||
@ -21,7 +21,7 @@ export class SsrTimeoutError extends SsrError {
|
||||
|
||||
export class SsrDataFormatError extends SsrError {
|
||||
constructor(message, previous = null) {
|
||||
super(message && message.length ? message : `Data format error`)
|
||||
super(message && message.length ? message : `Data format error`);
|
||||
|
||||
this.name = "SsrDataFormatError";
|
||||
this.previous = previous;
|
||||
|
@ -1,35 +1,36 @@
|
||||
import {db} from '../db/db'
|
||||
import queues from '../network/queues/queues';
|
||||
import accSaberCategoriesApiClient from '../network/clients/accsaber/api-categories';
|
||||
import accSaberRankingApiClient from '../network/clients/accsaber/api-ranking';
|
||||
import accSaberScoresApiClient from '../network/clients/accsaber/api-scores';
|
||||
import accSaberPlayerRankHistoryApiClient from '../network/clients/accsaber/api-player-rank-history';
|
||||
import accSaberCategoriesRepository from '../db/repository/accsaber-categories'
|
||||
import accSaberPlayersRepository from '../db/repository/accsaber-players'
|
||||
import accSaberPlayersHistoryRepository from '../db/repository/accsaber-players-history';
|
||||
import keyValueRepository from '../db/repository/key-value'
|
||||
import createPlayerService from '../services/scoresaber/player';
|
||||
import {capitalize, convertArrayToObjectByKey} from '../utils/js'
|
||||
import log from '../utils/logger'
|
||||
import { db } from "../db/db";
|
||||
import queues from "../network/queues/queues";
|
||||
import accSaberCategoriesApiClient from "../network/clients/accsaber/api-categories";
|
||||
import accSaberRankingApiClient from "../network/clients/accsaber/api-ranking";
|
||||
import accSaberScoresApiClient from "../network/clients/accsaber/api-scores";
|
||||
import accSaberPlayerRankHistoryApiClient from "../network/clients/accsaber/api-player-rank-history";
|
||||
import accSaberCategoriesRepository from "../db/repository/accsaber-categories";
|
||||
import accSaberPlayersRepository from "../db/repository/accsaber-players";
|
||||
import accSaberPlayersHistoryRepository from "../db/repository/accsaber-players-history";
|
||||
import keyValueRepository from "../db/repository/key-value";
|
||||
import createPlayerService from "../services/scoresaber/player";
|
||||
import { capitalize, convertArrayToObjectByKey } from "../utils/js";
|
||||
import log from "../utils/logger";
|
||||
import {
|
||||
addToDate,
|
||||
toAccSaberMidnight,
|
||||
formatDate,
|
||||
HOUR,
|
||||
MINUTE,
|
||||
dateFromString, truncateDate,
|
||||
} from '../utils/date'
|
||||
import {PRIORITY} from '../network/queues/http-queue'
|
||||
import makePendingPromisePool from '../utils/pending-promises'
|
||||
import {getServicePlayerGain, serviceFilterFunc} from './utils'
|
||||
import {PLAYER_SCORES_PER_PAGE} from '../utils/accsaber/consts'
|
||||
import {roundToPrecision} from '../utils/format'
|
||||
dateFromString,
|
||||
truncateDate,
|
||||
} from "../utils/date";
|
||||
import { PRIORITY } from "../network/queues/http-queue";
|
||||
import makePendingPromisePool from "../utils/pending-promises";
|
||||
import { getServicePlayerGain, serviceFilterFunc } from "./utils";
|
||||
import { PLAYER_SCORES_PER_PAGE } from "../utils/accsaber/consts";
|
||||
import { roundToPrecision } from "../utils/format";
|
||||
|
||||
const REFRESH_INTERVAL = HOUR;
|
||||
const SCORES_NETWORK_TTL = MINUTE * 5;
|
||||
const HISTOGRAM_AP_PRECISION = 5;
|
||||
|
||||
const CATEGORIES_ORDER = ['overall', 'true', 'standard', 'tech'];
|
||||
const CATEGORIES_ORDER = ["overall", "true", "standard", "tech"];
|
||||
|
||||
let service = null;
|
||||
export default () => {
|
||||
@ -40,61 +41,120 @@ export default () => {
|
||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||
|
||||
const getCategories = async () => {
|
||||
const categories = await resolvePromiseOrWaitForPending(`accSaberCategories`, () => accSaberCategoriesRepository().getAll());
|
||||
const categories = await resolvePromiseOrWaitForPending(
|
||||
`accSaberCategories`,
|
||||
() => accSaberCategoriesRepository().getAll(),
|
||||
);
|
||||
|
||||
const getIdx = category => {
|
||||
const idx = CATEGORIES_ORDER.findIndex(v => v === category?.name);
|
||||
const getIdx = (category) => {
|
||||
const idx = CATEGORIES_ORDER.findIndex((v) => v === category?.name);
|
||||
|
||||
return idx >= 0 ? idx : 100000;
|
||||
}
|
||||
return categories.sort((a,b) => getIdx(a) - getIdx(b));
|
||||
}
|
||||
};
|
||||
return categories.sort((a, b) => getIdx(a) - getIdx(b));
|
||||
};
|
||||
|
||||
const getPlayer = async playerId => resolvePromiseOrWaitForPending(`accSaberPlayer/${playerId}`, () => accSaberPlayersRepository().getAllFromIndex('accsaber-players-playerId', playerId));
|
||||
const getRanking = async (category = 'overall') => accSaberPlayersRepository().getAllFromIndex('accsaber-players-category', category);
|
||||
const getPlayerHistory = async playerId => resolvePromiseOrWaitForPending(`accSaberPlayerHistory/${playerId}`, () => accSaberPlayersHistoryRepository().getAllFromIndex('accsaber-players-history-playerId', playerId))
|
||||
const getPlayer = async (playerId) =>
|
||||
resolvePromiseOrWaitForPending(`accSaberPlayer/${playerId}`, () =>
|
||||
accSaberPlayersRepository().getAllFromIndex(
|
||||
"accsaber-players-playerId",
|
||||
playerId,
|
||||
),
|
||||
);
|
||||
const getRanking = async (category = "overall") =>
|
||||
accSaberPlayersRepository().getAllFromIndex(
|
||||
"accsaber-players-category",
|
||||
category,
|
||||
);
|
||||
const getPlayerHistory = async (playerId) =>
|
||||
resolvePromiseOrWaitForPending(`accSaberPlayerHistory/${playerId}`, () =>
|
||||
accSaberPlayersHistoryRepository().getAllFromIndex(
|
||||
"accsaber-players-history-playerId",
|
||||
playerId,
|
||||
),
|
||||
);
|
||||
|
||||
const isDataForPlayerAvailable = async playerId => (await Promise.all([getPlayer(playerId), getCategories()])).every(d => d?.length)
|
||||
const isDataForPlayerAvailable = async (playerId) =>
|
||||
(await Promise.all([getPlayer(playerId), getCategories()])).every(
|
||||
(d) => d?.length,
|
||||
);
|
||||
|
||||
const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) => getServicePlayerGain(playerHistory, toAccSaberMidnight, 'accSaberDate', daysAgo, maxDaysAgo);
|
||||
const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) =>
|
||||
getServicePlayerGain(
|
||||
playerHistory,
|
||||
toAccSaberMidnight,
|
||||
"accSaberDate",
|
||||
daysAgo,
|
||||
maxDaysAgo,
|
||||
);
|
||||
|
||||
const getLastUpdatedKey = type => `accSaber${capitalize(type)}LastUpdated`;
|
||||
const getLastUpdated = async (type = 'all') => keyValueRepository().get(getLastUpdatedKey(type));
|
||||
const setLastUpdated = async (type = 'all', date) => keyValueRepository().set(date, getLastUpdatedKey(type));
|
||||
const getLastUpdatedKey = (type) => `accSaber${capitalize(type)}LastUpdated`;
|
||||
const getLastUpdated = async (type = "all") =>
|
||||
keyValueRepository().get(getLastUpdatedKey(type));
|
||||
const setLastUpdated = async (type = "all", date) =>
|
||||
keyValueRepository().set(date, getLastUpdatedKey(type));
|
||||
|
||||
const shouldRefresh = async (type = 'all', forceUpdate = false) => {
|
||||
const shouldRefresh = async (type = "all", forceUpdate = false) => {
|
||||
if (!forceUpdate) {
|
||||
const lastUpdated = await getLastUpdated(type);
|
||||
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
|
||||
log.debug(`Refresh interval for ${type} not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'AccSaberService')
|
||||
log.debug(
|
||||
`Refresh interval for ${type} not yet expired, skipping. Next refresh on ${formatDate(
|
||||
addToDate(REFRESH_INTERVAL, lastUpdated),
|
||||
)}`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchScoresPage = async (playerId, page = 1, priority = PRIORITY.FG_LOW, {...options} = {}) => {
|
||||
const fetchScoresPage = async (
|
||||
playerId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
{ ...options } = {},
|
||||
) => {
|
||||
if (!options) options = {};
|
||||
if (!options.hasOwnProperty('cacheTtl')) options.cacheTtl = SCORES_NETWORK_TTL;
|
||||
if (!options.hasOwnProperty("cacheTtl"))
|
||||
options.cacheTtl = SCORES_NETWORK_TTL;
|
||||
|
||||
const categoriesByDisplayName = convertArrayToObjectByKey(await getCategories(), 'displayName');
|
||||
const categoriesByDisplayName = convertArrayToObjectByKey(
|
||||
await getCategories(),
|
||||
"displayName",
|
||||
);
|
||||
|
||||
return (await resolvePromiseOrWaitForPending(`fetchPlayerScores/${playerId}/${page}`, () => accSaberScoresApiClient.getProcessed({...options, playerId, page, priority})))
|
||||
.map(s => ({
|
||||
return (
|
||||
await resolvePromiseOrWaitForPending(
|
||||
`fetchPlayerScores/${playerId}/${page}`,
|
||||
() =>
|
||||
accSaberScoresApiClient.getProcessed({
|
||||
...options,
|
||||
playerId,
|
||||
page,
|
||||
priority,
|
||||
}),
|
||||
)
|
||||
).map((s) => ({
|
||||
...s,
|
||||
leaderboard: {
|
||||
...s?.leaderboard,
|
||||
category: categoriesByDisplayName[s?.leaderboard?.categoryDisplayName]?.name ?? null,
|
||||
}
|
||||
}))
|
||||
}
|
||||
category:
|
||||
categoriesByDisplayName[s?.leaderboard?.categoryDisplayName]?.name ??
|
||||
null,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const getScoresHistogramDefinition = (serviceParams = {type: 'overall', sort: 'ap', order: 'desc'}) => {
|
||||
const scoreType = serviceParams?.type ?? 'overall';
|
||||
const sort = serviceParams?.sort ?? 'ap';
|
||||
const order = serviceParams?.order ?? 'desc';
|
||||
const getScoresHistogramDefinition = (
|
||||
serviceParams = { type: "overall", sort: "ap", order: "desc" },
|
||||
) => {
|
||||
const scoreType = serviceParams?.type ?? "overall";
|
||||
const sort = serviceParams?.sort ?? "ap";
|
||||
const order = serviceParams?.order ?? "desc";
|
||||
|
||||
const commonFilterFunc = serviceFilterFunc(serviceParams);
|
||||
|
||||
@ -104,68 +164,75 @@ export default () => {
|
||||
let maxBucketSize = null;
|
||||
let bucketSizeStep = null;
|
||||
let bucketSizeValues = null;
|
||||
let type = 'linear';
|
||||
let valFunc = s => s;
|
||||
let filterFunc = s => commonFilterFunc(s) && (scoreType === 'overall' || s?.leaderboard?.category === scoreType);
|
||||
let histogramFilterFunc = s => s;
|
||||
let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear'
|
||||
let type = "linear";
|
||||
let valFunc = (s) => s;
|
||||
let filterFunc = (s) =>
|
||||
commonFilterFunc(s) &&
|
||||
(scoreType === "overall" || s?.leaderboard?.category === scoreType);
|
||||
let histogramFilterFunc = (s) => s;
|
||||
let roundedValFunc = (s, type = type, precision = bucketSize) =>
|
||||
type === "linear"
|
||||
? roundToPrecision(valFunc(s), precision)
|
||||
: truncateDate(valFunc(s), precision);
|
||||
let prefix = '';
|
||||
let prefixLong = '';
|
||||
let suffix = '';
|
||||
let suffixLong = '';
|
||||
let prefix = "";
|
||||
let prefixLong = "";
|
||||
let suffix = "";
|
||||
let suffixLong = "";
|
||||
|
||||
switch(sort) {
|
||||
case 'ap':
|
||||
valFunc = s => s?.ap;
|
||||
type = 'linear';
|
||||
switch (sort) {
|
||||
case "ap":
|
||||
valFunc = (s) => s?.ap;
|
||||
type = "linear";
|
||||
bucketSize = HISTOGRAM_AP_PRECISION;
|
||||
minBucketSize = 1;
|
||||
maxBucketSize = 100;
|
||||
bucketSizeStep = 1;
|
||||
round = 0;
|
||||
suffix = ' AP';
|
||||
suffixLong = ' AP';
|
||||
suffix = " AP";
|
||||
suffixLong = " AP";
|
||||
break;
|
||||
|
||||
case 'recent':
|
||||
valFunc = s => s?.timeSet;
|
||||
type = 'time';
|
||||
bucketSize = 'day'
|
||||
case "recent":
|
||||
valFunc = (s) => s?.timeSet;
|
||||
type = "time";
|
||||
bucketSize = "day";
|
||||
break;
|
||||
|
||||
case 'acc':
|
||||
valFunc = s => s?.acc;
|
||||
type = 'linear';
|
||||
case "acc":
|
||||
valFunc = (s) => s?.acc;
|
||||
type = "linear";
|
||||
bucketSize = 0.05;
|
||||
minBucketSize = 0.05;
|
||||
maxBucketSize = 1;
|
||||
bucketSizeStep = 0.05;
|
||||
round = 2;
|
||||
suffix = '%';
|
||||
suffixLong = '%';
|
||||
suffix = "%";
|
||||
suffixLong = "%";
|
||||
break;
|
||||
|
||||
case 'rank':
|
||||
valFunc = s => s?.score?.rank;
|
||||
type = 'linear';
|
||||
case "rank":
|
||||
valFunc = (s) => s?.score?.rank;
|
||||
type = "linear";
|
||||
bucketSize = 5;
|
||||
minBucketSize = 1;
|
||||
maxBucketSize = 100;
|
||||
bucketSizeStep = 1;
|
||||
round = 0;
|
||||
prefix = '';
|
||||
prefixLong = '#';
|
||||
prefix = "";
|
||||
prefixLong = "#";
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
getValue: valFunc,
|
||||
getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize),
|
||||
getRoundedValue:
|
||||
(bucketSize = bucketSize) =>
|
||||
(s) =>
|
||||
roundedValFunc(s, type, bucketSize),
|
||||
filter: filterFunc,
|
||||
histogramFilter: histogramFilterFunc,
|
||||
sort: (a, b) => order === 'asc' ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a),
|
||||
sort: (a, b) =>
|
||||
order === "asc" ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a),
|
||||
type,
|
||||
bucketSize,
|
||||
minBucketSize,
|
||||
@ -177,164 +244,224 @@ export default () => {
|
||||
prefixLong,
|
||||
suffix,
|
||||
suffixLong,
|
||||
order
|
||||
}
|
||||
}
|
||||
order,
|
||||
};
|
||||
};
|
||||
|
||||
const getPlayerScores = async playerId => {
|
||||
const getPlayerScores = async (playerId) => {
|
||||
try {
|
||||
return fetchScoresPage(playerId, 1);
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getPlayerScoresPage = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}) => {
|
||||
const getPlayerScoresPage = async (
|
||||
playerId,
|
||||
serviceParams = { sort: "recent", order: "desc", page: 1 },
|
||||
) => {
|
||||
let page = serviceParams?.page ?? 1;
|
||||
if (page < 1) page = 1;
|
||||
|
||||
let playerScores;
|
||||
try {
|
||||
playerScores = await fetchScoresPage(playerId, page);
|
||||
}
|
||||
catch (err) {
|
||||
return {total: 0, scores: []};
|
||||
} catch (err) {
|
||||
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;
|
||||
if (playerScores.length < startIdx + 1) return {total: 0, scores: []};
|
||||
if (playerScores.length < startIdx + 1) return { total: 0, scores: [] };
|
||||
|
||||
return {
|
||||
total: playerScores.length,
|
||||
itemsPerPage: PLAYER_SCORES_PER_PAGE,
|
||||
scores: playerScores
|
||||
.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE)
|
||||
}
|
||||
}
|
||||
scores: playerScores.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE),
|
||||
};
|
||||
};
|
||||
|
||||
const fetchPlayerRankHistory = async (playerId, priority = PRIORITY.FG_LOW, {...options} = {}) => {
|
||||
const fetchPlayerRankHistory = async (
|
||||
playerId,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
{ ...options } = {},
|
||||
) => {
|
||||
if (!options) options = {};
|
||||
if (!options.hasOwnProperty('cacheTtl')) options.cacheTtl = SCORES_NETWORK_TTL;
|
||||
if (!options.hasOwnProperty("cacheTtl"))
|
||||
options.cacheTtl = SCORES_NETWORK_TTL;
|
||||
|
||||
return accSaberPlayerRankHistoryApiClient.getProcessed({...options, playerId, priority});
|
||||
}
|
||||
return accSaberPlayerRankHistoryApiClient.getProcessed({
|
||||
...options,
|
||||
playerId,
|
||||
priority,
|
||||
});
|
||||
};
|
||||
|
||||
const refreshCategories = async (forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
|
||||
log.debug(`Starting AccSaber categories refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
|
||||
const refreshCategories = async (
|
||||
forceUpdate = false,
|
||||
priority = queues.PRIORITY.BG_NORMAL,
|
||||
throwErrors = false,
|
||||
) => {
|
||||
log.debug(
|
||||
`Starting AccSaber categories refreshing${
|
||||
forceUpdate ? " (forced)" : ""
|
||||
}...`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
try {
|
||||
log.trace(`Fetching categories from DB...`, 'AccSaberService');
|
||||
log.trace(`Fetching categories from DB...`, "AccSaberService");
|
||||
|
||||
const dbCategories = await getCategories();
|
||||
|
||||
log.trace(`DB categories fetched`, 'AccSaberService', dbCategories);
|
||||
log.trace(`DB categories fetched`, "AccSaberService", dbCategories);
|
||||
|
||||
if (!await shouldRefresh('categories', forceUpdate)) return {changed: [], all: dbCategories};
|
||||
if (!(await shouldRefresh("categories", forceUpdate)))
|
||||
return { changed: [], all: dbCategories };
|
||||
|
||||
log.trace(`Fetching current categories from AccSaber...`, 'AccSaberService');
|
||||
log.trace(
|
||||
`Fetching current categories from AccSaber...`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
let categories = await accSaberCategoriesApiClient.getProcessed({priority});
|
||||
let categories = await accSaberCategoriesApiClient.getProcessed({
|
||||
priority,
|
||||
});
|
||||
if (!categories || !categories.length) {
|
||||
log.warn(`AccSaber returned empty categories list`, 'AccSaberService')
|
||||
log.warn(`AccSaber returned empty categories list`, "AccSaberService");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
categories = categories.concat([{
|
||||
name: 'overall',
|
||||
displayName: 'Overall',
|
||||
categories = categories.concat([
|
||||
{
|
||||
name: "overall",
|
||||
displayName: "Overall",
|
||||
countsTowardsOverall: null,
|
||||
description: 'Overall'
|
||||
}]);
|
||||
description: "Overall",
|
||||
},
|
||||
]);
|
||||
|
||||
log.trace(`Categories fetched`, 'AccSaberService', categories);
|
||||
log.trace(`Categories fetched`, "AccSaberService", categories);
|
||||
|
||||
const dbCategoriesNames = dbCategories.map(c => c.name);
|
||||
const newCategories = categories.filter(c => !dbCategories || !dbCategoriesNames.includes(c.name));
|
||||
const dbCategoriesNames = dbCategories.map((c) => c.name);
|
||||
const newCategories = categories.filter(
|
||||
(c) => !dbCategories || !dbCategoriesNames.includes(c.name),
|
||||
);
|
||||
|
||||
if (newCategories && newCategories.length)
|
||||
log.debug(`${newCategories.length} new categories found`, 'AccSaberService');
|
||||
log.debug(
|
||||
`${newCategories.length} new categories found`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
await db.runInTransaction(['accsaber-categories', 'key-value'], async tx => {
|
||||
const newCategoriesNames = categories.map(c => c.name);
|
||||
await db.runInTransaction(
|
||||
["accsaber-categories", "key-value"],
|
||||
async (tx) => {
|
||||
const newCategoriesNames = categories.map((c) => c.name);
|
||||
|
||||
const accSaberCategoriesStore = tx.objectStore('accsaber-categories');
|
||||
const accSaberCategoriesStore = tx.objectStore("accsaber-categories");
|
||||
|
||||
let cursor = await accSaberCategoriesStore.openCursor();
|
||||
|
||||
log.trace(`Remove old categories from DB`, 'AccSaberService');
|
||||
log.trace(`Remove old categories from DB`, "AccSaberService");
|
||||
|
||||
while (cursor) {
|
||||
const category = cursor.value;
|
||||
if (!newCategoriesNames.includes(category.name)) await cursor.delete();
|
||||
if (!newCategoriesNames.includes(category.name))
|
||||
await cursor.delete();
|
||||
|
||||
cursor = await cursor.continue();
|
||||
}
|
||||
|
||||
log.trace(`Old categories removed from DB`, 'AccSaberService');
|
||||
log.trace(`Old categories removed from DB`, "AccSaberService");
|
||||
|
||||
log.trace(`Updating categories in DB...`, 'AccSaberService');
|
||||
log.trace(`Updating categories in DB...`, "AccSaberService");
|
||||
|
||||
await Promise.all(categories.map(async c => accSaberCategoriesStore.put(c)));
|
||||
await Promise.all(
|
||||
categories.map(async (c) => accSaberCategoriesStore.put(c)),
|
||||
);
|
||||
|
||||
log.trace(`Categories updated`, 'AccSaberService');
|
||||
log.trace(`Categories updated`, "AccSaberService");
|
||||
|
||||
log.trace(`Updating categories last update date in DB...`, 'AccSaberService');
|
||||
log.trace(
|
||||
`Updating categories last update date in DB...`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
await tx.objectStore('key-value').put(new Date(), getLastUpdatedKey('categories'));
|
||||
await tx
|
||||
.objectStore("key-value")
|
||||
.put(new Date(), getLastUpdatedKey("categories"));
|
||||
|
||||
log.debug(`Categories last update date updated`, 'AccSaberService');
|
||||
});
|
||||
log.debug(`Categories last update date updated`, "AccSaberService");
|
||||
},
|
||||
);
|
||||
|
||||
accSaberCategoriesRepository().addToCache(categories);
|
||||
keyValueRepository().setCache(getLastUpdatedKey('categories'), new Date());
|
||||
keyValueRepository().setCache(
|
||||
getLastUpdatedKey("categories"),
|
||||
new Date(),
|
||||
);
|
||||
|
||||
log.debug(`Categories refreshing completed`, 'AccSaberService');
|
||||
log.debug(`Categories refreshing completed`, "AccSaberService");
|
||||
|
||||
return {changed: newCategories, all: categories};
|
||||
}
|
||||
catch(e) {
|
||||
return { changed: newCategories, all: categories };
|
||||
} catch (e) {
|
||||
if (throwErrors) throw e;
|
||||
|
||||
log.debug(`Categories refreshing error`, 'AccSaberService', e)
|
||||
log.debug(`Categories refreshing error`, "AccSaberService", e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updatePlayerHistory = async player => {
|
||||
const updatePlayerHistory = async (player) => {
|
||||
if (!player?.playerId) return;
|
||||
|
||||
try {
|
||||
log.debug(`Updating player ${player.playerId} history`, 'AccSaberService');
|
||||
log.debug(
|
||||
`Updating player ${player.playerId} history`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
const accSaberDate = toAccSaberMidnight(new Date());
|
||||
const playerIdTimestamp = `${player.playerId}_${accSaberDate.getTime()}`;
|
||||
|
||||
const existingData = await accSaberPlayersHistoryRepository().get(playerIdTimestamp);
|
||||
const existingData =
|
||||
await accSaberPlayersHistoryRepository().get(playerIdTimestamp);
|
||||
const lastUpdated = dateFromString(existingData?.lastUpdated);
|
||||
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
|
||||
log.debug(`Refresh interval for player ${player.playerId} history not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'AccSaberService')
|
||||
log.debug(
|
||||
`Refresh interval for player ${
|
||||
player.playerId
|
||||
} history not yet expired, skipping. Next refresh on ${formatDate(
|
||||
addToDate(REFRESH_INTERVAL, lastUpdated),
|
||||
)}`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const categories = (await getCategories())?.map(c => c.name) ?? null;
|
||||
const categories = (await getCategories())?.map((c) => c.name) ?? null;
|
||||
if (!categories) {
|
||||
log.trace(`No categories found, skip updating player ${player.playerId} history.`);
|
||||
log.trace(
|
||||
`No categories found, skip updating player ${player.playerId} history.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let accStats = {};
|
||||
for (const category of categories) {
|
||||
const playerAccInfo = (await getRanking(category) ?? []).find(p => p.playerId === player.playerId);
|
||||
const playerAccInfo = ((await getRanking(category)) ?? []).find(
|
||||
(p) => p.playerId === player.playerId,
|
||||
);
|
||||
if (!playerAccInfo) continue;
|
||||
|
||||
const {
|
||||
@ -358,109 +485,175 @@ export default () => {
|
||||
accSaberDate,
|
||||
lastUpdated: new Date(),
|
||||
playerIdTimestamp,
|
||||
categories: accStats
|
||||
}
|
||||
categories: accStats,
|
||||
};
|
||||
|
||||
await accSaberPlayersHistoryRepository().set(stats);
|
||||
} else {
|
||||
log.trace(`No Acc Saber data for player ${player.playerId}, skipping history updating.`, 'AccSaberService');
|
||||
log.trace(
|
||||
`No Acc Saber data for player ${player.playerId}, skipping history updating.`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Player ${player.playerId} history updated`, 'AccSaberService');
|
||||
}
|
||||
catch(e) {
|
||||
log.debug(`Player ${player.playerId} history updating error.`, 'AccSaberService', e);
|
||||
}
|
||||
log.debug(`Player ${player.playerId} history updated`, "AccSaberService");
|
||||
} catch (e) {
|
||||
log.debug(
|
||||
`Player ${player.playerId} history updating error.`,
|
||||
"AccSaberService",
|
||||
e,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshRanking = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
|
||||
log.debug(`Starting AccSaber ${category} ranking refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
|
||||
const refreshRanking = async (
|
||||
category = "overall",
|
||||
forceUpdate = false,
|
||||
priority = queues.PRIORITY.BG_NORMAL,
|
||||
throwErrors = false,
|
||||
) => {
|
||||
log.debug(
|
||||
`Starting AccSaber ${category} ranking refreshing${
|
||||
forceUpdate ? " (forced)" : ""
|
||||
}...`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
try {
|
||||
log.trace(`Fetching ${category} ranking from DB...`, 'AccSaberService');
|
||||
log.trace(`Fetching ${category} ranking from DB...`, "AccSaberService");
|
||||
|
||||
const dbRanking = await getRanking(category);
|
||||
|
||||
log.trace(`DB ${category} ranking fetched`, 'AccSaberService', dbRanking);
|
||||
log.trace(`DB ${category} ranking fetched`, "AccSaberService", dbRanking);
|
||||
|
||||
const rankingType = `${category}Ranking`
|
||||
const rankingType = `${category}Ranking`;
|
||||
|
||||
if (!await shouldRefresh(rankingType, forceUpdate)) return dbRanking.sort((a, b) => a.rank - b.rank);
|
||||
if (!(await shouldRefresh(rankingType, forceUpdate)))
|
||||
return dbRanking.sort((a, b) => a.rank - b.rank);
|
||||
|
||||
log.trace(`Fetching current ${category} ranking from AccSaber...`, 'AccSaberService');
|
||||
log.trace(
|
||||
`Fetching current ${category} ranking from AccSaber...`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
const ranking = await accSaberRankingApiClient.getProcessed({category, priority});
|
||||
const ranking = await accSaberRankingApiClient.getProcessed({
|
||||
category,
|
||||
priority,
|
||||
});
|
||||
if (!ranking || !ranking.length) {
|
||||
log.warn(`AccSaber returned empty ${category} ranking`, 'AccSaberService')
|
||||
log.warn(
|
||||
`AccSaber returned empty ${category} ranking`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
log.trace(`${capitalize(category)} ranking fetched`, 'AccSaberService', ranking);
|
||||
log.trace(
|
||||
`${capitalize(category)} ranking fetched`,
|
||||
"AccSaberService",
|
||||
ranking,
|
||||
);
|
||||
|
||||
log.trace(`Updating ${category} ranking...`, 'AccSaberService');
|
||||
log.trace(`Updating ${category} ranking...`, "AccSaberService");
|
||||
|
||||
await db.runInTransaction(['accsaber-players', 'key-value'], async tx => {
|
||||
const newPlayerIds = ranking.map(c => c.playerId);
|
||||
await db.runInTransaction(
|
||||
["accsaber-players", "key-value"],
|
||||
async (tx) => {
|
||||
const newPlayerIds = ranking.map((c) => c.playerId);
|
||||
|
||||
const accSaberPlayersStore = tx.objectStore('accsaber-players');
|
||||
const accSaberPlayersStore = tx.objectStore("accsaber-players");
|
||||
|
||||
let cursor = await accSaberPlayersStore.openCursor();
|
||||
|
||||
log.trace(`Remove old players from DB for category ${category}`, 'AccSaberService');
|
||||
log.trace(
|
||||
`Remove old players from DB for category ${category}`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
while (cursor) {
|
||||
const player = cursor.value;
|
||||
if (player.category === category && !newPlayerIds.includes(player.playerId)) await cursor.delete();
|
||||
if (
|
||||
player.category === category &&
|
||||
!newPlayerIds.includes(player.playerId)
|
||||
)
|
||||
await cursor.delete();
|
||||
|
||||
cursor = await cursor.continue();
|
||||
}
|
||||
|
||||
log.trace(`Old players removed from DB`, 'AccSaberService');
|
||||
log.trace(`Old players removed from DB`, "AccSaberService");
|
||||
|
||||
log.trace(`Updating players in DB...`, 'AccSaberService');
|
||||
log.trace(`Updating players in DB...`, "AccSaberService");
|
||||
|
||||
await Promise.all(ranking.map(async p => accSaberPlayersStore.put(p)));
|
||||
await Promise.all(
|
||||
ranking.map(async (p) => accSaberPlayersStore.put(p)),
|
||||
);
|
||||
|
||||
log.trace(`Players updated`, 'AccSaberService');
|
||||
log.trace(`Players updated`, "AccSaberService");
|
||||
|
||||
log.trace(`Updating players last update date in DB...`, 'AccSaberService');
|
||||
log.trace(
|
||||
`Updating players last update date in DB...`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
await tx.objectStore('key-value').put(new Date(), getLastUpdatedKey(rankingType));
|
||||
await tx
|
||||
.objectStore("key-value")
|
||||
.put(new Date(), getLastUpdatedKey(rankingType));
|
||||
|
||||
log.debug(`Players last update date updated`, 'AccSaberService');
|
||||
});
|
||||
log.debug(`Players last update date updated`, "AccSaberService");
|
||||
},
|
||||
);
|
||||
|
||||
accSaberPlayersRepository().addToCache(ranking);
|
||||
keyValueRepository().setCache(getLastUpdatedKey(rankingType), new Date());
|
||||
|
||||
log.debug(`${capitalize(category)} ranking refreshing completed`, 'AccSaberService');
|
||||
log.debug(
|
||||
`${capitalize(category)} ranking refreshing completed`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
return ranking.sort((a, b) => a.rank - b.rank);
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
if (throwErrors) throw e;
|
||||
|
||||
log.debug(` ${capitalize(category)} ranking refreshing error`, 'AccSaberService', e)
|
||||
log.debug(
|
||||
` ${capitalize(category)} ranking refreshing error`,
|
||||
"AccSaberService",
|
||||
e,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAll = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
|
||||
log.trace(`Starting AccSaber all data refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService')
|
||||
const refreshAll = async (
|
||||
category = "overall",
|
||||
forceUpdate = false,
|
||||
priority = queues.PRIORITY.BG_NORMAL,
|
||||
throwErrors = false,
|
||||
) => {
|
||||
log.trace(
|
||||
`Starting AccSaber all data refreshing${
|
||||
forceUpdate ? " (forced)" : ""
|
||||
}...`,
|
||||
"AccSaberService",
|
||||
);
|
||||
|
||||
try {
|
||||
const dbCategories = await refreshCategories();
|
||||
if (!dbCategories || !dbCategories.all) throw 'Can not refresh categories';
|
||||
if (!dbCategories || !dbCategories.all)
|
||||
throw "Can not refresh categories";
|
||||
|
||||
const allRankings = await Promise.all(
|
||||
dbCategories.all.map(c => c.name).map(async category => refreshRanking(category))
|
||||
)
|
||||
dbCategories.all
|
||||
.map((c) => c.name)
|
||||
.map(async (category) => refreshRanking(category)),
|
||||
);
|
||||
|
||||
log.debug(`All data refreshing completed.`, 'AccSaberService')
|
||||
log.debug(`All data refreshing completed.`, "AccSaberService");
|
||||
|
||||
const rankings = allRankings.reduce((cum, ranking) => {
|
||||
if (!ranking || !ranking.length) return cum;
|
||||
@ -470,21 +663,28 @@ export default () => {
|
||||
return cum;
|
||||
}, {});
|
||||
|
||||
Promise.all((await playerService.getAllActive()).map(async player => updatePlayerHistory(player))).then(_ => _);
|
||||
Promise.all(
|
||||
(await playerService.getAllActive()).map(async (player) =>
|
||||
updatePlayerHistory(player),
|
||||
),
|
||||
).then((_) => _);
|
||||
|
||||
return dbCategories.all.map(c => ({...c, ranking: rankings?.[c.name] ?? []}));
|
||||
return dbCategories.all.map((c) => ({
|
||||
...c,
|
||||
ranking: rankings?.[c.name] ?? [],
|
||||
}));
|
||||
} catch (e) {
|
||||
if (throwErrors) throw e;
|
||||
|
||||
log.debug(`All data refreshing error`, 'AccSaberService', e)
|
||||
log.debug(`All data refreshing error`, "AccSaberService", e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const destroyService = () => {
|
||||
service = null;
|
||||
}
|
||||
};
|
||||
|
||||
service = {
|
||||
isDataForPlayerAvailable,
|
||||
@ -502,7 +702,7 @@ export default () => {
|
||||
refreshRanking,
|
||||
refreshAll,
|
||||
destroyService,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
@ -1,28 +1,29 @@
|
||||
import hashApiClient from '../network/clients/beatmaps/api-hash';
|
||||
import keyApiClient from '../network/clients/beatmaps/api-key';
|
||||
import {PRIORITY} from '../network/queues/http-queue';
|
||||
import log from '../utils/logger'
|
||||
import {SsrHttpNotFoundError, SsrNetworkError} from '../network/errors'
|
||||
import hashApiClient from "../network/clients/beatmaps/api-hash";
|
||||
import keyApiClient from "../network/clients/beatmaps/api-key";
|
||||
import { PRIORITY } from "../network/queues/http-queue";
|
||||
import log from "../utils/logger";
|
||||
import { SsrHttpNotFoundError, SsrNetworkError } from "../network/errors";
|
||||
import songsBeatMapsRepository from "../db/repository/songs-beatmaps";
|
||||
import cacheRepository from "../db/repository/cache";
|
||||
import {addToDate, dateFromString, HOUR} from '../utils/date'
|
||||
import {capitalize, opt} from '../utils/js'
|
||||
import { addToDate, dateFromString, HOUR } from "../utils/date";
|
||||
import { capitalize, opt } from "../utils/js";
|
||||
|
||||
const BM_SUSPENSION_KEY = 'bmSuspension';
|
||||
const BM_NOT_FOUND_KEY = 'bm404';
|
||||
const BM_SUSPENSION_KEY = "bmSuspension";
|
||||
const BM_NOT_FOUND_KEY = "bm404";
|
||||
const BM_NOT_FOUND_HOURS_BETWEEN_COUNTS = 1;
|
||||
|
||||
const INVALID_NOTES_COUNT_FIXES = {
|
||||
'e738b38b594861745bfb0473c66ca5cca15072ff': [
|
||||
{type: 'Standard', diff: "ExpertPlus", notes: 942}
|
||||
]
|
||||
}
|
||||
e738b38b594861745bfb0473c66ca5cca15072ff: [
|
||||
{ type: "Standard", diff: "ExpertPlus", notes: 942 },
|
||||
],
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const cacheSongInfo = async (songInfo, originalHash) => {
|
||||
if (!songInfo) return null;
|
||||
|
||||
const hash = originalHash && originalHash.length ? originalHash : songInfo.hash;
|
||||
const hash =
|
||||
originalHash && originalHash.length ? originalHash : songInfo.hash;
|
||||
|
||||
if (!hash || !songInfo.key) return null;
|
||||
|
||||
@ -34,30 +35,46 @@ export default () => {
|
||||
await songsBeatMapsRepository().set(songInfo);
|
||||
|
||||
return songInfo;
|
||||
}
|
||||
};
|
||||
|
||||
const isSuspended = bsSuspension => !!bsSuspension && bsSuspension.activeTo > new Date() && bsSuspension.started > addToDate(-24 * HOUR);
|
||||
const getCurrentSuspension = async () => cacheRepository().get(BM_SUSPENSION_KEY);
|
||||
const prolongSuspension = async bsSuspension => {
|
||||
const isSuspended = (bsSuspension) =>
|
||||
!!bsSuspension &&
|
||||
bsSuspension.activeTo > new Date() &&
|
||||
bsSuspension.started > addToDate(-24 * HOUR);
|
||||
const getCurrentSuspension = async () =>
|
||||
cacheRepository().get(BM_SUSPENSION_KEY);
|
||||
const prolongSuspension = async (bsSuspension) => {
|
||||
const current = new Date();
|
||||
|
||||
const suspension = isSuspended(bsSuspension) ? bsSuspension : {started: current, activeTo: new Date(), count: 0};
|
||||
const suspension = isSuspended(bsSuspension)
|
||||
? bsSuspension
|
||||
: { started: current, activeTo: new Date(), count: 0 };
|
||||
|
||||
suspension.activeTo = addToDate(Math.pow(2, suspension.count) * HOUR, suspension.activeTo);
|
||||
suspension.activeTo = addToDate(
|
||||
Math.pow(2, suspension.count) * HOUR,
|
||||
suspension.activeTo,
|
||||
);
|
||||
suspension.count++;
|
||||
|
||||
return await cacheRepository().set(suspension, BM_SUSPENSION_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY);
|
||||
const set404Hashes = async hashes => cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
|
||||
const setHashNotFound = async hash => {
|
||||
const set404Hashes = async (hashes) =>
|
||||
cacheRepository().set(hashes, BM_NOT_FOUND_KEY);
|
||||
const setHashNotFound = async (hash) => {
|
||||
let songs404 = await get404Hashes();
|
||||
if (!songs404) songs404 = {};
|
||||
|
||||
const item = songs404[hash] ? songs404[hash] : {firstTry: new Date(), recentTry: null, count: 0};
|
||||
const item = songs404[hash]
|
||||
? songs404[hash]
|
||||
: { firstTry: new Date(), recentTry: null, count: 0 };
|
||||
|
||||
if (!item.recentTry || addToDate(BM_NOT_FOUND_HOURS_BETWEEN_COUNTS * HOUR, item.recentTry) < new Date()) {
|
||||
if (
|
||||
!item.recentTry ||
|
||||
addToDate(BM_NOT_FOUND_HOURS_BETWEEN_COUNTS * HOUR, item.recentTry) <
|
||||
new Date()
|
||||
) {
|
||||
item.recentTry = new Date();
|
||||
item.count++;
|
||||
|
||||
@ -65,39 +82,52 @@ export default () => {
|
||||
|
||||
await set404Hashes(songs404);
|
||||
}
|
||||
}
|
||||
const isHashUnavailable = async hash => {
|
||||
};
|
||||
const isHashUnavailable = async (hash) => {
|
||||
const songs404 = await get404Hashes();
|
||||
return songs404 && songs404[hash] && songs404[hash].count >= 3;
|
||||
}
|
||||
};
|
||||
|
||||
const fixInvalidNotesCount = (hash, songInfo) => {
|
||||
if (!hash) return songInfo;
|
||||
|
||||
if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions)
|
||||
songInfo.versions.forEach(si => {
|
||||
songInfo.versions.forEach((si) => {
|
||||
if (!si?.diffs) return;
|
||||
|
||||
si.diffs.forEach(d => {
|
||||
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(f => f.type === d?.characteristic && f.diff === d?.difficulty);
|
||||
si.diffs.forEach((d) => {
|
||||
const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(
|
||||
(f) => f.type === d?.characteristic && f.diff === d?.difficulty,
|
||||
);
|
||||
if (!newNotesCnt) return;
|
||||
|
||||
d.notes = newNotesCnt.notes;
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return songInfo;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSong = async (songInfo, fetchFunc, forceUpdate = false, cacheOnly = false, errSongId = '', hash = null) => {
|
||||
const fetchSong = async (
|
||||
songInfo,
|
||||
fetchFunc,
|
||||
forceUpdate = false,
|
||||
cacheOnly = false,
|
||||
errSongId = "",
|
||||
hash = null,
|
||||
) => {
|
||||
if (!forceUpdate && songInfo) return fixInvalidNotesCount(hash, songInfo);
|
||||
|
||||
if(cacheOnly) return null;
|
||||
if (cacheOnly) return null;
|
||||
|
||||
let bsSuspension = await getCurrentSuspension();
|
||||
|
||||
try {
|
||||
if (isSuspended(bsSuspension) || (hash && await isHashUnavailable(hash))) return null;
|
||||
if (
|
||||
isSuspended(bsSuspension) ||
|
||||
(hash && (await isHashUnavailable(hash)))
|
||||
)
|
||||
return null;
|
||||
|
||||
const songInfo = await fetchFunc();
|
||||
if (!songInfo) {
|
||||
@ -111,36 +141,78 @@ export default () => {
|
||||
await setHashNotFound(hash);
|
||||
}
|
||||
|
||||
if (err instanceof SsrNetworkError && err.message === 'Network error') {
|
||||
try {await prolongSuspension(bsSuspension)} catch {}
|
||||
if (err instanceof SsrNetworkError && err.message === "Network error") {
|
||||
try {
|
||||
await prolongSuspension(bsSuspension);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
log.warn(`Error fetching BeatSaver song "${errSongId}"`);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const byHash = async (hash, forceUpdate = false, cacheOnly = false, signal = null, priority = PRIORITY.FG_LOW) => {
|
||||
const byHash = async (
|
||||
hash,
|
||||
forceUpdate = false,
|
||||
cacheOnly = false,
|
||||
signal = null,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
) => {
|
||||
hash = hash.toLowerCase();
|
||||
|
||||
const songInfo = await songsBeatMapsRepository().get(hash);
|
||||
|
||||
return fetchSong(songInfo, () => hashApiClient.getProcessed({hash, signal, priority}), forceUpdate, cacheOnly, hash, hash)
|
||||
}
|
||||
return fetchSong(
|
||||
songInfo,
|
||||
() => hashApiClient.getProcessed({ hash, signal, priority }),
|
||||
forceUpdate,
|
||||
cacheOnly,
|
||||
hash,
|
||||
hash,
|
||||
);
|
||||
};
|
||||
|
||||
const byKey = async (key, forceUpdate = false, cacheOnly = false, signal = null, priority = PRIORITY.FG_LOW) => {
|
||||
const byKey = async (
|
||||
key,
|
||||
forceUpdate = false,
|
||||
cacheOnly = false,
|
||||
signal = null,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
) => {
|
||||
key = key.toLowerCase();
|
||||
|
||||
const songInfo = await songsBeatMapsRepository().getFromIndex('songs-beatmaps-key', key);
|
||||
const songInfo = await songsBeatMapsRepository().getFromIndex(
|
||||
"songs-beatmaps-key",
|
||||
key,
|
||||
);
|
||||
|
||||
return fetchSong(songInfo, () => keyApiClient.getProcessed({key, signal, priority}), forceUpdate, cacheOnly, key)
|
||||
}
|
||||
return fetchSong(
|
||||
songInfo,
|
||||
() => keyApiClient.getProcessed({ key, signal, priority }),
|
||||
forceUpdate,
|
||||
cacheOnly,
|
||||
key,
|
||||
);
|
||||
};
|
||||
|
||||
const convertOldBeatSaverToBeatMaps = song => {
|
||||
let {key, hash, name, metadata: {characteristics}} = song;
|
||||
const convertOldBeatSaverToBeatMaps = (song) => {
|
||||
let {
|
||||
key,
|
||||
hash,
|
||||
name,
|
||||
metadata: { characteristics },
|
||||
} = song;
|
||||
|
||||
if (!key || !hash || !name || !characteristics || !Array.isArray(characteristics)) return null;
|
||||
if (
|
||||
!key ||
|
||||
!hash ||
|
||||
!name ||
|
||||
!characteristics ||
|
||||
!Array.isArray(characteristics)
|
||||
)
|
||||
return null;
|
||||
|
||||
if (hash.toLowerCase) hash = hash.toLowerCase();
|
||||
|
||||
@ -148,25 +220,25 @@ export default () => {
|
||||
if (!ch.name || !ch.difficulties) return diffs;
|
||||
const characteristic = ch.name;
|
||||
|
||||
return diffs.concat(
|
||||
Object.entries(ch.difficulties)
|
||||
.map(([difficulty, obj]) => {
|
||||
return diffs
|
||||
.concat(
|
||||
Object.entries(ch.difficulties).map(([difficulty, obj]) => {
|
||||
if (!obj) return null;
|
||||
difficulty = capitalize(difficulty);
|
||||
|
||||
const seconds = opt(obj, 'length', null);
|
||||
const notes = opt(obj, 'notes', null)
|
||||
const seconds = opt(obj, "length", null);
|
||||
const notes = opt(obj, "notes", null);
|
||||
|
||||
const nps = notes && seconds ? notes / seconds : null;
|
||||
|
||||
return {
|
||||
njs: opt(obj, 'njs', null),
|
||||
offset: opt(obj, 'njsOffset', null),
|
||||
njs: opt(obj, "njs", null),
|
||||
offset: opt(obj, "njsOffset", null),
|
||||
notes,
|
||||
bombs: opt(obj, 'bombs', null),
|
||||
obstacles: opt(obj, 'obstacles', null),
|
||||
bombs: opt(obj, "bombs", null),
|
||||
obstacles: opt(obj, "obstacles", null),
|
||||
nps,
|
||||
length: opt(obj, 'duration', null),
|
||||
length: opt(obj, "duration", null),
|
||||
characteristic,
|
||||
difficulty,
|
||||
events: null,
|
||||
@ -182,41 +254,42 @@ export default () => {
|
||||
},
|
||||
stars: null,
|
||||
};
|
||||
}))
|
||||
.filter(diff => diff)
|
||||
}),
|
||||
)
|
||||
.filter((diff) => diff);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
lastUpdated: dateFromString(opt(song, 'uploaded', new Date())),
|
||||
oldBeatSaverId: opt(song, '_id', null),
|
||||
lastUpdated: dateFromString(opt(song, "uploaded", new Date())),
|
||||
oldBeatSaverId: opt(song, "_id", null),
|
||||
id: key,
|
||||
hash,
|
||||
key,
|
||||
name,
|
||||
description: '',
|
||||
description: "",
|
||||
uploader: {
|
||||
id: null,
|
||||
name: opt(song, 'uploader.username', null),
|
||||
name: opt(song, "uploader.username", null),
|
||||
hash: null,
|
||||
avatar: null
|
||||
avatar: null,
|
||||
},
|
||||
metadata: {
|
||||
bpm: opt(song, 'metadata.bpm', null),
|
||||
duration: opt(song, 'metadata.duration', null),
|
||||
songName: opt(song, 'metadata.songName', ''),
|
||||
songSubName: opt(song, 'metadata.songSubName', ''),
|
||||
songAuthorName: opt(song, 'metadata.songAuthorName', ''),
|
||||
levelAuthorName: opt(song, 'metadata.levelAuthorName', '')
|
||||
bpm: opt(song, "metadata.bpm", null),
|
||||
duration: opt(song, "metadata.duration", null),
|
||||
songName: opt(song, "metadata.songName", ""),
|
||||
songSubName: opt(song, "metadata.songSubName", ""),
|
||||
songAuthorName: opt(song, "metadata.songAuthorName", ""),
|
||||
levelAuthorName: opt(song, "metadata.levelAuthorName", ""),
|
||||
},
|
||||
stats: {
|
||||
plays: opt(song, 'stats.plays', 0),
|
||||
downloads: opt(song, 'stats.downloads', 0),
|
||||
upvotes: opt(song, 'stats.upVotes', 0),
|
||||
downvotes: opt(song, 'stats.downVotes', 0),
|
||||
score: null
|
||||
plays: opt(song, "stats.plays", 0),
|
||||
downloads: opt(song, "stats.downloads", 0),
|
||||
upvotes: opt(song, "stats.upVotes", 0),
|
||||
downvotes: opt(song, "stats.downVotes", 0),
|
||||
score: null,
|
||||
},
|
||||
uploaded: opt(song, 'uploaded', null),
|
||||
automapper: !!opt(song, 'metadata.automapper', false),
|
||||
uploaded: opt(song, "uploaded", null),
|
||||
automapper: !!opt(song, "metadata.automapper", false),
|
||||
ranked: null,
|
||||
qualified: null,
|
||||
versions: [
|
||||
@ -224,20 +297,20 @@ export default () => {
|
||||
hash,
|
||||
key,
|
||||
state: "Published",
|
||||
createdAt: opt(song, 'uploaded', null),
|
||||
createdAt: opt(song, "uploaded", null),
|
||||
sageScore: null,
|
||||
diffs,
|
||||
downloadURL: `https://cdn.beatsaver.com/${hash}.zip`,
|
||||
coverURL: `https://cdn.beatsaver.com/${hash}.jpg`,
|
||||
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
previewURL: `https://cdn.beatsaver.com/${hash}.mp3`,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
byHash,
|
||||
byKey,
|
||||
convertOldBeatSaverToBeatMaps
|
||||
}
|
||||
}
|
||||
convertOldBeatSaverToBeatMaps,
|
||||
};
|
||||
};
|
||||
|
@ -1,16 +1,24 @@
|
||||
import {PRIORITY} from '../network/queues/http-queue';
|
||||
import createPlayerService from './scoresaber/player'
|
||||
import createScoresService from './scoresaber/scores'
|
||||
import beatSaviorApiClient from '../network/clients/beatsavior/api';
|
||||
import beatSaviorRepository from '../db/repository/beat-savior'
|
||||
import beatSaviorPlayersRepository from '../db/repository/beat-savior-players'
|
||||
import {addToDate, DAY, formatDate, HOUR, MINUTE, SECOND, truncateDate} from '../utils/date'
|
||||
import log from '../utils/logger'
|
||||
import {opt} from '../utils/js'
|
||||
import makePendingPromisePool from '../utils/pending-promises'
|
||||
import {PLAYER_SCORES_PER_PAGE} from '../utils/scoresaber/consts'
|
||||
import {roundToPrecision} from '../utils/format'
|
||||
import {serviceFilterFunc} from './utils'
|
||||
import { PRIORITY } from "../network/queues/http-queue";
|
||||
import createPlayerService from "./scoresaber/player";
|
||||
import createScoresService from "./scoresaber/scores";
|
||||
import beatSaviorApiClient from "../network/clients/beatsavior/api";
|
||||
import beatSaviorRepository from "../db/repository/beat-savior";
|
||||
import beatSaviorPlayersRepository from "../db/repository/beat-savior-players";
|
||||
import {
|
||||
addToDate,
|
||||
DAY,
|
||||
formatDate,
|
||||
HOUR,
|
||||
MINUTE,
|
||||
SECOND,
|
||||
truncateDate,
|
||||
} from "../utils/date";
|
||||
import log from "../utils/logger";
|
||||
import { opt } from "../utils/js";
|
||||
import makePendingPromisePool from "../utils/pending-promises";
|
||||
import { PLAYER_SCORES_PER_PAGE } from "../utils/scoresaber/consts";
|
||||
import { roundToPrecision } from "../utils/format";
|
||||
import { serviceFilterFunc } from "./utils";
|
||||
|
||||
const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 15;
|
||||
const CACHED_PLAYER_REFRESH_INTERVAL = HOUR * 3;
|
||||
@ -30,52 +38,73 @@ export default () => {
|
||||
const playerService = createPlayerService();
|
||||
const scoresService = createScoresService();
|
||||
|
||||
const getPlayerScores = async playerId => resolvePromiseOrWaitForPending(`getPlayerScores/${playerId}`, () => beatSaviorRepository().getAllFromIndex('beat-savior-playerId', playerId));
|
||||
const getPlayerScores = async (playerId) =>
|
||||
resolvePromiseOrWaitForPending(`getPlayerScores/${playerId}`, () =>
|
||||
beatSaviorRepository().getAllFromIndex("beat-savior-playerId", playerId),
|
||||
);
|
||||
|
||||
const getPlayerScoresWithScoreSaber = async playerId => {
|
||||
const getPlayerScoresWithScoreSaber = async (playerId) => {
|
||||
const [beatSaviorData, playerScores] = await Promise.all([
|
||||
getPlayerScores(playerId),
|
||||
resolvePromiseOrWaitForPending(`getSsPlayerScores/${playerId}`, () => scoresService.getPlayerScoresAsObject(
|
||||
resolvePromiseOrWaitForPending(`getSsPlayerScores/${playerId}`, () =>
|
||||
scoresService.getPlayerScoresAsObject(
|
||||
playerId,
|
||||
score => score?.leaderboard?.song?.hash?.toLowerCase() ?? null,
|
||||
(score) => score?.leaderboard?.song?.hash?.toLowerCase() ?? null,
|
||||
true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
return beatSaviorData.map(bsData => {
|
||||
if (!bsData?.hash || !playerScores?.[bsData?.hash?.toLowerCase()]) return bsData;
|
||||
return beatSaviorData.map((bsData) => {
|
||||
if (!bsData?.hash || !playerScores?.[bsData?.hash?.toLowerCase()])
|
||||
return bsData;
|
||||
|
||||
const ssScore = playerScores[bsData.hash.toLowerCase()].find(ssScore => isScoreMatchingBsData(ssScore, bsData, true)) ?? null;
|
||||
const ssScore =
|
||||
playerScores[bsData.hash.toLowerCase()].find((ssScore) =>
|
||||
isScoreMatchingBsData(ssScore, bsData, true),
|
||||
) ?? null;
|
||||
|
||||
return {
|
||||
...bsData,
|
||||
ssScore
|
||||
}
|
||||
ssScore,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isScoreMatchingBsData = (score, bsData, exact = true) => {
|
||||
if (!bsData.hash || !bsData.score || !bsData.timeSet || !opt(bsData, 'stats.won')) return false;
|
||||
if (
|
||||
!bsData.hash ||
|
||||
!bsData.score ||
|
||||
!bsData.timeSet ||
|
||||
!opt(bsData, "stats.won")
|
||||
)
|
||||
return false;
|
||||
|
||||
const diff = opt(score, 'leaderboard.diffInfo.diff');
|
||||
const scoreValue = opt(score, 'score.score');
|
||||
const timeSet = opt(score, 'score.timeSet')
|
||||
let hash = opt(score, 'leaderboard.song.hash');
|
||||
const diff = opt(score, "leaderboard.diffInfo.diff");
|
||||
const scoreValue = opt(score, "score.score");
|
||||
const timeSet = opt(score, "score.timeSet");
|
||||
let hash = opt(score, "leaderboard.song.hash");
|
||||
|
||||
if (!diff || !score || !timeSet || !hash) return false;
|
||||
|
||||
hash = hash.toLowerCase();
|
||||
|
||||
if (bsData.hash === hash && bsData.diff === diff) {
|
||||
return !exact || (bsData.score === scoreValue && Math.abs(timeSet.getTime() - bsData.timeSet.getTime()) < MINUTE);
|
||||
return (
|
||||
!exact ||
|
||||
(bsData.score === scoreValue &&
|
||||
Math.abs(timeSet.getTime() - bsData.timeSet.getTime()) < MINUTE)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getScoresHistogramDefinition = (serviceParams = {sort: 'recent', order: 'desc'}) => {
|
||||
const sort = serviceParams?.sort ?? 'recent';
|
||||
const order = serviceParams?.order ?? 'desc';
|
||||
const getScoresHistogramDefinition = (
|
||||
serviceParams = { sort: "recent", order: "desc" },
|
||||
) => {
|
||||
const sort = serviceParams?.sort ?? "recent";
|
||||
const order = serviceParams?.order ?? "desc";
|
||||
|
||||
let round = 2;
|
||||
let bucketSize = 1;
|
||||
@ -83,57 +112,65 @@ export default () => {
|
||||
let maxBucketSize = null;
|
||||
let bucketSizeStep = null;
|
||||
let bucketSizeValues = null;
|
||||
let type = 'linear';
|
||||
let valFunc = s => s;
|
||||
let type = "linear";
|
||||
let valFunc = (s) => s;
|
||||
let filterFunc = serviceFilterFunc(serviceParams);
|
||||
let histogramFilterFunc = s => s;
|
||||
let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear'
|
||||
let histogramFilterFunc = (s) => s;
|
||||
let roundedValFunc = (s, type = type, precision = bucketSize) =>
|
||||
type === "linear"
|
||||
? roundToPrecision(valFunc(s), precision)
|
||||
: truncateDate(valFunc(s), precision);
|
||||
let prefix = '';
|
||||
let prefixLong = '';
|
||||
let suffix = '';
|
||||
let suffixLong = '';
|
||||
let prefix = "";
|
||||
let prefixLong = "";
|
||||
let suffix = "";
|
||||
let suffixLong = "";
|
||||
|
||||
switch(sort) {
|
||||
case 'recent':
|
||||
valFunc = s => s?.timeSet;
|
||||
type = 'time';
|
||||
bucketSize = 'day'
|
||||
switch (sort) {
|
||||
case "recent":
|
||||
valFunc = (s) => s?.timeSet;
|
||||
type = "time";
|
||||
bucketSize = "day";
|
||||
break;
|
||||
|
||||
case 'acc':
|
||||
valFunc = s => (s?.trackers?.scoreTracker?.rawRatio ?? 0) * 100;
|
||||
histogramFilterFunc = h => h?.x >= HISTOGRAM_ACC_THRESHOLD;
|
||||
type = 'linear';
|
||||
case "acc":
|
||||
valFunc = (s) => (s?.trackers?.scoreTracker?.rawRatio ?? 0) * 100;
|
||||
histogramFilterFunc = (h) => h?.x >= HISTOGRAM_ACC_THRESHOLD;
|
||||
type = "linear";
|
||||
bucketSize = 0.25;
|
||||
minBucketSize = 0.05;
|
||||
maxBucketSize = 10;
|
||||
bucketSizeStep = 0.05;
|
||||
round = 2;
|
||||
suffix = '%';
|
||||
suffixLong = '%';
|
||||
suffix = "%";
|
||||
suffixLong = "%";
|
||||
break;
|
||||
|
||||
case 'mistakes':
|
||||
valFunc = s => (s?.stats?.miss ?? 0) + (s?.stats?.wallHit ?? 0) + (s?.stats?.bombHit ?? 0);
|
||||
histogramFilterFunc = h => h?.x <= HISTOGRAM_MISTAKES_THRESHOLD;
|
||||
type = 'linear';
|
||||
case "mistakes":
|
||||
valFunc = (s) =>
|
||||
(s?.stats?.miss ?? 0) +
|
||||
(s?.stats?.wallHit ?? 0) +
|
||||
(s?.stats?.bombHit ?? 0);
|
||||
histogramFilterFunc = (h) => h?.x <= HISTOGRAM_MISTAKES_THRESHOLD;
|
||||
type = "linear";
|
||||
bucketSize = 1;
|
||||
minBucketSize = 1;
|
||||
maxBucketSize = 50;
|
||||
bucketSizeStep = 1;
|
||||
round = 0;
|
||||
suffixLong = ' mistake(s)';
|
||||
suffixLong = " mistake(s)";
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
getValue: valFunc,
|
||||
getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize),
|
||||
getRoundedValue:
|
||||
(bucketSize = bucketSize) =>
|
||||
(s) =>
|
||||
roundedValFunc(s, type, bucketSize),
|
||||
filter: filterFunc,
|
||||
histogramFilter: histogramFilterFunc,
|
||||
sort: (a, b) => order === 'asc' ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a),
|
||||
sort: (a, b) =>
|
||||
order === "asc" ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a),
|
||||
type,
|
||||
bucketSize,
|
||||
minBucketSize,
|
||||
@ -145,37 +182,42 @@ export default () => {
|
||||
prefixLong,
|
||||
suffix,
|
||||
suffixLong,
|
||||
order
|
||||
}
|
||||
}
|
||||
order,
|
||||
};
|
||||
};
|
||||
|
||||
const getPlayerScoresPage = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}) => {
|
||||
const getPlayerScoresPage = async (
|
||||
playerId,
|
||||
serviceParams = { sort: "recent", order: "desc", page: 1 },
|
||||
) => {
|
||||
let page = serviceParams?.page ?? 1;
|
||||
if (page < 1) page = 1;
|
||||
|
||||
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;
|
||||
|
||||
if (playerScores.length < startIdx + 1) return {total: 0, scores: []};
|
||||
if (playerScores.length < startIdx + 1) return { total: 0, scores: [] };
|
||||
|
||||
return {
|
||||
total: playerScores.length,
|
||||
scores: playerScores
|
||||
.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE)
|
||||
.map(bs => {
|
||||
.map((bs) => {
|
||||
const leaderboard = bs.leaderboard;
|
||||
if (!leaderboard.leaderboardId) leaderboard.leaderboardId = bs.beatSaviorId;
|
||||
if (!leaderboard.leaderboardId)
|
||||
leaderboard.leaderboardId = bs.beatSaviorId;
|
||||
leaderboard.leaderboardId += Math.random(); // ScoresSvelte needs different keys for each scores row
|
||||
|
||||
const rawScore = opt(bs, 'trackers.scoreTracker.rawScore', 0);
|
||||
const rawRatio = opt(bs, 'trackers.scoreTracker.rawRatio', 0);
|
||||
const rawScore = opt(bs, "trackers.scoreTracker.rawScore", 0);
|
||||
const rawRatio = opt(bs, "trackers.scoreTracker.rawRatio", 0);
|
||||
const maxScore = rawRatio & rawScore ? rawScore / rawRatio : 0;
|
||||
|
||||
return {
|
||||
@ -188,51 +230,76 @@ export default () => {
|
||||
score: {
|
||||
acc: rawRatio * 100,
|
||||
maxScore,
|
||||
mods: opt(bs, 'trackers.scoreTracker.modifiers', null),
|
||||
percentage: opt(bs, 'trackers.scoreTracker.rawRatio', 0) * 100,
|
||||
mods: opt(bs, "trackers.scoreTracker.modifiers", null),
|
||||
percentage: opt(bs, "trackers.scoreTracker.rawRatio", 0) * 100,
|
||||
pp: 0,
|
||||
ppWeighted: 0,
|
||||
rank: null,
|
||||
score: opt(bs, 'trackers.scoreTracker.score', 0),
|
||||
score: opt(bs, "trackers.scoreTracker.score", 0),
|
||||
scoreId: bs.beatSaviorId,
|
||||
timeSet: bs.timeSet,
|
||||
unmodifiedScore: rawScore,
|
||||
weight: 0,
|
||||
},
|
||||
timeSet: bs.timeSet,
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const updateData = async (playerId, data) => {
|
||||
log.debug(`Updating Beat Savior data for player "${playerId}"...`, 'BeatSaviorService')
|
||||
log.debug(
|
||||
`Updating Beat Savior data for player "${playerId}"...`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
await Promise.all(data.map(async d => beatSaviorRepository().set(d)));
|
||||
await Promise.all(data.map(async (d) => beatSaviorRepository().set(d)));
|
||||
|
||||
log.debug(`Update player "${playerId}" Beat Savior last refresh date...`, 'BeatSaviorService')
|
||||
log.debug(
|
||||
`Update player "${playerId}" Beat Savior last refresh date...`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
await beatSaviorPlayersRepository().set({playerId, lastRefresh: new Date()})
|
||||
await beatSaviorPlayersRepository().set({
|
||||
playerId,
|
||||
lastRefresh: new Date(),
|
||||
});
|
||||
|
||||
log.debug(`Beat Savior data for player "${playerId}" updated.`, 'BeatSaviorService')
|
||||
log.debug(
|
||||
`Beat Savior data for player "${playerId}" updated.`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPlayer = async (playerId, priority = PRIORITY.BG_NORMAL) => {
|
||||
try {
|
||||
log.debug(`Fetching Beat Savior data for player "${playerId}"...`, 'BeatSaviorService');
|
||||
log.debug(
|
||||
`Fetching Beat Savior data for player "${playerId}"...`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
const data = await beatSaviorApiClient.getProcessed({playerId, priority});
|
||||
const data = await beatSaviorApiClient.getProcessed({
|
||||
playerId,
|
||||
priority,
|
||||
});
|
||||
if (!data) {
|
||||
log.debug(`No Beat Savior data for player "${playerId}"`, 'BeatSaviorService')
|
||||
log.debug(
|
||||
`No Beat Savior data for player "${playerId}"`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: check if data already exists in DB
|
||||
|
||||
log.trace(`Beat Savior data for player "${playerId}" fetched`, 'BeatSaviorService', data);
|
||||
log.trace(
|
||||
`Beat Savior data for player "${playerId}" fetched`,
|
||||
"BeatSaviorService",
|
||||
data,
|
||||
);
|
||||
|
||||
return updateData(playerId, data);
|
||||
} catch (err) {
|
||||
@ -240,62 +307,121 @@ export default () => {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = async (playerId, force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
|
||||
log.trace(`Starting refreshing BeatSavior for player "${playerId}" ${force ? ' (forced)' : ''}...`, 'BeatSaviorService')
|
||||
const refresh = async (
|
||||
playerId,
|
||||
force = false,
|
||||
priority = PRIORITY.BG_NORMAL,
|
||||
throwErrors = false,
|
||||
) => {
|
||||
log.trace(
|
||||
`Starting refreshing BeatSavior for player "${playerId}" ${
|
||||
force ? " (forced)" : ""
|
||||
}...`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
try {
|
||||
const player = await playerService.get(playerId);
|
||||
|
||||
const REFRESH_INTERVAL = playerService.isMainPlayer(playerId) ? MAIN_PLAYER_REFRESH_INTERVAL : (player ? CACHED_PLAYER_REFRESH_INTERVAL : OTHER_PLAYER_REFRESH_INTERVAL);
|
||||
const REFRESH_INTERVAL = playerService.isMainPlayer(playerId)
|
||||
? MAIN_PLAYER_REFRESH_INTERVAL
|
||||
: player
|
||||
? CACHED_PLAYER_REFRESH_INTERVAL
|
||||
: OTHER_PLAYER_REFRESH_INTERVAL;
|
||||
|
||||
const bsPlayerInfo = await beatSaviorPlayersRepository().get(playerId);
|
||||
const nextUpdate = bsPlayerInfo && bsPlayerInfo.lastRefresh ? addToDate(REFRESH_INTERVAL, bsPlayerInfo.lastRefresh) : addToDate(-SECOND);
|
||||
const nextUpdate =
|
||||
bsPlayerInfo && bsPlayerInfo.lastRefresh
|
||||
? addToDate(REFRESH_INTERVAL, bsPlayerInfo.lastRefresh)
|
||||
: addToDate(-SECOND);
|
||||
if (!force && bsPlayerInfo && nextUpdate > new Date()) {
|
||||
log.debug(`Beat Savior data is still fresh, skipping. Next refresh on ${formatDate(nextUpdate)}`, 'BeatSaviorService')
|
||||
log.debug(
|
||||
`Beat Savior data is still fresh, skipping. Next refresh on ${formatDate(
|
||||
nextUpdate,
|
||||
)}`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
return null;
|
||||
|
||||
if (player) {
|
||||
log.trace(`Player "${playerId}" is a cached one, checking recent play date`, 'BeatSaviorService')
|
||||
log.trace(
|
||||
`Player "${playerId}" is a cached one, checking recent play date`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
if (player.recentPlay && player.recentPlay < bsPlayerInfo.lastRefresh) {
|
||||
log.debug(`Beat Savior data for player "${playerId}" was refreshed after recent play, skipping`, 'BeatSaviorService')
|
||||
if (
|
||||
player.recentPlay &&
|
||||
player.recentPlay < bsPlayerInfo.lastRefresh
|
||||
) {
|
||||
log.debug(
|
||||
`Beat Savior data for player "${playerId}" was refreshed after recent play, skipping`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolvePromiseOrWaitForPending(`refresh/${playerId}`, () => fetchPlayer(playerId, priority));
|
||||
return resolvePromiseOrWaitForPending(`refresh/${playerId}`, () =>
|
||||
fetchPlayer(playerId, priority),
|
||||
);
|
||||
} catch (e) {
|
||||
if (throwErrors) throw e;
|
||||
|
||||
log.debug(`Beat Savior data refreshing error${e.toString ? `: ${e.toString()}` : ''}`, 'BeatSaviorService', e)
|
||||
log.debug(
|
||||
`Beat Savior data refreshing error${
|
||||
e.toString ? `: ${e.toString()}` : ""
|
||||
}`,
|
||||
"BeatSaviorService",
|
||||
e,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
|
||||
log.trace(`Starting refreshing Beat Savior data for all players${force ? ' (forced)' : ''}...`, 'BeatSaviorService');
|
||||
const refreshAll = async (
|
||||
force = false,
|
||||
priority = PRIORITY.BG_NORMAL,
|
||||
throwErrors = false,
|
||||
) => {
|
||||
log.trace(
|
||||
`Starting refreshing Beat Savior data for all players${
|
||||
force ? " (forced)" : ""
|
||||
}...`,
|
||||
"BeatSaviorService",
|
||||
);
|
||||
|
||||
const allPlayers = await playerService.getAll();
|
||||
if (!allPlayers || !allPlayers.length) {
|
||||
log.trace(`No players in DB, skipping.`, 'BeatSaviorService');
|
||||
log.trace(`No players in DB, skipping.`, "BeatSaviorService");
|
||||
return null;
|
||||
}
|
||||
|
||||
const allRefreshed = await Promise.all(allPlayers.map(async player => ({
|
||||
const allRefreshed = await Promise.all(
|
||||
allPlayers.map(async (player) => ({
|
||||
playerId: player.playerId,
|
||||
beatSavior: await refresh(player.playerId, force, priority, throwErrors),
|
||||
})));
|
||||
beatSavior: await refresh(
|
||||
player.playerId,
|
||||
force,
|
||||
priority,
|
||||
throwErrors,
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
log.trace(`Beat Savior data for all players refreshed.`, 'BeatSaviorService', allRefreshed);
|
||||
log.trace(
|
||||
`Beat Savior data for all players refreshed.`,
|
||||
"BeatSaviorService",
|
||||
allRefreshed,
|
||||
);
|
||||
|
||||
return allRefreshed;
|
||||
}
|
||||
};
|
||||
|
||||
const get = async (playerId, score) => {
|
||||
if (score && score.beatSavior) return score.beatSavior;
|
||||
@ -303,12 +429,18 @@ export default () => {
|
||||
const playerBsData = await getPlayerScores(playerId);
|
||||
if (!playerBsData || !playerBsData.length) return null;
|
||||
|
||||
const bsData = playerBsData.find(bsData => isScoreMatchingBsData(score, bsData, true));
|
||||
const bsData = playerBsData.find((bsData) =>
|
||||
isScoreMatchingBsData(score, bsData, true),
|
||||
);
|
||||
|
||||
return bsData ? bsData : null;
|
||||
}
|
||||
};
|
||||
|
||||
const isDataForPlayerAvailable = async playerId => await beatSaviorRepository().getFromIndex('beat-savior-playerId', playerId) !== undefined;
|
||||
const isDataForPlayerAvailable = async (playerId) =>
|
||||
(await beatSaviorRepository().getFromIndex(
|
||||
"beat-savior-playerId",
|
||||
playerId,
|
||||
)) !== undefined;
|
||||
|
||||
const destroyService = () => {
|
||||
serviceCreationCount--;
|
||||
@ -319,7 +451,7 @@ export default () => {
|
||||
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
service = {
|
||||
fetchPlayer,
|
||||
@ -332,7 +464,7 @@ export default () => {
|
||||
isDataForPlayerAvailable,
|
||||
getScoresHistogramDefinition,
|
||||
destroyService,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import keyValueRepository from '../db/repository/key-value';
|
||||
import {opt} from '../utils/js'
|
||||
import keyValueRepository from "../db/repository/key-value";
|
||||
import { opt } from "../utils/js";
|
||||
|
||||
const STORE_CONFIG_KEY = 'config';
|
||||
const STORE_CONFIG_KEY = "config";
|
||||
|
||||
let service = null;
|
||||
|
||||
@ -9,22 +9,23 @@ export default () => {
|
||||
if (service) return service;
|
||||
|
||||
const get = async () => keyValueRepository().get(STORE_CONFIG_KEY);
|
||||
const set = async config => keyValueRepository().set(config, STORE_CONFIG_KEY);
|
||||
const set = async (config) =>
|
||||
keyValueRepository().set(config, STORE_CONFIG_KEY);
|
||||
|
||||
const getMainPlayerId = async () => {
|
||||
const config = await get();
|
||||
|
||||
return opt(config, 'users.main');
|
||||
}
|
||||
return opt(config, "users.main");
|
||||
};
|
||||
|
||||
const destroyService = () => {}
|
||||
const destroyService = () => {};
|
||||
|
||||
service = {
|
||||
get,
|
||||
set,
|
||||
getMainPlayerId,
|
||||
destroyService,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
@ -1,14 +1,14 @@
|
||||
import leaderboardPageClient from '../../network/clients/scoresaber/leaderboard/page-leaderboard'
|
||||
import accSaberLeaderboardApiClient from '../../network/clients/accsaber/api-leaderboard'
|
||||
import makePendingPromisePool from '../../utils/pending-promises'
|
||||
import createPlayersService from '../../services/scoresaber/player'
|
||||
import createScoresService from '../../services/scoresaber/scores'
|
||||
import {PRIORITY} from '../../network/queues/http-queue'
|
||||
import {LEADERBOARD_SCORES_PER_PAGE} from '../../utils/scoresaber/consts'
|
||||
import {LEADERBOARD_SCORES_PER_PAGE as ACCSABER_LEADERBOARD_SCORES_PER_PAGE} from '../../utils/accsaber/consts'
|
||||
import {formatDateRelative, MINUTE} from '../../utils/date'
|
||||
import {convertArrayToObjectByKey, opt} from '../../utils/js'
|
||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
||||
import leaderboardPageClient from "../../network/clients/scoresaber/leaderboard/page-leaderboard";
|
||||
import accSaberLeaderboardApiClient from "../../network/clients/accsaber/api-leaderboard";
|
||||
import makePendingPromisePool from "../../utils/pending-promises";
|
||||
import createPlayersService from "../../services/scoresaber/player";
|
||||
import createScoresService from "../../services/scoresaber/scores";
|
||||
import { PRIORITY } from "../../network/queues/http-queue";
|
||||
import { LEADERBOARD_SCORES_PER_PAGE } from "../../utils/scoresaber/consts";
|
||||
import { LEADERBOARD_SCORES_PER_PAGE as ACCSABER_LEADERBOARD_SCORES_PER_PAGE } from "../../utils/accsaber/consts";
|
||||
import { formatDateRelative, MINUTE } from "../../utils/date";
|
||||
import { convertArrayToObjectByKey, opt } from "../../utils/js";
|
||||
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||
|
||||
const ACCSABER_LEADERBOARD_NETWORK_TTL = MINUTE * 5;
|
||||
|
||||
@ -20,79 +20,118 @@ export default () => {
|
||||
const scoresService = createScoresService();
|
||||
|
||||
let friendsPromise = Promise.resolve([]);
|
||||
const refreshFriends = async () => friendsPromise = playersService.getAll();
|
||||
eventBus.on('player-profile-removed', playerId => refreshFriends());
|
||||
eventBus.on('player-profile-added', player => refreshFriends());
|
||||
eventBus.on('player-profile-changed', player => refreshFriends());
|
||||
refreshFriends().then(_ => {});
|
||||
const refreshFriends = async () => (friendsPromise = playersService.getAll());
|
||||
eventBus.on("player-profile-removed", (playerId) => refreshFriends());
|
||||
eventBus.on("player-profile-added", (player) => refreshFriends());
|
||||
eventBus.on("player-profile-changed", (player) => refreshFriends());
|
||||
refreshFriends().then((_) => {});
|
||||
|
||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||
|
||||
const fetchPage = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, signal = null, force = false) => resolvePromiseOrWaitForPending(
|
||||
const fetchPage = async (
|
||||
leaderboardId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
signal = null,
|
||||
force = false,
|
||||
) =>
|
||||
resolvePromiseOrWaitForPending(
|
||||
`pageClient/leaderboard/${leaderboardId}/${page}`,
|
||||
() => leaderboardPageClient.getProcessed({
|
||||
() =>
|
||||
leaderboardPageClient.getProcessed({
|
||||
leaderboardId,
|
||||
page,
|
||||
signal,
|
||||
priority,
|
||||
cacheTtl: MINUTE,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
const fetchAccSaberPage = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, signal = null, force = false) => {
|
||||
const fetchAccSaberPage = async (
|
||||
leaderboardId,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
signal = null,
|
||||
force = false,
|
||||
) => {
|
||||
if (page < 1) page = 1;
|
||||
|
||||
const data = await resolvePromiseOrWaitForPending(
|
||||
`accSaberApiClient/leaderboard/${leaderboardId}/${page}`,
|
||||
() => accSaberLeaderboardApiClient.getProcessed({
|
||||
() =>
|
||||
accSaberLeaderboardApiClient.getProcessed({
|
||||
leaderboardId,
|
||||
page,
|
||||
signal,
|
||||
priority,
|
||||
cacheTtl: ACCSABER_LEADERBOARD_NETWORK_TTL,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
if (!data || !data.scores) return data
|
||||
if (!data || !data.scores) return data;
|
||||
|
||||
const startIdx = (page - 1) * ACCSABER_LEADERBOARD_SCORES_PER_PAGE;
|
||||
if (data.scores.length < startIdx + 1) return data;
|
||||
|
||||
return {
|
||||
...data,
|
||||
scores: data.scores
|
||||
.slice(startIdx, startIdx + ACCSABER_LEADERBOARD_SCORES_PER_PAGE)
|
||||
}
|
||||
}
|
||||
scores: data.scores.slice(
|
||||
startIdx,
|
||||
startIdx + ACCSABER_LEADERBOARD_SCORES_PER_PAGE,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const getFriendsLeaderboard = async (leaderboardId, priority = PRIORITY.FG_LOW, signal = null) => {
|
||||
const leaderboard = await resolvePromiseOrWaitForPending(`pageClient/leaderboard/${leaderboardId}/1`, () => leaderboardPageClient.getProcessed({leaderboardId, page: 1, signal, priority, cacheTtl: MINUTE}));
|
||||
const getFriendsLeaderboard = async (
|
||||
leaderboardId,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
signal = null,
|
||||
) => {
|
||||
const leaderboard = await resolvePromiseOrWaitForPending(
|
||||
`pageClient/leaderboard/${leaderboardId}/1`,
|
||||
() =>
|
||||
leaderboardPageClient.getProcessed({
|
||||
leaderboardId,
|
||||
page: 1,
|
||||
signal,
|
||||
priority,
|
||||
cacheTtl: MINUTE,
|
||||
}),
|
||||
);
|
||||
|
||||
const friends = convertArrayToObjectByKey(await friendsPromise, 'playerId');
|
||||
const friends = convertArrayToObjectByKey(await friendsPromise, "playerId");
|
||||
|
||||
const scores = (await scoresService.getLeaderboardScores(leaderboardId))
|
||||
.map(score => {
|
||||
.map((score) => {
|
||||
if (!score || !score.playerId || !friends[score.playerId]) return null;
|
||||
|
||||
const player = friends[score.playerId];
|
||||
|
||||
return {
|
||||
player: {playerId: player.playerId, name: player.name, playerInfo: {...player.playerInfo}},
|
||||
score: {...score.score},
|
||||
}
|
||||
player: {
|
||||
playerId: player.playerId,
|
||||
name: player.name,
|
||||
playerInfo: { ...player.playerInfo },
|
||||
},
|
||||
score: { ...score.score },
|
||||
};
|
||||
})
|
||||
.filter(s => s)
|
||||
.sort((a, b) => opt(b, 'score.score', 0) - opt(a, 'score.score', 0))
|
||||
.filter((s) => s)
|
||||
.sort((a, b) => opt(b, "score.score", 0) - opt(a, "score.score", 0))
|
||||
.map((score, idx) => ({
|
||||
player: score.player,
|
||||
score: {...score.score, rank: idx + 1, timeSetString: formatDateRelative(score.score.timeSet)},
|
||||
}))
|
||||
;
|
||||
|
||||
return {...leaderboard, scores, pageQty: 1, totalItems: scores.length};
|
||||
}
|
||||
score: {
|
||||
...score.score,
|
||||
rank: idx + 1,
|
||||
timeSetString: formatDateRelative(score.score.timeSet),
|
||||
},
|
||||
}));
|
||||
return { ...leaderboard, scores, pageQty: 1, totalItems: scores.length };
|
||||
};
|
||||
|
||||
const destroyService = () => {
|
||||
service = null;
|
||||
}
|
||||
};
|
||||
|
||||
service = {
|
||||
fetchPage,
|
||||
@ -100,7 +139,7 @@ export default () => {
|
||||
getFriendsLeaderboard,
|
||||
LEADERBOARD_SCORES_PER_PAGE,
|
||||
destroyService,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
||||
import {configStore} from '../../stores/config'
|
||||
import playerApiClient from '../../network/clients/scoresaber/player/api'
|
||||
import playerFindApiClient from '../../network/clients/scoresaber/players/api-player-find'
|
||||
import playerPageClient from '../../network/clients/scoresaber/player/page'
|
||||
import {PRIORITY} from '../../network/queues/http-queue'
|
||||
import playersRepository from '../../db/repository/players'
|
||||
import playersHistoryRepository from '../../db/repository/players-history'
|
||||
import log from '../../utils/logger'
|
||||
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||
import { configStore } from "../../stores/config";
|
||||
import playerApiClient from "../../network/clients/scoresaber/player/api";
|
||||
import playerFindApiClient from "../../network/clients/scoresaber/players/api-player-find";
|
||||
import playerPageClient from "../../network/clients/scoresaber/player/page";
|
||||
import { PRIORITY } from "../../network/queues/http-queue";
|
||||
import playersRepository from "../../db/repository/players";
|
||||
import playersHistoryRepository from "../../db/repository/players-history";
|
||||
import log from "../../utils/logger";
|
||||
import {
|
||||
addToDate,
|
||||
formatDate,
|
||||
@ -14,12 +14,12 @@ import {
|
||||
SECOND,
|
||||
toSsMidnight,
|
||||
truncateDate,
|
||||
} from '../../utils/date'
|
||||
import {opt} from '../../utils/js'
|
||||
import {db} from '../../db/db'
|
||||
import makePendingPromisePool from '../../utils/pending-promises'
|
||||
import {worker} from '../../utils/worker-wrappers'
|
||||
import {getServicePlayerGain} from '../utils'
|
||||
} from "../../utils/date";
|
||||
import { opt } from "../../utils/js";
|
||||
import { db } from "../../db/db";
|
||||
import makePendingPromisePool from "../../utils/pending-promises";
|
||||
import { worker } from "../../utils/worker-wrappers";
|
||||
import { getServicePlayerGain } from "../utils";
|
||||
|
||||
const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 3;
|
||||
const PLAYER_REFRESH_INTERVAL = MINUTE * 20;
|
||||
@ -34,91 +34,128 @@ export default () => {
|
||||
|
||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||
|
||||
const configStoreUnsubscribe = configStore.subscribe(config => {
|
||||
const newMainPlayerId = opt(config, 'users.main')
|
||||
const configStoreUnsubscribe = configStore.subscribe((config) => {
|
||||
const newMainPlayerId = opt(config, "users.main");
|
||||
if (mainPlayerId !== newMainPlayerId) {
|
||||
mainPlayerId = newMainPlayerId;
|
||||
|
||||
log.debug(`Main player changed to ${mainPlayerId}`, 'PlayerService')
|
||||
log.debug(`Main player changed to ${mainPlayerId}`, "PlayerService");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const isMainPlayer = playerId => mainPlayerId && playerId === mainPlayerId;
|
||||
const isMainPlayer = (playerId) => mainPlayerId && playerId === mainPlayerId;
|
||||
|
||||
const getAll = async (force = false) => playersRepository().getAll(force);
|
||||
|
||||
// TODO: just for now
|
||||
const getFriends = async () => (await getAll()).filter(player => player && player.playerId && !isPlayerMain(player.playerId)).map(p => p.playerId);
|
||||
const getFriends = async () =>
|
||||
(await getAll())
|
||||
.filter(
|
||||
(player) => player && player.playerId && !isPlayerMain(player.playerId),
|
||||
)
|
||||
.map((p) => p.playerId);
|
||||
|
||||
const getAllActive = async () => {
|
||||
const players = await getAll();
|
||||
if (!players) return [];
|
||||
|
||||
return players.filter(player => player && player.playerInfo && !player.playerInfo.inactive && !player.playerInfo.banned);
|
||||
}
|
||||
return players.filter(
|
||||
(player) =>
|
||||
player &&
|
||||
player.playerInfo &&
|
||||
!player.playerInfo.inactive &&
|
||||
!player.playerInfo.banned,
|
||||
);
|
||||
};
|
||||
|
||||
const getPlayer = async playerId => await playersRepository().get(playerId);
|
||||
const getPlayer = async (playerId) => await playersRepository().get(playerId);
|
||||
|
||||
const removePlayer = async (playerId, purgeScores = false) => {
|
||||
await playersRepository().delete(playerId);
|
||||
|
||||
// TODO: purge scores if requested
|
||||
|
||||
eventBus.publish('player-profile-removed', playerId);
|
||||
}
|
||||
eventBus.publish("player-profile-removed", playerId);
|
||||
};
|
||||
|
||||
const addPlayer = async (playerId, priority = PRIORITY.FG_LOW) => {
|
||||
log.trace(`Starting to add a player "${playerId}"...`, 'PlayerService');
|
||||
log.trace(`Starting to add a player "${playerId}"...`, "PlayerService");
|
||||
|
||||
const player = await refresh(playerId, true, priority, false, true);
|
||||
if (!player) {
|
||||
log.warn(`Can not add player "${playerId}"`, 'PlayerService');
|
||||
log.warn(`Can not add player "${playerId}"`, "PlayerService");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
eventBus.publish('player-profile-added', player);
|
||||
eventBus.publish('player-profile-changed', player);
|
||||
eventBus.publish("player-profile-added", player);
|
||||
eventBus.publish("player-profile-changed", player);
|
||||
|
||||
log.trace(`Player "${playerId}" added.`, 'PlayerService')
|
||||
log.trace(`Player "${playerId}" added.`, "PlayerService");
|
||||
|
||||
return player;
|
||||
}
|
||||
};
|
||||
|
||||
const setPlayer = async (player) => {
|
||||
await playersRepository().set(player);
|
||||
|
||||
eventBus.publish('player-profile-changed', player);
|
||||
eventBus.publish("player-profile-changed", player);
|
||||
|
||||
return player;
|
||||
}
|
||||
};
|
||||
|
||||
const updatePlayer = async (player, waitForSaving = true, forceAdd = false) => {
|
||||
const updatePlayer = async (
|
||||
player,
|
||||
waitForSaving = true,
|
||||
forceAdd = false,
|
||||
) => {
|
||||
if (!player || !player.playerId) {
|
||||
log.warn(`Can not update player, empty playerId`, 'PlayerService', player)
|
||||
log.warn(
|
||||
`Can not update player, empty playerId`,
|
||||
"PlayerService",
|
||||
player,
|
||||
);
|
||||
}
|
||||
|
||||
const dbPlayer = await getPlayer(player.playerId);
|
||||
if (!dbPlayer && !forceAdd) return player;
|
||||
|
||||
const finalPlayer = {...dbPlayer, ...player}
|
||||
const finalPlayer = { ...dbPlayer, ...player };
|
||||
|
||||
if (!waitForSaving) {
|
||||
setPlayer(finalPlayer).then(_ => _)
|
||||
setPlayer(finalPlayer).then((_) => _);
|
||||
|
||||
return finalPlayer;
|
||||
}
|
||||
|
||||
return await setPlayer(finalPlayer);
|
||||
}
|
||||
};
|
||||
|
||||
const getPlayerHistory = async playerId => resolvePromiseOrWaitForPending(`playerHistory/${playerId}`, () => playersHistoryRepository().getAllFromIndex('players-history-playerId', playerId))
|
||||
const getPlayerHistory = async (playerId) =>
|
||||
resolvePromiseOrWaitForPending(`playerHistory/${playerId}`, () =>
|
||||
playersHistoryRepository().getAllFromIndex(
|
||||
"players-history-playerId",
|
||||
playerId,
|
||||
),
|
||||
);
|
||||
|
||||
const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) => getServicePlayerGain(playerHistory, toSsMidnight, 'ssDate', daysAgo, maxDaysAgo);
|
||||
const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) =>
|
||||
getServicePlayerGain(
|
||||
playerHistory,
|
||||
toSsMidnight,
|
||||
"ssDate",
|
||||
daysAgo,
|
||||
maxDaysAgo,
|
||||
);
|
||||
|
||||
const updatePlayerHistory = async player => {
|
||||
const updatePlayerHistory = async (player) => {
|
||||
if (!player) return null;
|
||||
const {playerId, profileLastUpdated, playerInfo: {banned, countries, inactive, pp, rank}, scoreStats} = player;
|
||||
const {
|
||||
playerId,
|
||||
profileLastUpdated,
|
||||
playerInfo: { banned, countries, inactive, pp, rank },
|
||||
scoreStats,
|
||||
} = player;
|
||||
|
||||
if (!playerId) return null;
|
||||
|
||||
@ -130,65 +167,87 @@ export default () => {
|
||||
const playerIdLocalTimestamp = `${playerId}_${localDate.getTime()}`;
|
||||
const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`;
|
||||
|
||||
return playersHistoryRepository().getFromIndex('players-history-playerIdSsTimestamp', playerIdSsTimestamp)
|
||||
.then(async ph => {
|
||||
return playersHistoryRepository()
|
||||
.getFromIndex("players-history-playerIdSsTimestamp", playerIdSsTimestamp)
|
||||
.then(async (ph) => {
|
||||
if (ph && ph._idbId) {
|
||||
await playersHistoryRepository().delete(ph._idbId);
|
||||
|
||||
const {_idbId, ...previous} = ph;
|
||||
const { _idbId, ...previous } = ph;
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.then(async previous => {
|
||||
.then(async (previous) => {
|
||||
let accStats = {};
|
||||
|
||||
if (worker) {
|
||||
const stats = await worker.calcPlayerStats(playerId);
|
||||
|
||||
const ppBoundary = await worker.calcPpBoundary(playerId) ?? null;
|
||||
const ppBoundary = (await worker.calcPpBoundary(playerId)) ?? null;
|
||||
|
||||
const {badges, totalScore, playCount, ...playerStats} = stats ?? {};
|
||||
const { badges, totalScore, playCount, ...playerStats } = stats ?? {};
|
||||
|
||||
accStats = {...playerStats}
|
||||
accStats = { ...playerStats };
|
||||
|
||||
if (ppBoundary) accStats.ppBoundary = ppBoundary;
|
||||
if (badges?.length) accStats.accBadges = badges.reduce((cum, b) => ({...cum, [b.label]: b.value}), {});
|
||||
if (badges?.length)
|
||||
accStats.accBadges = badges.reduce(
|
||||
(cum, b) => ({ ...cum, [b.label]: b.value }),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
return playersHistoryRepository().set({
|
||||
...previous,
|
||||
...accStats,
|
||||
playerId, banned, countries, inactive, pp, rank, ...scoreStats,
|
||||
localDate, ssDate,
|
||||
playerId,
|
||||
banned,
|
||||
countries,
|
||||
inactive,
|
||||
pp,
|
||||
rank,
|
||||
...scoreStats,
|
||||
localDate,
|
||||
ssDate,
|
||||
playerIdLocalTimestamp,
|
||||
playerIdSsTimestamp,
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch(err => {}) // swallow error
|
||||
}
|
||||
.catch((err) => {}); // swallow error
|
||||
};
|
||||
|
||||
const isPlayerMain = playerId => playerId === mainPlayerId;
|
||||
const isPlayerMain = (playerId) => playerId === mainPlayerId;
|
||||
|
||||
const getProfileFreshnessDate = (player, refreshInterval = null) => {
|
||||
const lastUpdated = player && player.profileLastUpdated ? player.profileLastUpdated : null;
|
||||
const lastUpdated =
|
||||
player && player.profileLastUpdated ? player.profileLastUpdated : null;
|
||||
if (!lastUpdated) return addToDate(-SECOND);
|
||||
|
||||
const REFRESH_INTERVAL = refreshInterval ? refreshInterval : (isPlayerMain(player.playerId) ? MAIN_PLAYER_REFRESH_INTERVAL : PLAYER_REFRESH_INTERVAL);
|
||||
const REFRESH_INTERVAL = refreshInterval
|
||||
? refreshInterval
|
||||
: isPlayerMain(player.playerId)
|
||||
? MAIN_PLAYER_REFRESH_INTERVAL
|
||||
: PLAYER_REFRESH_INTERVAL;
|
||||
|
||||
return addToDate(REFRESH_INTERVAL, lastUpdated);
|
||||
}
|
||||
};
|
||||
|
||||
const isProfileFresh = (player, refreshInterval = null) => getProfileFreshnessDate(player, refreshInterval) > new Date();
|
||||
const isProfileFresh = (player, refreshInterval = null) =>
|
||||
getProfileFreshnessDate(player, refreshInterval) > new Date();
|
||||
|
||||
const updatePlayerRecentPlay = async (playerId, recentPlay, recentPlayLastUpdated = new Date()) => {
|
||||
const updatePlayerRecentPlay = async (
|
||||
playerId,
|
||||
recentPlay,
|
||||
recentPlayLastUpdated = new Date(),
|
||||
) => {
|
||||
let player;
|
||||
|
||||
try {
|
||||
await db.runInTransaction(['players'], async tx => {
|
||||
const playersStore = tx.objectStore('players')
|
||||
await db.runInTransaction(["players"], async (tx) => {
|
||||
const playersStore = tx.objectStore("players");
|
||||
player = await playersStore.get(playerId);
|
||||
if (player) {
|
||||
player.recentPlayLastUpdated = recentPlayLastUpdated;
|
||||
@ -200,61 +259,135 @@ export default () => {
|
||||
|
||||
if (player) {
|
||||
playersRepository().addToCache([player]);
|
||||
eventBus.publish('player-profile-changed', player);
|
||||
eventBus.publish("player-profile-changed", player);
|
||||
|
||||
eventBus.publish('player-recent-play-updated', {playerId, player, recentPlay, recentPlayLastUpdated});
|
||||
eventBus.publish("player-recent-play-updated", {
|
||||
playerId,
|
||||
player,
|
||||
recentPlay,
|
||||
recentPlayLastUpdated,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPlayerAndUpdateRecentPlay = async playerId => {
|
||||
try {
|
||||
const player = await resolvePromiseOrWaitForPending(`pageClient/${playerId}`, () =>playerPageClient.getProcessed({playerId}));
|
||||
const recentPlay = opt(player, 'playerInfo.recentPlay');
|
||||
const recentPlayLastUpdated = opt(player, 'playerInfo.recentPlayLastUpdated');
|
||||
if (!recentPlay || !recentPlayLastUpdated) return null;
|
||||
|
||||
return updatePlayerRecentPlay(playerId, recentPlay, recentPlayLastUpdated);
|
||||
} catch (err) {
|
||||
// swallow error
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPlayerAndUpdateRecentPlay = async (playerId) => {
|
||||
try {
|
||||
const player = await resolvePromiseOrWaitForPending(
|
||||
`pageClient/${playerId}`,
|
||||
() => playerPageClient.getProcessed({ playerId }),
|
||||
);
|
||||
const recentPlay = opt(player, "playerInfo.recentPlay");
|
||||
const recentPlayLastUpdated = opt(
|
||||
player,
|
||||
"playerInfo.recentPlayLastUpdated",
|
||||
);
|
||||
if (!recentPlay || !recentPlayLastUpdated) return null;
|
||||
|
||||
return updatePlayerRecentPlay(
|
||||
playerId,
|
||||
recentPlay,
|
||||
recentPlayLastUpdated,
|
||||
);
|
||||
} catch (err) {
|
||||
// swallow error
|
||||
}
|
||||
};
|
||||
|
||||
const isResponseCached = response => playerApiClient.isResponseCached(response);
|
||||
const getDataFromResponse = response => playerApiClient.getDataFromResponse(response);
|
||||
const isResponseCached = (response) =>
|
||||
playerApiClient.isResponseCached(response);
|
||||
const getDataFromResponse = (response) =>
|
||||
playerApiClient.getDataFromResponse(response);
|
||||
|
||||
const fetchPlayer = async (playerId, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => resolvePromiseOrWaitForPending(`apiClient/${playerId}/${fullResponse}`, () => playerApiClient.getProcessed({...options, playerId, priority, fullResponse}));
|
||||
const fetchPlayer = async (
|
||||
playerId,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
{ fullResponse = false, ...options } = {},
|
||||
) =>
|
||||
resolvePromiseOrWaitForPending(
|
||||
`apiClient/${playerId}/${fullResponse}`,
|
||||
() =>
|
||||
playerApiClient.getProcessed({
|
||||
...options,
|
||||
playerId,
|
||||
priority,
|
||||
fullResponse,
|
||||
}),
|
||||
);
|
||||
|
||||
const findPlayer = async (query, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => resolvePromiseOrWaitForPending(`apiClient/find/${query}/${fullResponse}`, () => playerFindApiClient.getProcessed({...options, query, priority, fullResponse}));
|
||||
const findPlayer = async (
|
||||
query,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
{ fullResponse = false, ...options } = {},
|
||||
) =>
|
||||
resolvePromiseOrWaitForPending(
|
||||
`apiClient/find/${query}/${fullResponse}`,
|
||||
() =>
|
||||
playerFindApiClient.getProcessed({
|
||||
...options,
|
||||
query,
|
||||
priority,
|
||||
fullResponse,
|
||||
}),
|
||||
);
|
||||
|
||||
const fetchPlayerOrGetFromCache = async (playerId, refreshInterval = MINUTE, priority = PRIORITY.FG_LOW, signal = null, force = false) => {
|
||||
const fetchPlayerOrGetFromCache = async (
|
||||
playerId,
|
||||
refreshInterval = MINUTE,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
signal = null,
|
||||
force = false,
|
||||
) => {
|
||||
const player = await getPlayer(playerId);
|
||||
|
||||
if (!player || !isProfileFresh(player, refreshInterval)) {
|
||||
const fetchedPlayerResponse = await fetchPlayer(playerId, priority, {signal, cacheTtl: MINUTE, maxAge: force ? 0 : refreshInterval, fullResponse: true});
|
||||
if (isResponseCached(fetchedPlayerResponse)) return getDataFromResponse(fetchedPlayerResponse);
|
||||
const fetchedPlayerResponse = await fetchPlayer(playerId, priority, {
|
||||
signal,
|
||||
cacheTtl: MINUTE,
|
||||
maxAge: force ? 0 : refreshInterval,
|
||||
fullResponse: true,
|
||||
});
|
||||
if (isResponseCached(fetchedPlayerResponse))
|
||||
return getDataFromResponse(fetchedPlayerResponse);
|
||||
|
||||
return updatePlayer({...player, ...getDataFromResponse(fetchedPlayerResponse), profileLastUpdated: new Date()}, false)
|
||||
.then(player => {
|
||||
return updatePlayer(
|
||||
{
|
||||
...player,
|
||||
...getDataFromResponse(fetchedPlayerResponse),
|
||||
profileLastUpdated: new Date(),
|
||||
},
|
||||
false,
|
||||
).then((player) => {
|
||||
fetchPlayerAndUpdateRecentPlay(player.playerId);
|
||||
|
||||
updatePlayerHistory(player);
|
||||
|
||||
return player;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = async (playerId, force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false, addIfNotExists = false) => {
|
||||
log.trace(`Starting refreshing player "${playerId}" ${force ? ' (forced)' : ''}...`, 'PlayerService')
|
||||
const refresh = async (
|
||||
playerId,
|
||||
force = false,
|
||||
priority = PRIORITY.BG_NORMAL,
|
||||
throwErrors = false,
|
||||
addIfNotExists = false,
|
||||
) => {
|
||||
log.trace(
|
||||
`Starting refreshing player "${playerId}" ${force ? " (forced)" : ""}...`,
|
||||
"PlayerService",
|
||||
);
|
||||
|
||||
if (!playerId) {
|
||||
log.warn(`Can not refresh player if an empty playerId is given`, 'PlayerService');
|
||||
log.warn(
|
||||
`Can not refresh player if an empty playerId is given`,
|
||||
"PlayerService",
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -262,66 +395,101 @@ export default () => {
|
||||
try {
|
||||
let player = await getPlayer(playerId);
|
||||
if (!player && !addIfNotExists) {
|
||||
log.debug(`Profile is not added to DB, skipping.`, 'PlayerService')
|
||||
log.debug(`Profile is not added to DB, skipping.`, "PlayerService");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
log.trace(`Player fetched from DB`, 'PlayerService', player);
|
||||
log.trace(`Player fetched from DB`, "PlayerService", player);
|
||||
|
||||
if (!force) {
|
||||
const profileFreshnessDate = getProfileFreshnessDate(player);
|
||||
if (profileFreshnessDate > new Date()) {
|
||||
|
||||
log.debug(`Profile is still fresh, skipping. Next refresh on ${formatDate(profileFreshnessDate)}`, 'PlayerService')
|
||||
log.debug(
|
||||
`Profile is still fresh, skipping. Next refresh on ${formatDate(
|
||||
profileFreshnessDate,
|
||||
)}`,
|
||||
"PlayerService",
|
||||
);
|
||||
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
log.trace(`Fetching player ${playerId} from ScoreSaber...`, 'PlayerService')
|
||||
log.trace(
|
||||
`Fetching player ${playerId} from ScoreSaber...`,
|
||||
"PlayerService",
|
||||
);
|
||||
|
||||
const fetchedPlayer = await fetchPlayer(playerId, priority);
|
||||
|
||||
if (!fetchedPlayer || !fetchedPlayer.playerId || !fetchedPlayer.name || !fetchedPlayer.playerInfo || !fetchedPlayer.scoreStats) {
|
||||
log.warn(`ScoreSaber returned empty info for player ${playerId}`, 'PlayerService')
|
||||
if (
|
||||
!fetchedPlayer ||
|
||||
!fetchedPlayer.playerId ||
|
||||
!fetchedPlayer.name ||
|
||||
!fetchedPlayer.playerInfo ||
|
||||
!fetchedPlayer.scoreStats
|
||||
) {
|
||||
log.warn(
|
||||
`ScoreSaber returned empty info for player ${playerId}`,
|
||||
"PlayerService",
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
log.trace(`Player fetched`, 'PlayerService', fetchedPlayer);
|
||||
log.trace(`Player fetched`, "PlayerService", fetchedPlayer);
|
||||
|
||||
player = await updatePlayer({...fetchedPlayer, profileLastUpdated: new Date()}, true, addIfNotExists);
|
||||
player = await updatePlayer(
|
||||
{ ...fetchedPlayer, profileLastUpdated: new Date() },
|
||||
true,
|
||||
addIfNotExists,
|
||||
);
|
||||
|
||||
updatePlayerHistory(player).then(_ => _);
|
||||
updatePlayerHistory(player).then((_) => _);
|
||||
|
||||
log.debug(`Player refreshed.`, 'PlayerService', player);
|
||||
log.debug(`Player refreshed.`, "PlayerService", player);
|
||||
|
||||
return player;
|
||||
} catch (e) {
|
||||
if (throwErrors) throw e;
|
||||
|
||||
log.debug(`Player refreshing error${e.toString ? `: ${e.toString()}` : ''}`, 'PlayerService', e)
|
||||
log.debug(
|
||||
`Player refreshing error${e.toString ? `: ${e.toString()}` : ""}`,
|
||||
"PlayerService",
|
||||
e,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => {
|
||||
log.trace(`Starting refreshing all players${force ? ' (forced)' : ''}...`, 'PlayerService');
|
||||
const refreshAll = async (
|
||||
force = false,
|
||||
priority = PRIORITY.BG_NORMAL,
|
||||
throwErrors = false,
|
||||
) => {
|
||||
log.trace(
|
||||
`Starting refreshing all players${force ? " (forced)" : ""}...`,
|
||||
"PlayerService",
|
||||
);
|
||||
|
||||
const allPlayers = await getAll();
|
||||
if (!allPlayers || !allPlayers.length) {
|
||||
log.trace(`No players in DB, skipping.`, 'PlayerService');
|
||||
log.trace(`No players in DB, skipping.`, "PlayerService");
|
||||
return null;
|
||||
}
|
||||
|
||||
const allRefreshed = await Promise.all(allPlayers.map(player => refresh(player.playerId, force, priority, throwErrors)));
|
||||
const allRefreshed = await Promise.all(
|
||||
allPlayers.map((player) =>
|
||||
refresh(player.playerId, force, priority, throwErrors),
|
||||
),
|
||||
);
|
||||
|
||||
log.trace(`All players refreshed.`, 'PlayerService', allRefreshed);
|
||||
log.trace(`All players refreshed.`, "PlayerService", allRefreshed);
|
||||
|
||||
return allRefreshed;
|
||||
}
|
||||
};
|
||||
|
||||
const destroyService = () => {
|
||||
serviceCreationCount--;
|
||||
@ -331,7 +499,7 @@ export default () => {
|
||||
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
service = {
|
||||
isMainPlayer,
|
||||
@ -356,7 +524,7 @@ export default () => {
|
||||
destroyService,
|
||||
isResponseCached,
|
||||
getDataFromResponse,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import createScoresService from './scores'
|
||||
import makePendingPromisePool from '../../utils/pending-promises'
|
||||
import {getTotalPpFromSortedPps} from '../../utils/scoresaber/pp'
|
||||
import createScoresService from "./scores";
|
||||
import makePendingPromisePool from "../../utils/pending-promises";
|
||||
import { getTotalPpFromSortedPps } from "../../utils/scoresaber/pp";
|
||||
|
||||
let service = null;
|
||||
let serviceCreationCount = 0;
|
||||
@ -12,18 +12,22 @@ export default () => {
|
||||
|
||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||
|
||||
const getTotalPp = scores => scores && Array.isArray(scores)
|
||||
const getTotalPp = (scores) =>
|
||||
scores && Array.isArray(scores)
|
||||
? getTotalPpFromSortedPps(
|
||||
scores
|
||||
.filter(s => s.pp > 0)
|
||||
.map(s => s.pp)
|
||||
.filter((s) => s.pp > 0)
|
||||
.map((s) => s.pp)
|
||||
.sort((a, b) => b - a),
|
||||
)
|
||||
: null;
|
||||
|
||||
const getTotalPlayerPp = async (playerId, modifiedScores = {}) => getTotalPp(
|
||||
const getTotalPlayerPp = async (playerId, modifiedScores = {}) =>
|
||||
getTotalPp(
|
||||
Object.values({
|
||||
...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () => scoresService.getPlayerScoresAsObject(playerId))),
|
||||
...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () =>
|
||||
scoresService.getPlayerScoresAsObject(playerId),
|
||||
)),
|
||||
...modifiedScores,
|
||||
}),
|
||||
);
|
||||
@ -33,7 +37,7 @@ export default () => {
|
||||
if (!currentTotalPp) return null;
|
||||
|
||||
const newTotalPp = await getTotalPlayerPp(playerId, {
|
||||
[leaderboardId]: {pp},
|
||||
[leaderboardId]: { pp },
|
||||
});
|
||||
|
||||
return {
|
||||
@ -67,7 +71,7 @@ export default () => {
|
||||
if (!acc || acc <= 0) {
|
||||
return 0;
|
||||
}
|
||||
let index = ppCurve.findIndex(o => o.at >= acc);
|
||||
let index = ppCurve.findIndex((o) => o.at >= acc);
|
||||
if (index === -1) {
|
||||
return ppCurve[ppCurve.length - 1].value;
|
||||
}
|
||||
@ -83,7 +87,7 @@ export default () => {
|
||||
function accFromPpFactor(ppFactor) {
|
||||
if (!ppFactor || ppFactor <= 0) return 0;
|
||||
|
||||
const idx = ppCurve.findIndex(o => o.value >= ppFactor);
|
||||
const idx = ppCurve.findIndex((o) => o.value >= ppFactor);
|
||||
if (idx < 0) return ppCurve[ppCurve.length - 1].at;
|
||||
|
||||
const from = ppCurve[idx - 1];
|
||||
@ -101,7 +105,7 @@ export default () => {
|
||||
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
service = {
|
||||
getWhatIfScore,
|
||||
@ -111,7 +115,7 @@ export default () => {
|
||||
accFromPpFactor,
|
||||
PP_PER_STAR,
|
||||
destroyService,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
@ -1,13 +1,17 @@
|
||||
import {db} from '../../db/db'
|
||||
import queues from '../../network/queues/queues';
|
||||
import rankedsPageClient from '../../network/clients/scoresaber/rankeds/page';
|
||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
||||
import {arrayDifference, convertArrayToObjectByKey, opt} from '../../utils/js'
|
||||
import rankedsRepository from '../../db/repository/rankeds'
|
||||
import rankedsChangesRepository from '../../db/repository/rankeds-changes'
|
||||
import keyValueRepository from '../../db/repository/key-value'
|
||||
import log from '../../utils/logger'
|
||||
import {addToDate, formatDate, HOUR} from '../../utils/date'
|
||||
import { db } from "../../db/db";
|
||||
import queues from "../../network/queues/queues";
|
||||
import rankedsPageClient from "../../network/clients/scoresaber/rankeds/page";
|
||||
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||
import {
|
||||
arrayDifference,
|
||||
convertArrayToObjectByKey,
|
||||
opt,
|
||||
} from "../../utils/js";
|
||||
import rankedsRepository from "../../db/repository/rankeds";
|
||||
import rankedsChangesRepository from "../../db/repository/rankeds-changes";
|
||||
import keyValueRepository from "../../db/repository/key-value";
|
||||
import log from "../../utils/logger";
|
||||
import { addToDate, formatDate, HOUR } from "../../utils/date";
|
||||
|
||||
const REFRESH_INTERVAL = HOUR;
|
||||
|
||||
@ -16,16 +20,27 @@ export default () => {
|
||||
if (service) return service;
|
||||
|
||||
const getRankeds = async () => {
|
||||
const dbRankeds = await rankedsRepository().getAll()
|
||||
const dbRankeds = await rankedsRepository().getAll();
|
||||
|
||||
return dbRankeds ? convertArrayToObjectByKey(dbRankeds, 'leaderboardId') : {}
|
||||
}
|
||||
return dbRankeds
|
||||
? convertArrayToObjectByKey(dbRankeds, "leaderboardId")
|
||||
: {};
|
||||
};
|
||||
|
||||
const getLastUpdated = async () => keyValueRepository().get('rankedsLastUpdated');
|
||||
const setLastUpdated = async date => keyValueRepository().set(date, 'rankedsLastUpdated');
|
||||
const getLastUpdated = async () =>
|
||||
keyValueRepository().get("rankedsLastUpdated");
|
||||
const setLastUpdated = async (date) =>
|
||||
keyValueRepository().set(date, "rankedsLastUpdated");
|
||||
|
||||
const refreshRankeds = async (forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => {
|
||||
log.trace(`Starting rankeds refreshing${forceUpdate ? ' (forced)' : ''}...`, 'RankedsService')
|
||||
const refreshRankeds = async (
|
||||
forceUpdate = false,
|
||||
priority = queues.PRIORITY.BG_NORMAL,
|
||||
throwErrors = false,
|
||||
) => {
|
||||
log.trace(
|
||||
`Starting rankeds refreshing${forceUpdate ? " (forced)" : ""}...`,
|
||||
"RankedsService",
|
||||
);
|
||||
|
||||
try {
|
||||
let fetchedRankedSongs;
|
||||
@ -33,40 +48,50 @@ export default () => {
|
||||
if (!forceUpdate) {
|
||||
const lastUpdated = await getLastUpdated();
|
||||
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
|
||||
log.debug(`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'RankedsService')
|
||||
log.debug(
|
||||
`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(
|
||||
addToDate(REFRESH_INTERVAL, lastUpdated),
|
||||
)}`,
|
||||
"RankedsService",
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
log.trace(`Fetching current rankeds from ScoreSaber...`, 'RankedsService')
|
||||
fetchedRankedSongs = await rankedsPageClient.getProcessed({priority});
|
||||
log.trace(
|
||||
`Fetching current rankeds from ScoreSaber...`,
|
||||
"RankedsService",
|
||||
);
|
||||
fetchedRankedSongs = await rankedsPageClient.getProcessed({ priority });
|
||||
if (!fetchedRankedSongs || !fetchedRankedSongs.length) {
|
||||
log.warn(`ScoreSaber returned empty rankeds list`, 'RankedsService')
|
||||
log.warn(`ScoreSaber returned empty rankeds list`, "RankedsService");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
log.trace('Fetching rankeds from DB', 'RankedsService');
|
||||
log.trace("Fetching rankeds from DB", "RankedsService");
|
||||
const oldRankedSongs = await getRankeds();
|
||||
|
||||
// add firstSeen & oldStars properties
|
||||
fetchedRankedSongs = convertArrayToObjectByKey(
|
||||
fetchedRankedSongs.map(s => {
|
||||
const firstSeen = oldRankedSongs[s.leaderboardId] && oldRankedSongs[s.leaderboardId].firstSeen
|
||||
fetchedRankedSongs.map((s) => {
|
||||
const firstSeen =
|
||||
oldRankedSongs[s.leaderboardId] &&
|
||||
oldRankedSongs[s.leaderboardId].firstSeen
|
||||
? oldRankedSongs[s.leaderboardId].firstSeen
|
||||
: new Date();
|
||||
|
||||
return {...s, firstSeen, oldStars: null}
|
||||
return { ...s, firstSeen, oldStars: null };
|
||||
}),
|
||||
'leaderboardId',
|
||||
"leaderboardId",
|
||||
);
|
||||
|
||||
// find differences between old and new ranked songs
|
||||
const newRankeds = arrayDifference(
|
||||
Object.keys(fetchedRankedSongs),
|
||||
Object.keys(oldRankedSongs),
|
||||
).map(leaderboardId => ({
|
||||
).map((leaderboardId) => ({
|
||||
leaderboardId: parseInt(leaderboardId, 10),
|
||||
oldStars: null,
|
||||
stars: fetchedRankedSongs[leaderboardId].stars,
|
||||
@ -74,77 +99,99 @@ export default () => {
|
||||
}));
|
||||
|
||||
if (newRankeds && newRankeds.length)
|
||||
log.debug(`${newRankeds.length} ranked(s) found`, 'RankedsService');
|
||||
log.debug(`${newRankeds.length} ranked(s) found`, "RankedsService");
|
||||
|
||||
const changed =
|
||||
// concat new rankeds with changed rankeds
|
||||
newRankeds
|
||||
.concat(
|
||||
newRankeds.concat(
|
||||
Object.values(oldRankedSongs)
|
||||
.filter(s => s.stars !== (fetchedRankedSongs[s.leaderboardId] ? opt(fetchedRankedSongs[s.leaderboardId], 'stars', null) : null))
|
||||
.map(s => ({
|
||||
.filter(
|
||||
(s) =>
|
||||
s.stars !==
|
||||
(fetchedRankedSongs[s.leaderboardId]
|
||||
? opt(fetchedRankedSongs[s.leaderboardId], "stars", null)
|
||||
: null),
|
||||
)
|
||||
.map((s) => ({
|
||||
leaderboardId: s.leaderboardId,
|
||||
oldStars: s.stars,
|
||||
stars: opt(fetchedRankedSongs[s.leaderboardId], 'stars', null),
|
||||
stars: opt(fetchedRankedSongs[s.leaderboardId], "stars", null),
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
)
|
||||
})),
|
||||
);
|
||||
|
||||
if(newRankeds && changed && changed.length - newRankeds.length > 0)
|
||||
log.debug(`${changed.length - newRankeds.length} changed ranked(s) found`, 'RankedsService');
|
||||
if (newRankeds && changed && changed.length - newRankeds.length > 0)
|
||||
log.debug(
|
||||
`${changed.length - newRankeds.length} changed ranked(s) found`,
|
||||
"RankedsService",
|
||||
);
|
||||
|
||||
const changedLeaderboards = changed
|
||||
.map(s => {
|
||||
const ranked = fetchedRankedSongs[s.leaderboardId] ? fetchedRankedSongs[s.leaderboardId] : oldRankedSongs[s.leaderboardId];
|
||||
.map((s) => {
|
||||
const ranked = fetchedRankedSongs[s.leaderboardId]
|
||||
? fetchedRankedSongs[s.leaderboardId]
|
||||
: oldRankedSongs[s.leaderboardId];
|
||||
|
||||
return {
|
||||
...ranked,
|
||||
...s,
|
||||
}
|
||||
},
|
||||
)
|
||||
.filter(s => s && s.hash)
|
||||
.map(l => {
|
||||
const {oldStars, timestamp, ...leaderboard} = l;
|
||||
};
|
||||
})
|
||||
.filter((s) => s && s.hash)
|
||||
.map((l) => {
|
||||
const { oldStars, timestamp, ...leaderboard } = l;
|
||||
return leaderboard;
|
||||
});
|
||||
|
||||
log.trace('Saving rankeds to DB...', 'RankedsService');
|
||||
log.trace("Saving rankeds to DB...", "RankedsService");
|
||||
|
||||
await db.runInTransaction(['rankeds', 'rankeds-changes', 'key-value'], async tx => {
|
||||
await Promise.all(changedLeaderboards.map(async ranked => rankedsRepository().set(ranked, undefined, tx)));
|
||||
await Promise.all(changed.map(async rc => rankedsChangesRepository().set(rc, undefined, tx)));
|
||||
await setLastUpdated(new Date())
|
||||
});
|
||||
await db.runInTransaction(
|
||||
["rankeds", "rankeds-changes", "key-value"],
|
||||
async (tx) => {
|
||||
await Promise.all(
|
||||
changedLeaderboards.map(async (ranked) =>
|
||||
rankedsRepository().set(ranked, undefined, tx),
|
||||
),
|
||||
);
|
||||
await Promise.all(
|
||||
changed.map(async (rc) =>
|
||||
rankedsChangesRepository().set(rc, undefined, tx),
|
||||
),
|
||||
);
|
||||
await setLastUpdated(new Date());
|
||||
},
|
||||
);
|
||||
|
||||
log.trace('Rankeds saved', 'RankedsService');
|
||||
log.trace("Rankeds saved", "RankedsService");
|
||||
|
||||
if (changed.length) {
|
||||
eventBus.publish('rankeds-changed', {changed, allRankeds: fetchedRankedSongs});
|
||||
eventBus.publish("rankeds-changed", {
|
||||
changed,
|
||||
allRankeds: fetchedRankedSongs,
|
||||
});
|
||||
}
|
||||
|
||||
log.debug(`Rankeds refreshing complete.`, 'RankedsService')
|
||||
log.debug(`Rankeds refreshing complete.`, "RankedsService");
|
||||
|
||||
return changed;
|
||||
} catch (e) {
|
||||
if (throwErrors) throw e;
|
||||
|
||||
log.debug(`Rankeds refreshing error`, 'RankedsService', e)
|
||||
log.debug(`Rankeds refreshing error`, "RankedsService", e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const destroyService = () => {
|
||||
service = null;
|
||||
}
|
||||
};
|
||||
|
||||
service = {
|
||||
get: getRankeds,
|
||||
refresh: refreshRankeds,
|
||||
destroyService,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import playersGlobalRankingApiClient from '../../network/clients/scoresaber/players/api-ranking-global'
|
||||
import playersGlobalRankingPagesApiClient from '../../network/clients/scoresaber/players/api-ranking-global-pages'
|
||||
import playersCountryRankingPageClient from '../../network/clients/scoresaber/players/page-ranking-country'
|
||||
import makePendingPromisePool from '../../utils/pending-promises'
|
||||
import {PRIORITY} from '../../network/queues/http-queue'
|
||||
import {PLAYERS_PER_PAGE} from '../../utils/scoresaber/consts'
|
||||
import {opt} from '../../utils/js'
|
||||
import playersGlobalRankingApiClient from "../../network/clients/scoresaber/players/api-ranking-global";
|
||||
import playersGlobalRankingPagesApiClient from "../../network/clients/scoresaber/players/api-ranking-global-pages";
|
||||
import playersCountryRankingPageClient from "../../network/clients/scoresaber/players/page-ranking-country";
|
||||
import makePendingPromisePool from "../../utils/pending-promises";
|
||||
import { PRIORITY } from "../../network/queues/http-queue";
|
||||
import { PLAYERS_PER_PAGE } from "../../utils/scoresaber/consts";
|
||||
import { opt } from "../../utils/js";
|
||||
|
||||
let service = null;
|
||||
export default () => {
|
||||
@ -12,24 +12,52 @@ export default () => {
|
||||
|
||||
const resolvePromiseOrWaitForPending = makePendingPromisePool();
|
||||
|
||||
const fetchGlobal = async (page = 1, priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`apiClient/ranking/global/${page}`, () => playersGlobalRankingApiClient.getProcessed({page, signal, priority}));
|
||||
const fetchGlobal = async (
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
signal = null,
|
||||
) =>
|
||||
resolvePromiseOrWaitForPending(`apiClient/ranking/global/${page}`, () =>
|
||||
playersGlobalRankingApiClient.getProcessed({ page, signal, priority }),
|
||||
);
|
||||
|
||||
const fetchCountry = async (country, page = 1, priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`pageClient/ranking/${country}/${page}`, () => playersCountryRankingPageClient.getProcessed({country, page, signal, priority}));
|
||||
const fetchCountry = async (
|
||||
country,
|
||||
page = 1,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
signal = null,
|
||||
) =>
|
||||
resolvePromiseOrWaitForPending(
|
||||
`pageClient/ranking/${country}/${page}`,
|
||||
() =>
|
||||
playersCountryRankingPageClient.getProcessed({
|
||||
country,
|
||||
page,
|
||||
signal,
|
||||
priority,
|
||||
}),
|
||||
);
|
||||
|
||||
const fetchGlobalPages = async (priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`apiClient/rankingGlobalPages`, () => playersGlobalRankingPagesApiClient.getProcessed({signal, priority}));
|
||||
const fetchGlobalPages = async (priority = PRIORITY.FG_LOW, signal = null) =>
|
||||
resolvePromiseOrWaitForPending(`apiClient/rankingGlobalPages`, () =>
|
||||
playersGlobalRankingPagesApiClient.getProcessed({ signal, priority }),
|
||||
);
|
||||
|
||||
const fetchGlobalCount = async (priority = PRIORITY.FG_LOW, signal = null) => {
|
||||
const fetchGlobalCount = async (
|
||||
priority = PRIORITY.FG_LOW,
|
||||
signal = null,
|
||||
) => {
|
||||
const pages = await fetchGlobalPages(priority, signal);
|
||||
if (!pages || !Number.isFinite(pages)) return 0;
|
||||
|
||||
return pages * PLAYERS_PER_PAGE;
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchMiniRanking(rank, country = null, numOfPlayers = 5) {
|
||||
try {
|
||||
if (!Number.isFinite(numOfPlayers)) numOfPlayers = 5;
|
||||
|
||||
const getPage = rank => Math.floor((rank - 1) / PLAYERS_PER_PAGE) + 1;
|
||||
const getPage = (rank) => Math.floor((rank - 1) / PLAYERS_PER_PAGE) + 1;
|
||||
|
||||
const playerPage = getPage(rank);
|
||||
let firstPlayerRank = rank - (numOfPlayers - (numOfPlayers > 2 ? 2 : 1));
|
||||
@ -38,25 +66,33 @@ export default () => {
|
||||
const lastPlayerRank = firstPlayerRank + numOfPlayers - 1;
|
||||
const lastPlayerRankPage = getPage(lastPlayerRank);
|
||||
|
||||
const pages = [...new Set([playerPage, firstPlayerRankPage, lastPlayerRankPage])].filter(p => p);
|
||||
const pages = [
|
||||
...new Set([playerPage, firstPlayerRankPage, lastPlayerRankPage]),
|
||||
].filter((p) => p);
|
||||
|
||||
const ranking = (await Promise.all(pages.map(async page => (country ? fetchCountry(country, page) : fetchGlobal(page)))))
|
||||
const ranking = (
|
||||
await Promise.all(
|
||||
pages.map(async (page) =>
|
||||
country ? fetchCountry(country, page) : fetchGlobal(page),
|
||||
),
|
||||
)
|
||||
)
|
||||
.reduce((cum, arr) => cum.concat(arr), [])
|
||||
.filter(player => {
|
||||
const rank = opt(player, 'playerInfo.rank')
|
||||
.filter((player) => {
|
||||
const rank = opt(player, "playerInfo.rank");
|
||||
return rank >= firstPlayerRank && rank <= lastPlayerRank;
|
||||
})
|
||||
.sort((a,b) => opt(a, 'playerInfo.rank') - opt(b, 'playerInfo.rank'))
|
||||
.sort((a, b) => opt(a, "playerInfo.rank") - opt(b, "playerInfo.rank"));
|
||||
|
||||
return ranking;
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const destroyService = () => {
|
||||
service = null;
|
||||
}
|
||||
};
|
||||
|
||||
service = {
|
||||
getGlobal: fetchGlobal,
|
||||
@ -66,7 +102,7 @@ export default () => {
|
||||
getMiniRanking: fetchMiniRanking,
|
||||
PLAYERS_PER_PAGE,
|
||||
destroyService,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,23 @@
|
||||
import queues from '../network/queues/queues';
|
||||
import keyValueRepository from '../db/repository/key-value'
|
||||
import twitchRepository from '../db/repository/twitch'
|
||||
import createPlayerService from '../services/scoresaber/player'
|
||||
import profileApiClient from '../network/clients/twitch/api-profile'
|
||||
import videosApiClient from '../network/clients/twitch/api-videos'
|
||||
import eventBus from '../utils/broadcast-channel-pubsub'
|
||||
import log from '../utils/logger'
|
||||
import {addToDate, dateFromString, durationToMillis, formatDate, millisToDuration, MINUTE} from '../utils/date'
|
||||
import {PRIORITY} from '../network/queues/http-queue'
|
||||
import makePendingPromisePool from '../utils/pending-promises'
|
||||
import queues from "../network/queues/queues";
|
||||
import keyValueRepository from "../db/repository/key-value";
|
||||
import twitchRepository from "../db/repository/twitch";
|
||||
import createPlayerService from "../services/scoresaber/player";
|
||||
import profileApiClient from "../network/clients/twitch/api-profile";
|
||||
import videosApiClient from "../network/clients/twitch/api-videos";
|
||||
import eventBus from "../utils/broadcast-channel-pubsub";
|
||||
import log from "../utils/logger";
|
||||
import {
|
||||
addToDate,
|
||||
dateFromString,
|
||||
durationToMillis,
|
||||
formatDate,
|
||||
millisToDuration,
|
||||
MINUTE,
|
||||
} from "../utils/date";
|
||||
import { PRIORITY } from "../network/queues/http-queue";
|
||||
import makePendingPromisePool from "../utils/pending-promises";
|
||||
|
||||
const TWITCH_TOKEN_KEY = 'twitchToken';
|
||||
const TWITCH_TOKEN_KEY = "twitchToken";
|
||||
|
||||
const REFRESH_INTERVAL = 5 * MINUTE;
|
||||
|
||||
@ -24,31 +31,38 @@ export default () => {
|
||||
|
||||
const playerService = createPlayerService();
|
||||
|
||||
const getAuthUrl = (state = '', scopes = '') => queues.TWITCH.getAuthUrl(state, scopes)
|
||||
const getAuthUrl = (state = "", scopes = "") =>
|
||||
queues.TWITCH.getAuthUrl(state, scopes);
|
||||
|
||||
const getTwitchTokenFromUrl = () => {
|
||||
const url = (new URL(document.location));
|
||||
const url = new URL(document.location);
|
||||
|
||||
const error = url.searchParams.get('error')
|
||||
const error = url.searchParams.get("error");
|
||||
if (error) {
|
||||
const errorMsg = url.searchParams.get('error_description');
|
||||
const errorMsg = url.searchParams.get("error_description");
|
||||
throw new Error(errorMsg ? errorMsg : error);
|
||||
}
|
||||
|
||||
const hash = url.hash;
|
||||
if (!hash || !hash.length) throw new Error("Twitch did not return access token")
|
||||
if (!hash || !hash.length)
|
||||
throw new Error("Twitch did not return access token");
|
||||
|
||||
const accessTokenMatch = /access_token=(.*?)(&|$)/.exec(hash);
|
||||
if (!accessTokenMatch) throw new Error("Twitch did not return access token")
|
||||
if (!accessTokenMatch)
|
||||
throw new Error("Twitch did not return access token");
|
||||
|
||||
const stateMatch = /state=(.*?)(&|$)/.exec(hash);
|
||||
|
||||
return {accessToken: accessTokenMatch[1], url: stateMatch ? decodeURIComponent(stateMatch[1]) : ''};
|
||||
}
|
||||
return {
|
||||
accessToken: accessTokenMatch[1],
|
||||
url: stateMatch ? decodeURIComponent(stateMatch[1]) : "",
|
||||
};
|
||||
};
|
||||
|
||||
const processToken = async accessToken => {
|
||||
const processToken = async (accessToken) => {
|
||||
// validate token
|
||||
const tokenValidation = (await queues.TWITCH.validateToken(accessToken)).body;
|
||||
const tokenValidation = (await queues.TWITCH.validateToken(accessToken))
|
||||
.body;
|
||||
|
||||
const expiresIn = tokenValidation.expires_in * 1000;
|
||||
|
||||
@ -58,39 +72,77 @@ export default () => {
|
||||
obtained: new Date(),
|
||||
expires: new Date(Date.now() + expiresIn),
|
||||
expires_in: expiresIn,
|
||||
}
|
||||
};
|
||||
|
||||
await keyValueRepository().set(twitchToken, TWITCH_TOKEN_KEY);
|
||||
|
||||
eventBus.publish('twitch-token-refreshed', twitchToken)
|
||||
eventBus.publish("twitch-token-refreshed", twitchToken);
|
||||
|
||||
return twitchToken
|
||||
}
|
||||
return twitchToken;
|
||||
};
|
||||
|
||||
const getCurrentToken = async () => keyValueRepository().get(TWITCH_TOKEN_KEY, true);
|
||||
const getCurrentToken = async () =>
|
||||
keyValueRepository().get(TWITCH_TOKEN_KEY, true);
|
||||
|
||||
const fetchProfile = async (login, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => {
|
||||
const fetchProfile = async (
|
||||
login,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
{ fullResponse = false, ...options } = {},
|
||||
) => {
|
||||
const token = await getCurrentToken();
|
||||
if (!token || !token.expires || token.expires <= new Date()) return null;
|
||||
|
||||
return resolvePromiseOrWaitForPending(`profileApiClient/${login}/${fullResponse}`, () => profileApiClient.getProcessed({...options, accessToken: token.accessToken, login, priority, fullResponse}));
|
||||
}
|
||||
return resolvePromiseOrWaitForPending(
|
||||
`profileApiClient/${login}/${fullResponse}`,
|
||||
() =>
|
||||
profileApiClient.getProcessed({
|
||||
...options,
|
||||
accessToken: token.accessToken,
|
||||
login,
|
||||
priority,
|
||||
fullResponse,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const fetchVideos = async (userId, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => {
|
||||
const fetchVideos = async (
|
||||
userId,
|
||||
priority = PRIORITY.FG_LOW,
|
||||
{ fullResponse = false, ...options } = {},
|
||||
) => {
|
||||
const token = await getCurrentToken();
|
||||
if (!token || !token.expires || token.expires <= new Date()) return null;
|
||||
|
||||
return resolvePromiseOrWaitForPending(`videosApiClient/${userId}/${fullResponse}`, () => videosApiClient.getProcessed({...options, accessToken: token.accessToken, userId, priority, fullResponse}));
|
||||
}
|
||||
return resolvePromiseOrWaitForPending(
|
||||
`videosApiClient/${userId}/${fullResponse}`,
|
||||
() =>
|
||||
videosApiClient.getProcessed({
|
||||
...options,
|
||||
accessToken: token.accessToken,
|
||||
userId,
|
||||
priority,
|
||||
fullResponse,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getPlayerProfile = async playerId => twitchRepository().get(playerId);
|
||||
const updatePlayerProfile = async twitchProfile => twitchRepository().set(twitchProfile);
|
||||
const getPlayerProfile = async (playerId) => twitchRepository().get(playerId);
|
||||
const updatePlayerProfile = async (twitchProfile) =>
|
||||
twitchRepository().set(twitchProfile);
|
||||
|
||||
const refresh = async (playerId, forceUpdate = false, priority = queues.PRIORITY.FG_LOW, throwErrors = false) => {
|
||||
log.trace(`Starting Twitch videos refreshing${forceUpdate ? ' (forced)' : ''}...`, 'TwitchService')
|
||||
const refresh = async (
|
||||
playerId,
|
||||
forceUpdate = false,
|
||||
priority = queues.PRIORITY.FG_LOW,
|
||||
throwErrors = false,
|
||||
) => {
|
||||
log.trace(
|
||||
`Starting Twitch videos refreshing${forceUpdate ? " (forced)" : ""}...`,
|
||||
"TwitchService",
|
||||
);
|
||||
|
||||
if (!playerId) {
|
||||
log.debug(`No playerId provided, skipping`, 'TwitchService')
|
||||
log.debug(`No playerId provided, skipping`, "TwitchService");
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -98,7 +150,10 @@ export default () => {
|
||||
try {
|
||||
let twitchProfile = await twitchRepository().get(playerId);
|
||||
if (!twitchProfile || !twitchProfile.login) {
|
||||
log.debug(`Twitch profile for player ${playerId} is not set, skipping`, 'TwitchService')
|
||||
log.debug(
|
||||
`Twitch profile for player ${playerId} is not set, skipping`,
|
||||
"TwitchService",
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -106,7 +161,12 @@ export default () => {
|
||||
const lastUpdated = twitchProfile.lastUpdated;
|
||||
if (!forceUpdate) {
|
||||
if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) {
|
||||
log.debug(`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'TwitchService')
|
||||
log.debug(
|
||||
`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(
|
||||
addToDate(REFRESH_INTERVAL, lastUpdated),
|
||||
)}`,
|
||||
"TwitchService",
|
||||
);
|
||||
|
||||
return twitchProfile;
|
||||
}
|
||||
@ -115,7 +175,10 @@ export default () => {
|
||||
const player = playerService.get(playerId);
|
||||
if (player && player.recentPlay) {
|
||||
if (lastUpdated && lastUpdated > player.recentPlay) {
|
||||
log.debug(`Twitch updated after recent player play, skipping`, 'TwitchService')
|
||||
log.debug(
|
||||
`Twitch updated after recent player play, skipping`,
|
||||
"TwitchService",
|
||||
);
|
||||
|
||||
return twitchProfile;
|
||||
}
|
||||
@ -124,12 +187,15 @@ export default () => {
|
||||
if (!twitchProfile.id) {
|
||||
const fetchedProfile = await fetchProfile(twitchProfile.login);
|
||||
if (!fetchedProfile) {
|
||||
log.debug(`Can not fetch Twitch profile for player ${playerId}, skipping`, 'TwitchService')
|
||||
log.debug(
|
||||
`Can not fetch Twitch profile for player ${playerId}, skipping`,
|
||||
"TwitchService",
|
||||
);
|
||||
|
||||
return twitchProfile;
|
||||
}
|
||||
|
||||
twitchProfile = {...twitchProfile, ...fetchedProfile, playerId};
|
||||
twitchProfile = { ...twitchProfile, ...fetchedProfile, playerId };
|
||||
|
||||
await updatePlayerProfile(twitchProfile);
|
||||
}
|
||||
@ -141,7 +207,7 @@ export default () => {
|
||||
await updatePlayerProfile(twitchProfile);
|
||||
|
||||
if (videos && videos.length) {
|
||||
eventBus.publish('player-twitch-videos-updated', {
|
||||
eventBus.publish("player-twitch-videos-updated", {
|
||||
playerId,
|
||||
twitchProfile,
|
||||
});
|
||||
@ -151,25 +217,46 @@ export default () => {
|
||||
} catch (e) {
|
||||
if (throwErrors) throw e;
|
||||
|
||||
log.debug(`Twitch player ${playerId} refreshing error`, 'TwitchService', e)
|
||||
log.debug(
|
||||
`Twitch player ${playerId} refreshing error`,
|
||||
"TwitchService",
|
||||
e,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function findTwitchVideo(playerTwitchProfile, timeset, songLength) {
|
||||
if (!playerTwitchProfile || !playerTwitchProfile.videos || !timeset || !songLength) return null;
|
||||
if (
|
||||
!playerTwitchProfile ||
|
||||
!playerTwitchProfile.videos ||
|
||||
!timeset ||
|
||||
!songLength
|
||||
)
|
||||
return null;
|
||||
|
||||
const songStarted = addToDate(-songLength * 1000, timeset)
|
||||
const songStarted = addToDate(-songLength * 1000, timeset);
|
||||
const video = playerTwitchProfile.videos
|
||||
.map(v => ({
|
||||
.map((v) => ({
|
||||
...v,
|
||||
created_at: dateFromString(v.created_at),
|
||||
ended_at: addToDate(durationToMillis(v.duration), dateFromString(v.created_at)),
|
||||
ended_at: addToDate(
|
||||
durationToMillis(v.duration),
|
||||
dateFromString(v.created_at),
|
||||
),
|
||||
}))
|
||||
.find(v => v.created_at <= songStarted && songStarted < v.ended_at);
|
||||
.find((v) => v.created_at <= songStarted && songStarted < v.ended_at);
|
||||
|
||||
return video ? {...video, url: video.url + '?t=' + millisToDuration(songStarted - video.created_at)} : null;
|
||||
return video
|
||||
? {
|
||||
...video,
|
||||
url:
|
||||
video.url +
|
||||
"?t=" +
|
||||
millisToDuration(songStarted - video.created_at),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
const destroyService = () => {
|
||||
@ -179,7 +266,7 @@ export default () => {
|
||||
service = null;
|
||||
playerService.destroyService();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
service = {
|
||||
getAuthUrl,
|
||||
@ -192,7 +279,7 @@ export default () => {
|
||||
findTwitchVideo,
|
||||
refresh,
|
||||
destroyService,
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
};
|
||||
|
@ -1,43 +1,67 @@
|
||||
import {DateTime} from 'luxon';
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
export const getServicePlayerGain = (playerHistory, dateTruncFunc, dateKey, daysAgo = 1, maxDaysAgo = 7) => {
|
||||
export const getServicePlayerGain = (
|
||||
playerHistory,
|
||||
dateTruncFunc,
|
||||
dateKey,
|
||||
daysAgo = 1,
|
||||
maxDaysAgo = 7,
|
||||
) => {
|
||||
if (!playerHistory?.length) return null;
|
||||
|
||||
let todayServiceMidnightDate = dateTruncFunc(new Date());
|
||||
|
||||
const compareToDate = DateTime.fromJSDate(todayServiceMidnightDate).minus({days: daysAgo}).toJSDate();
|
||||
const maxServiceDate = DateTime.fromJSDate(todayServiceMidnightDate).minus({days: maxDaysAgo}).toJSDate();
|
||||
const compareToDate = DateTime.fromJSDate(todayServiceMidnightDate)
|
||||
.minus({ days: daysAgo })
|
||||
.toJSDate();
|
||||
const maxServiceDate = DateTime.fromJSDate(todayServiceMidnightDate)
|
||||
.minus({ days: maxDaysAgo })
|
||||
.toJSDate();
|
||||
|
||||
return playerHistory
|
||||
.sort((a, b) => b?.[dateKey]?.getTime() - a?.[dateKey]?.getTime())
|
||||
.find(h => h?.[dateKey] <= compareToDate && h?.[dateKey] >= maxServiceDate);
|
||||
}
|
||||
.find(
|
||||
(h) => h?.[dateKey] <= compareToDate && h?.[dateKey] >= maxServiceDate,
|
||||
);
|
||||
};
|
||||
|
||||
export const serviceFilterFunc = serviceParams => s => {
|
||||
export const serviceFilterFunc = (serviceParams) => (s) => {
|
||||
// accept score if there is no non-empty filter
|
||||
if (!Object.entries(serviceParams?.filters ?? {})?.filter(([key, val]) => val)?.length) return true;
|
||||
if (
|
||||
!Object.entries(serviceParams?.filters ?? {})?.filter(([key, val]) => val)
|
||||
?.length
|
||||
)
|
||||
return true;
|
||||
|
||||
let filterVal = true;
|
||||
|
||||
if (serviceParams?.filters?.search?.length) {
|
||||
const song = s?.leaderboard?.song ?? null;
|
||||
if (song) {
|
||||
const name = `${song?.name?.toLowerCase() ?? ''} ${song?.subName?.toLowerCase() ?? ''} ${song?.authorName?.toLowerCase() ?? ''} ${song?.levelAuthorName?.toLowerCase() ?? ''}`
|
||||
const name = `${song?.name?.toLowerCase() ?? ""} ${
|
||||
song?.subName?.toLowerCase() ?? ""
|
||||
} ${song?.authorName?.toLowerCase() ?? ""} ${
|
||||
song?.levelAuthorName?.toLowerCase() ?? ""
|
||||
}`;
|
||||
|
||||
filterVal &= name.indexOf(serviceParams.filters.search.toLowerCase()) >= 0;
|
||||
filterVal &=
|
||||
name.indexOf(serviceParams.filters.search.toLowerCase()) >= 0;
|
||||
} else {
|
||||
filterVal &= false;
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceParams?.filters.diff?.length) {
|
||||
filterVal &= s?.leaderboard?.diffInfo?.diff?.toLowerCase() === serviceParams.filters.diff?.toLowerCase()
|
||||
filterVal &=
|
||||
s?.leaderboard?.diffInfo?.diff?.toLowerCase() ===
|
||||
serviceParams.filters.diff?.toLowerCase();
|
||||
}
|
||||
|
||||
if (serviceParams?.filters?.songType?.length) {
|
||||
filterVal &= (serviceParams.filters.songType === 'ranked' && s?.pp > 0) ||
|
||||
(serviceParams.filters.songType === 'unranked' && (s?.pp ?? 0) === 0)
|
||||
filterVal &=
|
||||
(serviceParams.filters.songType === "ranked" && s?.pp > 0) ||
|
||||
(serviceParams.filters.songType === "unranked" && (s?.pp ?? 0) === 0);
|
||||
}
|
||||
|
||||
return filterVal;
|
||||
}
|
||||
};
|
||||
|
@ -1,21 +1,22 @@
|
||||
import {writable} from 'svelte/store'
|
||||
import keyValueRepository from '../db/repository/key-value';
|
||||
import {opt} from '../utils/js'
|
||||
import { writable } from "svelte/store";
|
||||
import keyValueRepository from "../db/repository/key-value";
|
||||
import { opt } from "../utils/js";
|
||||
|
||||
const STORE_CONFIG_KEY = 'config';
|
||||
const STORE_CONFIG_KEY = "config";
|
||||
|
||||
export const DEFAULT_LOCALE = 'en-US';
|
||||
export const DEFAULT_LOCALE = "en-US";
|
||||
|
||||
export let configStore = null;
|
||||
|
||||
const locales = {
|
||||
'de-DE': {id: 'de-DE', name: 'Deutschland'},
|
||||
'es-ES': {id: 'es-ES', name: 'España'},
|
||||
'pl-PL': {id: 'pl-PL', name: 'Polska'},
|
||||
'en-GB': {id: 'en-GB', name: 'United Kingdom'},
|
||||
'en-US': {id: 'en-US', name: 'United States'},
|
||||
"de-DE": { id: "de-DE", name: "Deutschland" },
|
||||
"es-ES": { id: "es-ES", name: "España" },
|
||||
"pl-PL": { id: "pl-PL", name: "Polska" },
|
||||
"en-GB": { id: "en-GB", name: "United Kingdom" },
|
||||
"en-US": { id: "en-US", name: "United States" },
|
||||
};
|
||||
export const getCurrentLocale = () => configStore ? configStore.getLocale() : DEFAULT_LOCALE;
|
||||
export const getCurrentLocale = () =>
|
||||
configStore ? configStore.getLocale() : DEFAULT_LOCALE;
|
||||
export const getSupportedLocales = () => Object.values(locales);
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
@ -24,41 +25,42 @@ const DEFAULT_CONFIG = {
|
||||
country: null,
|
||||
},
|
||||
scoreComparison: {
|
||||
method: 'in-place',
|
||||
method: "in-place",
|
||||
},
|
||||
preferences: {
|
||||
secondaryPp: 'attribution',
|
||||
avatarIcons: 'only-if-needed',
|
||||
secondaryPp: "attribution",
|
||||
avatarIcons: "only-if-needed",
|
||||
},
|
||||
locale: DEFAULT_LOCALE,
|
||||
}
|
||||
};
|
||||
|
||||
const newSettingsAvailableDefinition = {
|
||||
'scoreComparison.method': 'Method of displaying the comparison of scores',
|
||||
'preferences.secondaryPp': 'Setting the second PP metric',
|
||||
'preferences.avatarIcons': 'Showing icons on avatars',
|
||||
'locale': 'Locale selection',
|
||||
}
|
||||
"scoreComparison.method": "Method of displaying the comparison of scores",
|
||||
"preferences.secondaryPp": "Setting the second PP metric",
|
||||
"preferences.avatarIcons": "Showing icons on avatars",
|
||||
locale: "Locale selection",
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
if (configStore) return configStore;
|
||||
|
||||
let currentConfig = {...DEFAULT_CONFIG};
|
||||
let currentConfig = { ...DEFAULT_CONFIG };
|
||||
|
||||
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 newConfig = {...DEFAULT_CONFIG};
|
||||
Object.keys(config).forEach(key => {
|
||||
if (key === 'locale') {
|
||||
const newConfig = { ...DEFAULT_CONFIG };
|
||||
Object.keys(config).forEach((key) => {
|
||||
if (key === "locale") {
|
||||
newConfig[key] = config?.[key] ?? newConfig?.[key] ?? DEFAULT_LOCALE;
|
||||
return;
|
||||
}
|
||||
|
||||
newConfig[key] = {...newConfig?.[key], ...config?.[key]}
|
||||
newConfig[key] = { ...newConfig?.[key], ...config?.[key] };
|
||||
});
|
||||
|
||||
if (persist) await keyValueRepository().set(newConfig, STORE_CONFIG_KEY);
|
||||
@ -69,27 +71,31 @@ export default async () => {
|
||||
storeSet(newConfig);
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
};
|
||||
|
||||
const getLocale = () => opt(currentConfig, 'locale', DEFAULT_LOCALE);
|
||||
const getLocale = () => opt(currentConfig, "locale", DEFAULT_LOCALE);
|
||||
|
||||
const determineNewSettingsAvailable = dbConfig => Object.entries(newSettingsAvailableDefinition)
|
||||
.map(([key, description]) => opt(dbConfig, key) === undefined ? description : null)
|
||||
.filter(d => d)
|
||||
const determineNewSettingsAvailable = (dbConfig) =>
|
||||
Object.entries(newSettingsAvailableDefinition)
|
||||
.map(([key, description]) =>
|
||||
opt(dbConfig, key) === undefined ? description : null,
|
||||
)
|
||||
.filter((d) => d);
|
||||
|
||||
const dbConfig = await keyValueRepository().get(STORE_CONFIG_KEY);
|
||||
const newSettings= determineNewSettingsAvailable(dbConfig);
|
||||
const newSettings = determineNewSettingsAvailable(dbConfig);
|
||||
if (dbConfig) await set(dbConfig, false);
|
||||
newSettingsAvailable = newSettings && newSettings.length ? newSettings : undefined;
|
||||
newSettingsAvailable =
|
||||
newSettings && newSettings.length ? newSettings : undefined;
|
||||
|
||||
configStore = {
|
||||
subscribe,
|
||||
set,
|
||||
get,
|
||||
getMainPlayerId: () => opt(currentConfig, 'users.main'),
|
||||
getMainPlayerId: () => opt(currentConfig, "users.main"),
|
||||
getLocale,
|
||||
getNewSettingsAvailable: () => newSettingsAvailable,
|
||||
}
|
||||
};
|
||||
|
||||
return configStore;
|
||||
}
|
||||
};
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {writable} from 'svelte/store'
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => {
|
||||
const defaultValue = {name: null, width: null, nodeWidth: null, rect: null}
|
||||
const {subscribe, unsubscribe, set} = writable(defaultValue);
|
||||
export default (
|
||||
sizes = { phone: 0, tablet: 768, desktop: 1024, xxl: 1749 },
|
||||
) => {
|
||||
const defaultValue = { name: null, width: null, nodeWidth: null, rect: null };
|
||||
const { subscribe, unsubscribe, set } = writable(defaultValue);
|
||||
|
||||
let ro = null;
|
||||
let node = null;
|
||||
@ -10,12 +12,12 @@ export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => {
|
||||
const unobserve = () => {
|
||||
if (!node) return;
|
||||
|
||||
ro.unobserve(node)
|
||||
ro.unobserve(node);
|
||||
|
||||
node = null;
|
||||
}
|
||||
};
|
||||
|
||||
const observe = nodeToObserve => {
|
||||
const observe = (nodeToObserve) => {
|
||||
if (!nodeToObserve) return null;
|
||||
|
||||
if (node) unobserve();
|
||||
@ -34,19 +36,25 @@ export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => {
|
||||
set(
|
||||
Object.entries(sizes)
|
||||
.sort((a, b) => a[1] - b[1])
|
||||
.reduce((cum, item) => item[1] <= nodeWidth ? {name: item[0], width: item[1], nodeWidth, rect} : cum, defaultValue),
|
||||
)
|
||||
.reduce(
|
||||
(cum, item) =>
|
||||
item[1] <= nodeWidth
|
||||
? { name: item[0], width: item[1], nodeWidth, rect }
|
||||
: cum,
|
||||
defaultValue,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
ro.observe(node)
|
||||
ro.observe(node);
|
||||
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
observe,
|
||||
unobserve,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,12 +1,18 @@
|
||||
import {getFixedLeaderboardMaxScore, getMaxScore} from '../../../../utils/scoresaber/song'
|
||||
import {
|
||||
getFixedLeaderboardMaxScore,
|
||||
getMaxScore,
|
||||
} from "../../../../utils/scoresaber/song";
|
||||
|
||||
export default (score, bmStats, leaderboardId) => {
|
||||
let maxScore;
|
||||
|
||||
if (bmStats && bmStats.notes) {
|
||||
maxScore = getMaxScore(bmStats.notes)
|
||||
maxScore = getMaxScore(bmStats.notes);
|
||||
} else if (leaderboardId) {
|
||||
maxScore = getFixedLeaderboardMaxScore(leaderboardId, score?.maxScore ?? null)
|
||||
maxScore = getFixedLeaderboardMaxScore(
|
||||
leaderboardId,
|
||||
score?.maxScore ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
if (maxScore) {
|
||||
@ -17,14 +23,14 @@ export default (score, bmStats, leaderboardId) => {
|
||||
if (!unmodifiedScore) unmodifiedScore = score?.score ?? null;
|
||||
|
||||
if (unmodifiedScore && score.maxScore) {
|
||||
score.acc = unmodifiedScore ? unmodifiedScore / maxScore * 100 : null;
|
||||
score.acc = unmodifiedScore ? (unmodifiedScore / maxScore) * 100 : null;
|
||||
|
||||
if (score.score) score.percentage = score.score / score.maxScore * 100;
|
||||
if (score.score) score.percentage = (score.score / score.maxScore) * 100;
|
||||
}
|
||||
|
||||
if (score?.score && score?.maxScore) {
|
||||
score.percentage = score.score / score.maxScore * 100;
|
||||
score.percentage = (score.score / score.maxScore) * 100;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,14 @@
|
||||
import createBeatMapsService from '../../../../services/beatmaps'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import createBeatMapsService from "../../../../services/beatmaps";
|
||||
import { opt } from "../../../../utils/js";
|
||||
|
||||
const beatMaps = createBeatMapsService();
|
||||
|
||||
export default async (data, cachedOnly = false) => {
|
||||
if (!opt(data, 'leaderboard.song.hash.length')) return;
|
||||
if (!opt(data, "leaderboard.song.hash.length")) return;
|
||||
|
||||
data.leaderboard.beatMaps = await beatMaps.byHash(data.leaderboard.song.hash, false, cachedOnly);
|
||||
}
|
||||
data.leaderboard.beatMaps = await beatMaps.byHash(
|
||||
data.leaderboard.song.hash,
|
||||
false,
|
||||
cachedOnly,
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import createRankedsStore from '../../../../stores/scoresaber/rankeds'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import createRankedsStore from "../../../../stores/scoresaber/rankeds";
|
||||
import { opt } from "../../../../utils/js";
|
||||
|
||||
let rankeds;
|
||||
|
||||
@ -10,8 +10,11 @@ export default async (data) => {
|
||||
|
||||
if (!rankeds) return;
|
||||
|
||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
|
||||
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||
if (!leaderboardId) return;
|
||||
|
||||
data.leaderboard.stars = rankeds[leaderboardId] && rankeds[leaderboardId].stars ? rankeds[leaderboardId].stars : null;
|
||||
}
|
||||
data.leaderboard.stars =
|
||||
rankeds[leaderboardId] && rankeds[leaderboardId].stars
|
||||
? rankeds[leaderboardId].stars
|
||||
: null;
|
||||
};
|
||||
|
@ -1,16 +1,22 @@
|
||||
import {opt} from '../../../../utils/js'
|
||||
import calculateAcc from '../common/acc-calc'
|
||||
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
|
||||
import { opt } from "../../../../utils/js";
|
||||
import calculateAcc from "../common/acc-calc";
|
||||
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
|
||||
|
||||
export default async (data) => {
|
||||
if (!data || !data.score) return;
|
||||
|
||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId')
|
||||
const diffInfo = opt(data, 'leaderboard.diffInfo');
|
||||
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||
const diffInfo = opt(data, "leaderboard.diffInfo");
|
||||
|
||||
const versions = opt(data, 'leaderboard.beatMaps.versions')
|
||||
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
|
||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), diffInfo);
|
||||
const versions = opt(data, "leaderboard.beatMaps.versions");
|
||||
const versionsLastIdx =
|
||||
versions && Array.isArray(versions) && versions.length
|
||||
? versions.length - 1
|
||||
: 0;
|
||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(
|
||||
opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`),
|
||||
diffInfo,
|
||||
);
|
||||
|
||||
data.score = calculateAcc(data.score, bmStats, leaderboardId);
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import createBeatSaviorService from '../../../../services/beatsavior'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import {PRIORITY} from '../../../../network/queues/http-queue'
|
||||
import createBeatSaviorService from "../../../../services/beatsavior";
|
||||
import { opt } from "../../../../utils/js";
|
||||
import { PRIORITY } from "../../../../network/queues/http-queue";
|
||||
|
||||
let beatSaviorService;
|
||||
|
||||
@ -15,19 +15,19 @@ export default async (data, playerId = null) => {
|
||||
if (!bsData) return;
|
||||
|
||||
if (bsData?.stats)
|
||||
['left', 'right'].forEach(hand => {
|
||||
['Preswing', 'Postswing'].forEach(stat => {
|
||||
["left", "right"].forEach((hand) => {
|
||||
["Preswing", "Postswing"].forEach((stat) => {
|
||||
const key = `${hand}${stat}`;
|
||||
if (!bsData?.stats?.[key])
|
||||
bsData.stats[key] = bsData?.trackers?.accuracyTracker?.[key] ?? null;
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
const acc = opt(bsData, 'trackers.scoreTracker.rawRatio');
|
||||
const acc = opt(bsData, "trackers.scoreTracker.rawRatio");
|
||||
if (acc) data.score.acc = acc * 100;
|
||||
|
||||
const percentage = opt(bsData, 'trackers.scoreTracker.modifiedRatio');
|
||||
const percentage = opt(bsData, "trackers.scoreTracker.modifiedRatio");
|
||||
if (percentage) data.score.percentage = percentage * 100;
|
||||
|
||||
data.beatSavior = bsData;
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {configStore} from '../../../config'
|
||||
import createScoresService from '../../../../services/scoresaber/scores'
|
||||
import accEnhancer from './acc'
|
||||
import beatSaviorEnhancer from './beatsavior'
|
||||
import beatMapsEnhancer from '../common/beatmaps'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import produce from 'immer'
|
||||
import { configStore } from "../../../config";
|
||||
import createScoresService from "../../../../services/scoresaber/scores";
|
||||
import accEnhancer from "./acc";
|
||||
import beatSaviorEnhancer from "./beatsavior";
|
||||
import beatMapsEnhancer from "../common/beatmaps";
|
||||
import { opt } from "../../../../utils/js";
|
||||
import produce from "immer";
|
||||
|
||||
let scoresService = null;
|
||||
let mainPlayerId = null;
|
||||
@ -17,20 +17,29 @@ export const initCompareEnhancer = async () => {
|
||||
|
||||
scoresService = createScoresService();
|
||||
|
||||
configStoreUnsubscribe = configStore.subscribe(async config => {
|
||||
const newMainPlayerId = opt(config, 'users.main')
|
||||
configStoreUnsubscribe = configStore.subscribe(async (config) => {
|
||||
const newMainPlayerId = opt(config, "users.main");
|
||||
if (mainPlayerId !== newMainPlayerId) {
|
||||
mainPlayerId = newMainPlayerId;
|
||||
|
||||
if (!playerScores[mainPlayerId]) playerScores[mainPlayerId] = await scoresService.getPlayerScoresAsObject(mainPlayerId);
|
||||
if (!playerScores[mainPlayerId])
|
||||
playerScores[mainPlayerId] =
|
||||
await scoresService.getPlayerScoresAsObject(mainPlayerId);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default async (data, playerId = null) => {
|
||||
if (!data || !data.score || data.comparePlayers || !mainPlayerId || mainPlayerId === playerId) return;
|
||||
if (
|
||||
!data ||
|
||||
!data.score ||
|
||||
data.comparePlayers ||
|
||||
!mainPlayerId ||
|
||||
mainPlayerId === playerId
|
||||
)
|
||||
return;
|
||||
|
||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
|
||||
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||
if (!leaderboardId) return;
|
||||
|
||||
const comparePlayerScores = await playerScores[mainPlayerId];
|
||||
@ -38,14 +47,15 @@ export default async (data, playerId = null) => {
|
||||
|
||||
const mainPlayerScore = await produce(
|
||||
await produce(
|
||||
await produce(
|
||||
comparePlayerScores[leaderboardId],
|
||||
draft => beatMapsEnhancer(draft),
|
||||
await produce(comparePlayerScores[leaderboardId], (draft) =>
|
||||
beatMapsEnhancer(draft),
|
||||
),
|
||||
draft => accEnhancer(draft, true),
|
||||
(draft) => accEnhancer(draft, true),
|
||||
),
|
||||
draft => beatSaviorEnhancer(draft, mainPlayerId),
|
||||
(draft) => beatSaviorEnhancer(draft, mainPlayerId),
|
||||
);
|
||||
|
||||
data.comparePlayers = [{...mainPlayerScore, playerId: mainPlayerId, playerName: 'Me'}];
|
||||
}
|
||||
data.comparePlayers = [
|
||||
{ ...mainPlayerScore, playerId: mainPlayerId, playerName: "Me" },
|
||||
];
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import createScoresService from '../../../../services/scoresaber/scores';
|
||||
import calculateAcc from '../common/acc-calc'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
|
||||
import createScoresService from "../../../../services/scoresaber/scores";
|
||||
import calculateAcc from "../common/acc-calc";
|
||||
import { opt } from "../../../../utils/js";
|
||||
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
|
||||
|
||||
let scoresService;
|
||||
|
||||
@ -10,27 +10,39 @@ export default async (data, playerId = null) => {
|
||||
|
||||
if (data.prevScore) delete data.prevScore;
|
||||
|
||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
|
||||
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||
|
||||
if (!scoresService) scoresService = createScoresService();
|
||||
|
||||
const playerScores = scoresService.convertScoresToObject(await scoresService.getPlayerScores(playerId));
|
||||
const playerScores = scoresService.convertScoresToObject(
|
||||
await scoresService.getPlayerScores(playerId),
|
||||
);
|
||||
|
||||
// skip if no cached score
|
||||
if (!playerScores[leaderboardId]) return;
|
||||
|
||||
// compare to cached score if cached is equal to current or to cached history score otherwise
|
||||
let prevScore = playerScores[leaderboardId].score.score === data.score.score
|
||||
? (playerScores[leaderboardId].history && playerScores[leaderboardId].history.length ? playerScores[leaderboardId].history[0] : null)
|
||||
let prevScore =
|
||||
playerScores[leaderboardId].score.score === data.score.score
|
||||
? playerScores[leaderboardId].history &&
|
||||
playerScores[leaderboardId].history.length
|
||||
? playerScores[leaderboardId].history[0]
|
||||
: null
|
||||
: playerScores[leaderboardId].score;
|
||||
|
||||
// skip if no score to compare
|
||||
if (!prevScore) return;
|
||||
|
||||
const diffInfo = opt(data, 'leaderboard.diffInfo');
|
||||
const versions = opt(data, 'leaderboard.beatMaps.versions')
|
||||
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
|
||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), diffInfo);
|
||||
const diffInfo = opt(data, "leaderboard.diffInfo");
|
||||
const versions = opt(data, "leaderboard.beatMaps.versions");
|
||||
const versionsLastIdx =
|
||||
versions && Array.isArray(versions) && versions.length
|
||||
? versions.length - 1
|
||||
: 0;
|
||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(
|
||||
opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`),
|
||||
diffInfo,
|
||||
);
|
||||
|
||||
data.prevScore = calculateAcc(prevScore, bmStats, leaderboardId);
|
||||
}
|
||||
};
|
||||
|
@ -1,24 +1,28 @@
|
||||
import createPpService from '../../../../services/scoresaber/pp'
|
||||
import {configStore} from '../../../config'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import createPpService from "../../../../services/scoresaber/pp";
|
||||
import { configStore } from "../../../config";
|
||||
import { opt } from "../../../../utils/js";
|
||||
|
||||
let ppService;
|
||||
|
||||
export default async (data, playerId = null, whatIfOnly = false) => {
|
||||
if (!playerId) return;
|
||||
|
||||
const leaderboardId = opt(data, 'leaderboard.leaderboardId');
|
||||
const leaderboardId = opt(data, "leaderboard.leaderboardId");
|
||||
if (!leaderboardId) return;
|
||||
|
||||
const pp = opt(data, 'score.pp');
|
||||
const pp = opt(data, "score.pp");
|
||||
if (!pp) return;
|
||||
|
||||
if (!ppService) ppService = createPpService();
|
||||
|
||||
const mainPlayerId = configStore.getMainPlayerId();
|
||||
if (mainPlayerId && mainPlayerId !== playerId) {
|
||||
const whatIfPp = await ppService.getWhatIfScore(mainPlayerId, leaderboardId, pp)
|
||||
if (whatIfPp && whatIfPp.diff >= 0.01) data.score.whatIfPp = whatIfPp
|
||||
const whatIfPp = await ppService.getWhatIfScore(
|
||||
mainPlayerId,
|
||||
leaderboardId,
|
||||
pp,
|
||||
);
|
||||
if (whatIfPp && whatIfPp.diff >= 0.01) data.score.whatIfPp = whatIfPp;
|
||||
}
|
||||
|
||||
if (whatIfOnly) return;
|
||||
@ -27,4 +31,4 @@ export default async (data, playerId = null, whatIfOnly = false) => {
|
||||
if (!ppAttribution) return;
|
||||
|
||||
data.score.ppAttribution = -ppAttribution.diff;
|
||||
}
|
||||
};
|
||||
|
@ -1,15 +1,22 @@
|
||||
import createTwitchService from '../../../../services/twitch'
|
||||
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song'
|
||||
import {opt} from '../../../../utils/js'
|
||||
import createTwitchService from "../../../../services/twitch";
|
||||
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song";
|
||||
import { opt } from "../../../../utils/js";
|
||||
|
||||
let twitchService;
|
||||
|
||||
export default async (data, playerId = null) => {
|
||||
if (!data || !data.score || !data.leaderboard || !data.leaderboard.beatMaps) return;
|
||||
if (!data || !data.score || !data.leaderboard || !data.leaderboard.beatMaps)
|
||||
return;
|
||||
|
||||
const versions = opt(data, 'leaderboard.beatMaps.versions')
|
||||
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
|
||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), data.leaderboard.diffInfo);
|
||||
const versions = opt(data, "leaderboard.beatMaps.versions");
|
||||
const versionsLastIdx =
|
||||
versions && Array.isArray(versions) && versions.length
|
||||
? versions.length - 1
|
||||
: 0;
|
||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(
|
||||
opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`),
|
||||
data.leaderboard.diffInfo,
|
||||
);
|
||||
if (!bmStats || !bmStats.seconds) return;
|
||||
|
||||
if (!twitchService) twitchService = createTwitchService();
|
||||
@ -17,8 +24,12 @@ export default async (data, playerId = null) => {
|
||||
const twitchProfile = await twitchService.refresh(playerId);
|
||||
if (!twitchProfile) return;
|
||||
|
||||
const video = await twitchService.findTwitchVideo(twitchProfile, data.score.timeSet, bmStats.seconds);
|
||||
const video = await twitchService.findTwitchVideo(
|
||||
twitchProfile,
|
||||
data.score.timeSet,
|
||||
bmStats.seconds,
|
||||
);
|
||||
if (!video || !video.url) return;
|
||||
|
||||
data.twitchVideo = video;
|
||||
}
|
||||
};
|
||||
|
@ -1,44 +1,53 @@
|
||||
import createHttpStore from './http-store';
|
||||
import beatMapsEnhancer from './enhancers/common/beatmaps'
|
||||
import accEnhancer from './enhancers/scores/acc'
|
||||
import createLeaderboardPageProvider from './providers/page-leaderboard'
|
||||
import {writable} from 'svelte/store'
|
||||
import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../utils/scoresaber/song'
|
||||
import {debounce} from '../../utils/debounce'
|
||||
import produce, {applyPatches} from 'immer'
|
||||
import ppAttributionEnhancer from './enhancers/scores/pp-attribution'
|
||||
import createHttpStore from "./http-store";
|
||||
import beatMapsEnhancer from "./enhancers/common/beatmaps";
|
||||
import accEnhancer from "./enhancers/scores/acc";
|
||||
import createLeaderboardPageProvider from "./providers/page-leaderboard";
|
||||
import { writable } from "svelte/store";
|
||||
import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../utils/scoresaber/song";
|
||||
import { debounce } from "../../utils/debounce";
|
||||
import produce, { applyPatches } from "immer";
|
||||
import ppAttributionEnhancer from "./enhancers/scores/pp-attribution";
|
||||
|
||||
export default (leaderboardId, type = 'global', page = 1, initialState = null, initialStateType = 'initial') => {
|
||||
export default (
|
||||
leaderboardId,
|
||||
type = "global",
|
||||
page = 1,
|
||||
initialState = null,
|
||||
initialStateType = "initial",
|
||||
) => {
|
||||
let currentLeaderboardId = leaderboardId ? leaderboardId : null;
|
||||
let currentType = type ? type : 'global';
|
||||
let currentType = type ? type : "global";
|
||||
let currentPage = page ? page : 1;
|
||||
|
||||
const {subscribe: subscribeEnhanced, set: setEnhanced} = writable(null);
|
||||
const { subscribe: subscribeEnhanced, set: setEnhanced } = writable(null);
|
||||
|
||||
const getCurrentEnhanceTaskId = () => `${currentLeaderboardId}/${currentPage}/${currentType}`;
|
||||
const getPatchId = (leaderboardId, scoreRow) => `${leaderboardId}/${scoreRow?.player?.playerId}`
|
||||
const getCurrentEnhanceTaskId = () =>
|
||||
`${currentLeaderboardId}/${currentPage}/${currentType}`;
|
||||
const getPatchId = (leaderboardId, scoreRow) =>
|
||||
`${leaderboardId}/${scoreRow?.player?.playerId}`;
|
||||
|
||||
let enhancePatches = {};
|
||||
let currentEnhanceTaskId = null;
|
||||
|
||||
const onNewData = ({fetchParams, state, set}) => {
|
||||
const onNewData = ({ fetchParams, state, set }) => {
|
||||
currentLeaderboardId = fetchParams?.leaderboardId ?? null;
|
||||
currentType = fetchParams?.type ?? 'global';
|
||||
currentType = fetchParams?.type ?? "global";
|
||||
currentPage = fetchParams?.page ?? 1;
|
||||
|
||||
if (!state) return;
|
||||
|
||||
const enhanceTaskId = getCurrentEnhanceTaskId();
|
||||
if (currentEnhanceTaskId !== enhanceTaskId) {
|
||||
enhancePatches = {}
|
||||
enhancePatches = {};
|
||||
currentEnhanceTaskId = enhanceTaskId;
|
||||
}
|
||||
|
||||
const stateProduce = (state, patchId, producer) => produce(state, producer, patches => {
|
||||
const stateProduce = (state, patchId, producer) =>
|
||||
produce(state, producer, (patches) => {
|
||||
if (!enhancePatches[patchId]) enhancePatches[patchId] = [];
|
||||
|
||||
enhancePatches[patchId].push(...patches)
|
||||
})
|
||||
enhancePatches[patchId].push(...patches);
|
||||
});
|
||||
|
||||
const debouncedSetState = debounce((enhanceTaskId, state) => {
|
||||
if (currentEnhanceTaskId !== enhanceTaskId) return;
|
||||
@ -46,78 +55,119 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
|
||||
set(state);
|
||||
}, 100);
|
||||
|
||||
const newState = {...state};
|
||||
const newState = { ...state };
|
||||
|
||||
const setStateRow = (enhanceTaskId, scoreRow) => {
|
||||
if (currentEnhanceTaskId !== enhanceTaskId) return null;
|
||||
|
||||
const patchId = getPatchId(currentLeaderboardId, scoreRow)
|
||||
const stateRowIdx = newState.scores.findIndex(s => getPatchId(currentLeaderboardId, s) === patchId)
|
||||
const patchId = getPatchId(currentLeaderboardId, scoreRow);
|
||||
const stateRowIdx = newState.scores.findIndex(
|
||||
(s) => getPatchId(currentLeaderboardId, s) === patchId,
|
||||
);
|
||||
if (stateRowIdx < 0) return;
|
||||
|
||||
newState.scores[stateRowIdx] = applyPatches(newState.scores[stateRowIdx], enhancePatches[patchId]);
|
||||
newState.scores[stateRowIdx] = applyPatches(
|
||||
newState.scores[stateRowIdx],
|
||||
enhancePatches[patchId],
|
||||
);
|
||||
|
||||
debouncedSetState(enhanceTaskId, newState);
|
||||
|
||||
return newState.scores[stateRowIdx];
|
||||
}
|
||||
};
|
||||
|
||||
if (newState.leaderboard)
|
||||
beatMapsEnhancer(newState)
|
||||
.then(_ => {
|
||||
const versions = newState?.leaderboard?.beatMaps?.versions ?? null
|
||||
const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0;
|
||||
.then((_) => {
|
||||
const versions = newState?.leaderboard?.beatMaps?.versions ?? null;
|
||||
const versionsLastIdx =
|
||||
versions && Array.isArray(versions) && versions.length
|
||||
? versions.length - 1
|
||||
: 0;
|
||||
|
||||
const bpm = newState?.leaderboard?.beatMaps?.metadata?.bpm ?? null;
|
||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(newState?.leaderboard?.beatMaps?.versions?.[versionsLastIdx]?.diffs, newState?.leaderboard?.diffInfo);
|
||||
const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(
|
||||
newState?.leaderboard?.beatMaps?.versions?.[versionsLastIdx]?.diffs,
|
||||
newState?.leaderboard?.diffInfo,
|
||||
);
|
||||
if (!bmStats) return null;
|
||||
|
||||
newState.leaderboard.stats = {...newState.leaderboard.stats, ...bmStats, bpm};
|
||||
newState.leaderboard.stats = {
|
||||
...newState.leaderboard.stats,
|
||||
...bmStats,
|
||||
bpm,
|
||||
};
|
||||
|
||||
setEnhanced({leaderboardId, type, page, enhancedAt: new Date()})
|
||||
setEnhanced({ leaderboardId, type, page, enhancedAt: new Date() });
|
||||
debouncedSetState(enhanceTaskId, newState);
|
||||
|
||||
return newState.leaderboard.beatMaps;
|
||||
})
|
||||
.then(_ => {
|
||||
.then((_) => {
|
||||
if (!newState.scores || !newState.scores.length) return;
|
||||
|
||||
for (const scoreRow of newState.scores) {
|
||||
stateProduce({
|
||||
stateProduce(
|
||||
{
|
||||
...scoreRow,
|
||||
leaderboard: newState.leaderboard
|
||||
}, getPatchId(currentLeaderboardId, scoreRow), draft => accEnhancer(draft))
|
||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
||||
.then(scoreRow => stateProduce({...scoreRow, leaderboard: newState.leaderboard}, getPatchId(currentLeaderboardId, scoreRow), draft => ppAttributionEnhancer(draft, scoreRow?.player?.playerId, true))
|
||||
leaderboard: newState.leaderboard,
|
||||
},
|
||||
getPatchId(currentLeaderboardId, scoreRow),
|
||||
(draft) => accEnhancer(draft),
|
||||
)
|
||||
.then(scoreRow => setStateRow(enhanceTaskId, scoreRow))
|
||||
}
|
||||
})
|
||||
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow))
|
||||
.then((scoreRow) =>
|
||||
stateProduce(
|
||||
{ ...scoreRow, leaderboard: newState.leaderboard },
|
||||
getPatchId(currentLeaderboardId, scoreRow),
|
||||
(draft) =>
|
||||
ppAttributionEnhancer(
|
||||
draft,
|
||||
scoreRow?.player?.playerId,
|
||||
true,
|
||||
),
|
||||
),
|
||||
)
|
||||
.then((scoreRow) => setStateRow(enhanceTaskId, scoreRow));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const provider = createLeaderboardPageProvider();
|
||||
|
||||
const httpStore = createHttpStore(
|
||||
provider,
|
||||
{leaderboardId, type, page},
|
||||
{ leaderboardId, type, page },
|
||||
initialState,
|
||||
{
|
||||
onInitialized: 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 === 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 {
|
||||
...httpStore,
|
||||
@ -126,7 +176,6 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i
|
||||
getLeaderboardId: () => currentLeaderboardId,
|
||||
getType: () => currentType,
|
||||
getPage: () => currentPage,
|
||||
enhanced: {subscribe: subscribeEnhanced},
|
||||
}
|
||||
}
|
||||
|
||||
enhanced: { subscribe: subscribeEnhanced },
|
||||
};
|
||||
};
|
||||
|
@ -1,16 +1,20 @@
|
||||
import createHttpStore from './http-store';
|
||||
import playerApiClient from '../../network/clients/scoresaber/player/api'
|
||||
import createHttpStore from "./http-store";
|
||||
import playerApiClient from "../../network/clients/scoresaber/player/api";
|
||||
|
||||
export default (playerId = null, initialState = null, initialStateType = 'initial') => {
|
||||
export default (
|
||||
playerId = null,
|
||||
initialState = null,
|
||||
initialStateType = "initial",
|
||||
) => {
|
||||
let currentPlayerId = playerId;
|
||||
|
||||
const onNewData = ({fetchParams}) => {
|
||||
const onNewData = ({ fetchParams }) => {
|
||||
currentPlayerId = fetchParams?.playerId ?? null;
|
||||
}
|
||||
};
|
||||
|
||||
const httpStore = createHttpStore(
|
||||
playerApiClient,
|
||||
playerId ? {playerId} : null,
|
||||
playerId ? { playerId } : null,
|
||||
initialState,
|
||||
{
|
||||
onInitialized: onNewData,
|
||||
@ -22,13 +26,12 @@ export default (playerId = null, initialState = null, initialStateType = 'initia
|
||||
const fetch = async (playerId = currentPlayerId, force = false) => {
|
||||
if (!playerId || (playerId === currentPlayerId && !force)) return false;
|
||||
|
||||
return httpStore.fetch({playerId}, force, playerApiClient);
|
||||
}
|
||||
return httpStore.fetch({ playerId }, force, playerApiClient);
|
||||
};
|
||||
|
||||
return {
|
||||
...httpStore,
|
||||
fetch,
|
||||
getPlayerId: () => currentPlayerId,
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
@ -1,49 +1,61 @@
|
||||
import stringify from 'json-stable-stringify';
|
||||
import eventBus from '../../utils/broadcast-channel-pubsub'
|
||||
import createHttpStore from './http-store';
|
||||
import createApiPlayerWithScoresProvider from './providers/api-player-with-scores'
|
||||
import createPlayerService from '../../services/scoresaber/player'
|
||||
import {addToDate, MINUTE} from '../../utils/date'
|
||||
import {writable} from 'svelte/store'
|
||||
import stringify from "json-stable-stringify";
|
||||
import eventBus from "../../utils/broadcast-channel-pubsub";
|
||||
import createHttpStore from "./http-store";
|
||||
import createApiPlayerWithScoresProvider from "./providers/api-player-with-scores";
|
||||
import createPlayerService from "../../services/scoresaber/player";
|
||||
import { addToDate, MINUTE } from "../../utils/date";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export default (playerId = null, service = 'scoresaber', serviceParams = {type: 'recent', page: 1}, initialState = null, initialStateType = 'initial') => {
|
||||
export default (
|
||||
playerId = null,
|
||||
service = "scoresaber",
|
||||
serviceParams = { type: "recent", page: 1 },
|
||||
initialState = null,
|
||||
initialStateType = "initial",
|
||||
) => {
|
||||
let currentPlayerId = playerId;
|
||||
let currentService = service;
|
||||
let currentServiceParams = serviceParams;
|
||||
|
||||
const {subscribe: subscribeParams, set: setParams} = writable(null);
|
||||
const { subscribe: subscribeParams, set: setParams } = writable(null);
|
||||
|
||||
let playerService = createPlayerService();
|
||||
|
||||
let lastRecentPlay = null;
|
||||
let playerForLastRecentPlay = null;
|
||||
|
||||
const onNewData = ({fetchParams}) => {
|
||||
const onNewData = ({ fetchParams }) => {
|
||||
currentPlayerId = fetchParams?.playerId ?? null;
|
||||
currentService = fetchParams?.service ?? null;
|
||||
currentServiceParams = fetchParams?.serviceParams ?? null;
|
||||
|
||||
setParams({currentPlayerId, currentService, currentServiceParams})
|
||||
}
|
||||
setParams({ currentPlayerId, currentService, currentServiceParams });
|
||||
};
|
||||
|
||||
const provider = createApiPlayerWithScoresProvider();
|
||||
|
||||
const httpStore = createHttpStore(
|
||||
provider,
|
||||
playerId ? {playerId, service, serviceParams} : null,
|
||||
playerId ? { playerId, service, serviceParams } : null,
|
||||
initialState,
|
||||
{
|
||||
onInitialized: onNewData,
|
||||
onAfterStateChange: onNewData,
|
||||
},
|
||||
initialStateType
|
||||
initialStateType,
|
||||
);
|
||||
|
||||
const fetch = async (playerId = currentPlayerId, service = currentService, serviceParams = currentServiceParams, force = false) => {
|
||||
const fetch = async (
|
||||
playerId = currentPlayerId,
|
||||
service = currentService,
|
||||
serviceParams = currentServiceParams,
|
||||
force = false,
|
||||
) => {
|
||||
if (
|
||||
(!playerId || playerId === currentPlayerId) &&
|
||||
(!service || stringify(service) === stringify(currentService)) &&
|
||||
(!serviceParams || stringify(serviceParams) === stringify(currentServiceParams)) &&
|
||||
(!serviceParams ||
|
||||
stringify(serviceParams) === stringify(currentServiceParams)) &&
|
||||
!force
|
||||
)
|
||||
return false;
|
||||
@ -54,12 +66,20 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
||||
playerForLastRecentPlay = playerId;
|
||||
}
|
||||
|
||||
return httpStore.fetch({playerId, service, serviceParams}, force, provider, !playerId || playerId !== currentPlayerId || force);
|
||||
}
|
||||
return httpStore.fetch(
|
||||
{ playerId, service, serviceParams },
|
||||
force,
|
||||
provider,
|
||||
!playerId || playerId !== currentPlayerId || force,
|
||||
);
|
||||
};
|
||||
|
||||
const refresh = async () => fetch(currentPlayerId, currentService, currentServiceParams, true);
|
||||
const refresh = async () =>
|
||||
fetch(currentPlayerId, currentService, currentServiceParams, true);
|
||||
|
||||
const playerRecentPlayUpdatedUnsubscribe = eventBus.on('player-recent-play-updated', async ({playerId, recentPlay}) => {
|
||||
const playerRecentPlayUpdatedUnsubscribe = eventBus.on(
|
||||
"player-recent-play-updated",
|
||||
async ({ playerId, recentPlay }) => {
|
||||
if (!playerId || !currentPlayerId || playerId !== currentPlayerId) return;
|
||||
|
||||
if (!recentPlay || !lastRecentPlay || recentPlay <= lastRecentPlay) {
|
||||
@ -74,55 +94,61 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type:
|
||||
playerForLastRecentPlay = playerId;
|
||||
|
||||
await refresh();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const subscribe = fn => {
|
||||
const subscribe = (fn) => {
|
||||
const storeUnsubscribe = httpStore.subscribe(fn);
|
||||
|
||||
return () => {
|
||||
storeUnsubscribe();
|
||||
playerRecentPlayUpdatedUnsubscribe();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_RECENT_PLAY_REFRESH_INTERVAL = MINUTE;
|
||||
|
||||
const enqueueRecentPlayRefresh = async () => {
|
||||
if (!currentPlayerId) {
|
||||
setTimeout(() => enqueueRecentPlayRefresh(), DEFAULT_RECENT_PLAY_REFRESH_INTERVAL);
|
||||
setTimeout(
|
||||
() => enqueueRecentPlayRefresh(),
|
||||
DEFAULT_RECENT_PLAY_REFRESH_INTERVAL,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await playerService.fetchPlayerAndUpdateRecentPlay(currentPlayerId);
|
||||
|
||||
const refreshInterval = !lastRecentPlay || lastRecentPlay >= addToDate(-30 * MINUTE, new Date())
|
||||
const refreshInterval =
|
||||
!lastRecentPlay || lastRecentPlay >= addToDate(-30 * MINUTE, new Date())
|
||||
? DEFAULT_RECENT_PLAY_REFRESH_INTERVAL
|
||||
: 15 * MINUTE;
|
||||
|
||||
setTimeout(() => enqueueRecentPlayRefresh(), refreshInterval);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
setTimeout(() => enqueueRecentPlayRefresh(), DEFAULT_RECENT_PLAY_REFRESH_INTERVAL);
|
||||
setTimeout(
|
||||
() => enqueueRecentPlayRefresh(),
|
||||
DEFAULT_RECENT_PLAY_REFRESH_INTERVAL,
|
||||
);
|
||||
|
||||
return {
|
||||
...httpStore,
|
||||
subscribe,
|
||||
fetch,
|
||||
refresh,
|
||||
params: {subscribe: subscribeParams},
|
||||
params: { subscribe: subscribeParams },
|
||||
getPlayerId: () => currentPlayerId,
|
||||
getService: () => currentService,
|
||||
setService: type => {
|
||||
setService: (type) => {
|
||||
currentService = type;
|
||||
setParams({currentPlayerId, currentService, currentServiceParams})
|
||||
setParams({ currentPlayerId, currentService, currentServiceParams });
|
||||
},
|
||||
getServiceParams: () => currentServiceParams,
|
||||
setServiceParams: page => {
|
||||
currentServiceParams = page
|
||||
setParams({currentPlayerId, currentService, currentServiceParams})
|
||||
setServiceParams: (page) => {
|
||||
currentServiceParams = page;
|
||||
setParams({ currentPlayerId, currentService, currentServiceParams });
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
@ -1,34 +1,48 @@
|
||||
import createHttpStore from './http-store';
|
||||
import createApiRankingProvider from './providers/api-ranking'
|
||||
import createHttpStore from "./http-store";
|
||||
import createApiRankingProvider from "./providers/api-ranking";
|
||||
|
||||
export default (type = 'global', page = 1, initialState = null, initialStateType = 'initial') => {
|
||||
let currentType = type ? type : 'global';
|
||||
export default (
|
||||
type = "global",
|
||||
page = 1,
|
||||
initialState = null,
|
||||
initialStateType = "initial",
|
||||
) => {
|
||||
let currentType = type ? type : "global";
|
||||
let currentPage = page ? page : 1;
|
||||
|
||||
const onNewData = ({fetchParams}) => {
|
||||
currentType = fetchParams?.type ?? 'global';
|
||||
const onNewData = ({ fetchParams }) => {
|
||||
currentType = fetchParams?.type ?? "global";
|
||||
currentPage = fetchParams?.page ?? 1;
|
||||
}
|
||||
};
|
||||
|
||||
const provider = createApiRankingProvider();
|
||||
|
||||
const httpStore = createHttpStore(
|
||||
provider,
|
||||
{type, page},
|
||||
{ type, page },
|
||||
initialState,
|
||||
{
|
||||
onInitialized: onNewData,
|
||||
onAfterStateChange: onNewData,
|
||||
onSetPending: ({fetchParams}) => ({...fetchParams}),
|
||||
onSetPending: ({ fetchParams }) => ({ ...fetchParams }),
|
||||
},
|
||||
initialStateType
|
||||
initialStateType,
|
||||
);
|
||||
|
||||
const fetch = async (type = currentType, page = currentPage, force = false) => {
|
||||
if ((!type || type === currentType) && (!page || page === currentPage) && !force) return false;
|
||||
const fetch = async (
|
||||
type = currentType,
|
||||
page = currentPage,
|
||||
force = false,
|
||||
) => {
|
||||
if (
|
||||
(!type || type === currentType) &&
|
||||
(!page || page === currentPage) &&
|
||||
!force
|
||||
)
|
||||
return false;
|
||||
|
||||
return httpStore.fetch({type, page}, force, provider);
|
||||
}
|
||||
return httpStore.fetch({ type, page }, force, provider);
|
||||
};
|
||||
|
||||
const refresh = async () => fetch(currentType, currentPage, true);
|
||||
|
||||
@ -38,6 +52,5 @@ export default (type = 'global', page = 1, initialState = null, initialStateType
|
||||
refresh,
|
||||
getType: () => currentType,
|
||||
getPage: () => currentPage,
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user