diff --git a/LICENSE b/LICENSE index b6a55a6..162676c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -MIT License + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (c) 2022 qier222 +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +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. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"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 +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 +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 +on the Program. + +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 +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" +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 +extent that warranties are provided), that licensees may convey the +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. + +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 +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 +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 +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(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 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 +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +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 +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +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 +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 +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 +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +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 +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +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 +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 +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. + +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 +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, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +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: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + 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 +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 +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +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. + +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: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + 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 +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 +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, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +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, +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 +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 +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 +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +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 +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 +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, +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. + +"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 +that they are valid under applicable law. If additional permissions +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 +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 +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +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 +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +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 +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 +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +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 +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 +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 +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. + +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, +nothing other than this License grants you permission to propagate or +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. + +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 +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 +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +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 +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 +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +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 +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, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +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 +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 +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, +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, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +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 +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 +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 +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 +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +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 +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. + +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 +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +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. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +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 General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero 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 +versions of the GNU Affero 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 +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. + +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, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +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. + +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 +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +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. + +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 +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +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 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. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +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 AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b1c9ab --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +
+

+ +

R3PLAY

+ +

+ 高颜值的第三方网易云播放器(原YesPlayMusic) +
+ 🌎 访问DEMO  |   + 📦️ 下载安装包  |   + 💬 加入交流群 +
+
+

+

+ + + +## 关于 Alpha 版本 + +目前 R3PLAY 处于 Alpha 阶段,仍在开发中,功能尚未完善。建议每次更新时,先卸载旧版本的 R3PLAY,再重新安装。如遇到问题,欢迎加入[Telegram 交流群](https://t.me/yesplaymusic)反馈。 + +## ✨ 特性 + +- ✅ 使用 React + Electron 开发 +- 🔴 网易云账号登录(扫码/手机/邮箱登录) +- 📺 支持 MV 播放 +- 🚫🤝 无任何社交功能 +- 🛠 更多特性开发中 + +## 📦️ 安装 + +访问本项目的 [Releases](https://github.com/qier222/YesPlayMusic/releases) +页面下载安装包。 + + + +## 📜 开源许可 + +API 源代码来自 [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) + +本项目仅供个人学习研究之目的,不得用于任何商业或非法活动。 + +基于 [AGPL license](https://opensource.org/licenses/AGPL) 许可进行开源。 + +任何基于此项目开发的项目都必须遵守开源协议,在项目 README/应用内的关于页面和介绍网站中明确说明基于此项目开发,并附上此项目 GitHub 页面的链接。 + + + + + + + + +[album-screenshot]: images/album.png +[artist-screenshot]: images/artist.png +[explore-screenshot]: images/explore.png +[home-screenshot]: images/home.png +[home-2-screenshot]: images/home-2.png +[lyrics-screenshot]: images/lyrics.png +[library-screenshot]: images/library.png +[library-dark-screenshot]: images/library-dark.png +[search-screenshot]: images/search.png diff --git a/package.json b/package.json index aa649e4..bee70ab 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ }, "packageManager": "pnpm@7.20.0", "scripts": { - "postinstall": "turbo run post-install --parallel --no-cache", "install": "turbo run post-install --parallel --no-cache", "build": "cross-env-shell IS_ELECTRON=yes turbo run build", "build:web": "turbo run build:web", diff --git a/packages/desktop/.electron-builder.config.js b/packages/desktop/.electron-builder.config.js index 26e6169..78f5f40 100644 --- a/packages/desktop/.electron-builder.config.js +++ b/packages/desktop/.electron-builder.config.js @@ -19,6 +19,7 @@ module.exports = { buildDependenciesFromSource: false, electronVersion, forceCodeSigning: false, + afterPack: './scripts/copySQLite3.js', publish: [ { provider: 'github', @@ -118,17 +119,6 @@ module.exports = { '!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json,pnpm-lock.yaml}', '!**/*.{map,debug.min.js}', - // copy prisma - { - from: './prisma', - to: 'main/prisma', - }, - { - from: './prisma', - to: 'main', - filter: '*.prisma' // only copy prisma schema - }, - { from: './dist', to: './main', diff --git a/packages/desktop/build/icons/1024x1024.png b/packages/desktop/build/icons/1024x1024.png index 9fc1c62..f7c002c 100644 Binary files a/packages/desktop/build/icons/1024x1024.png and b/packages/desktop/build/icons/1024x1024.png differ diff --git a/packages/desktop/build/icons/128x128.png b/packages/desktop/build/icons/128x128.png index af6310e..f7c002c 100644 Binary files a/packages/desktop/build/icons/128x128.png and b/packages/desktop/build/icons/128x128.png differ diff --git a/packages/desktop/build/icons/16x16.png b/packages/desktop/build/icons/16x16.png index 25e80fe..f7c002c 100644 Binary files a/packages/desktop/build/icons/16x16.png and b/packages/desktop/build/icons/16x16.png differ diff --git a/packages/desktop/build/icons/24x24.png b/packages/desktop/build/icons/24x24.png index d8e4715..f7c002c 100644 Binary files a/packages/desktop/build/icons/24x24.png and b/packages/desktop/build/icons/24x24.png differ diff --git a/packages/desktop/build/icons/256x256.png b/packages/desktop/build/icons/256x256.png index c511962..f7c002c 100644 Binary files a/packages/desktop/build/icons/256x256.png and b/packages/desktop/build/icons/256x256.png differ diff --git a/packages/desktop/build/icons/32x32.png b/packages/desktop/build/icons/32x32.png index d70242b..f7c002c 100644 Binary files a/packages/desktop/build/icons/32x32.png and b/packages/desktop/build/icons/32x32.png differ diff --git a/packages/desktop/build/icons/48x48.png b/packages/desktop/build/icons/48x48.png index 74fda19..f7c002c 100644 Binary files a/packages/desktop/build/icons/48x48.png and b/packages/desktop/build/icons/48x48.png differ diff --git a/packages/desktop/build/icons/512x512.png b/packages/desktop/build/icons/512x512.png index c5cda24..f7c002c 100644 Binary files a/packages/desktop/build/icons/512x512.png and b/packages/desktop/build/icons/512x512.png differ diff --git a/packages/desktop/build/icons/64x64.png b/packages/desktop/build/icons/64x64.png index 5eee418..f7c002c 100644 Binary files a/packages/desktop/build/icons/64x64.png and b/packages/desktop/build/icons/64x64.png differ diff --git a/packages/desktop/build/icons/icon.icns b/packages/desktop/build/icons/icon.icns index 9c2b3f3..be71ae2 100644 Binary files a/packages/desktop/build/icons/icon.icns and b/packages/desktop/build/icons/icon.icns differ diff --git a/packages/desktop/build/icons/icon.ico b/packages/desktop/build/icons/icon.ico index c3daede..10529ea 100644 Binary files a/packages/desktop/build/icons/icon.ico and b/packages/desktop/build/icons/icon.ico differ diff --git a/packages/desktop/build/icons/icon.png b/packages/desktop/build/icons/icon.png index 23f64d2..f7c002c 100644 Binary files a/packages/desktop/build/icons/icon.png and b/packages/desktop/build/icons/icon.png differ diff --git a/packages/desktop/main/appServer/routes/r3play/audio.ts b/packages/desktop/main/appServer/routes/r3play/audio.ts index ddbefc3..cee9270 100644 --- a/packages/desktop/main/appServer/routes/r3play/audio.ts +++ b/packages/desktop/main/appServer/routes/r3play/audio.ts @@ -1,6 +1,5 @@ import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify' import NeteaseCloudMusicApi, { SoundQualityType } from 'NeteaseCloudMusicApi' -import prisma from '@/desktop/main/prisma' import { app } from 'electron' import log from '@/desktop/main/log' import { appName } from '@/desktop/main/env' @@ -10,10 +9,11 @@ import youtube from '@/desktop/main/youtube' import { CacheAPIs } from '@/shared/CacheAPIs' import { FetchTracksResponse } from '@/shared/api/Track' import store from '@/desktop/main/store' +import { db, Tables } from '@/desktop/main/db' const getAudioFromCache = async (id: number) => { // get from cache - const cache = await prisma.audio.findUnique({ where: { id } }) + const cache = await db.find(Tables.Audio, id) if (!cache) return const audioFileName = `${cache.id}-${cache.bitRate}.${cache.format}` diff --git a/packages/desktop/main/cache.ts b/packages/desktop/main/cache.ts index 6e7a6bd..bd3396c 100644 --- a/packages/desktop/main/cache.ts +++ b/packages/desktop/main/cache.ts @@ -1,9 +1,11 @@ -import prisma from './prisma' +import { db, Tables } from './db' +import type { FetchTracksResponse } from '@/shared/api/Track' import { app } from 'electron' import log from './log' import fs from 'fs' import * as musicMetadata from 'music-metadata' -import { CacheAPIs, CacheAPIsParams, CacheAPIsResponse } from '@/shared/CacheAPIs' +import { CacheAPIs, CacheAPIsParams } from '@/shared/CacheAPIs' +import { TablesStructures } from './db' import { FastifyReply } from 'fastify' class Cache { @@ -11,12 +13,7 @@ class Cache { // } - async set( - api: T, - data: CacheAPIsResponse[T], - query: { [key: string]: string } = {} - ) { - if (!data) return + async set(api: string, data: any, query: any = {}) { switch (api) { case CacheAPIs.UserPlaylist: case CacheAPIs.UserAccount: @@ -26,104 +23,117 @@ class Cache { case CacheAPIs.UserArtists: case CacheAPIs.ListenedRecords: case CacheAPIs.Likelist: { - const id = api - const row = { id, json: JSON.stringify(data) } - await prisma.accountData.upsert({ where: { id }, create: row, update: row }) + if (!data) return + db.upsert(Tables.AccountData, { + id: api, + json: JSON.stringify(data), + updatedAt: Date.now(), + }) break } case CacheAPIs.Track: { - const res = data as CacheAPIsResponse[CacheAPIs.Track] + const res = data as FetchTracksResponse if (!res.songs) return - await Promise.all( - res.songs.map(t => { - const id = t.id - const row = { id, json: JSON.stringify(t) } - return prisma.track.upsert({ where: { id }, create: row, update: row }) - }) - ) + const tracks = res.songs.map(t => ({ + id: t.id, + json: JSON.stringify(t), + updatedAt: Date.now(), + })) + db.upsertMany(Tables.Track, tracks) break } case CacheAPIs.Album: { - const res = data as CacheAPIsResponse[CacheAPIs.Album] - if (!res.album) return - res.album.songs = data.songs - const id = data.album.id - const row = { id, json: JSON.stringify(data.album) } - await prisma.album.upsert({ where: { id }, update: row, create: row }) + if (!data.album) return + data.album.songs = data.songs + db.upsert(Tables.Album, { + id: data.album.id, + json: JSON.stringify(data.album), + updatedAt: Date.now(), + }) break } case CacheAPIs.Playlist: { if (!data.playlist) return - const id = data.playlist.id - const row = { id, json: JSON.stringify(data) } - await prisma.playlist.upsert({ where: { id }, update: row, create: row }) + db.upsert(Tables.Playlist, { + id: data.playlist.id, + json: JSON.stringify(data), + updatedAt: Date.now(), + }) break } case CacheAPIs.Artist: { if (!data.artist) return - const id = data.artist.id - const row = { id, json: JSON.stringify(data) } - await prisma.artist.upsert({ where: { id }, update: row, create: row }) + db.upsert(Tables.Artist, { + id: data.artist.id, + json: JSON.stringify(data), + updatedAt: Date.now(), + }) break } case CacheAPIs.ArtistAlbum: { - const res = data as CacheAPIsResponse[CacheAPIs.ArtistAlbum] - if (!res.hotAlbums) return - - const id = data.artist.id - const row = { id, hotAlbums: res.hotAlbums.map(a => a.id).join(',') } - await prisma.artistAlbum.upsert({ where: { id }, update: row, create: row }) - await Promise.all( - res.hotAlbums.map(async album => { - const id = album.id - const existAlbum = await prisma.album.findUnique({ where: { id } }) - if (!existAlbum) { - await prisma.album.create({ data: { id, json: JSON.stringify(album) } }) - } - }) + if (!data.hotAlbums) return + db.createMany( + Tables.Album, + data.hotAlbums.map((a: Album) => ({ + id: a.id, + json: JSON.stringify(a), + updatedAt: Date.now(), + })) ) + const modifiedData = { + ...data, + hotAlbums: data.hotAlbums.map((a: Album) => a.id), + } + db.upsert(Tables.ArtistAlbum, { + id: data.artist.id, + json: JSON.stringify(modifiedData), + updatedAt: Date.now(), + }) break } case CacheAPIs.Lyric: { if (!data.lrc) return - const id = Number(query.id) - const row = { id, json: JSON.stringify(data) } - await prisma.lyrics.upsert({ where: { id }, update: row, create: row }) + db.upsert(Tables.Lyrics, { + id: query.id, + json: JSON.stringify(data), + updatedAt: Date.now(), + }) + break + } + case CacheAPIs.CoverColor: { + if (!data.id || !data.color) return + if (/^#([a-fA-F0-9]){3}$|[a-fA-F0-9]{6}$/.test(data.color) === false) { + return + } + db.upsert(Tables.CoverColor, { + id: data.id, + color: data.color, + queriedAt: Date.now(), + }) break } - // case CacheAPIs.CoverColor: { - // if (!data.id || !data.color) return - // if (/^#([a-fA-F0-9]){3}$|[a-fA-F0-9]{6}$/.test(data.color) === false) { - // return - // } - // db.upsert(Tables.CoverColor, { - // id: data.id, - // color: data.color, - // queriedAt: Date.now(), - // }) - // break - // } case CacheAPIs.AppleMusicAlbum: { if (!data.id) return - const id = data.id - const row = { id, json: JSON.stringify(data) } - await prisma.appleMusicAlbum.upsert({ where: { id }, update: row, create: row }) + db.upsert(Tables.AppleMusicAlbum, { + id: data.id, + json: data.album ? JSON.stringify(data.album) : 'no', + updatedAt: Date.now(), + }) break } case CacheAPIs.AppleMusicArtist: { if (!data) return - const id = data.id - const row = { id, json: JSON.stringify(data) } - await prisma.artist.upsert({ where: { id }, update: row, create: row }) + db.upsert(Tables.AppleMusicArtist, { + id: data.id, + json: data.artist ? JSON.stringify(data.artist) : 'no', + updatedAt: Date.now(), + }) break } } } - async get( - api: T, - query: CacheAPIsParams[T] - ): Promise { + get(api: T, params: any): any { switch (api) { case CacheAPIs.UserPlaylist: case CacheAPIs.UserAccount: @@ -132,18 +142,17 @@ class Cache { case CacheAPIs.UserArtists: case CacheAPIs.ListenedRecords: case CacheAPIs.Likelist: { - const data = await prisma.accountData.findUnique({ where: { id: api } }) + const data = db.find(Tables.AccountData, api) if (data?.json) return JSON.parse(data.json) break } case CacheAPIs.Track: { - const typedQuery = query as CacheAPIsParams[CacheAPIs.Track] - const ids: number[] = typedQuery?.ids.split(',').map((id: string) => Number(id)) + const ids: number[] = params?.ids.split(',').map((id: string) => Number(id)) if (ids.length === 0) return if (ids.includes(NaN)) return - const tracksRaw = await prisma.track.findMany({ where: { id: { in: ids } } }) + const tracksRaw = db.findMany(Tables.Track, ids) if (tracksRaw.length !== ids.length) { return @@ -159,10 +168,8 @@ class Cache { } } case CacheAPIs.Album: { - const typedQuery = query as CacheAPIsParams[CacheAPIs.Album] - const id = Number(typedQuery?.id) - if (isNaN(id)) return - const data = await prisma.album.findUnique({ where: { id } }) + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.Album, params.id) if (data?.json) return { resourceState: true, @@ -173,86 +180,91 @@ class Cache { break } case CacheAPIs.Playlist: { - const typedQuery = query as CacheAPIsParams[CacheAPIs.Playlist] - const id = Number(typedQuery?.id) - if (isNaN(id)) return - const data = await prisma.playlist.findUnique({ where: { id } }) + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.Playlist, params.id) if (data?.json) return JSON.parse(data.json) break } case CacheAPIs.Artist: { - const typedQuery = query as CacheAPIsParams[CacheAPIs.Artist] - const id = Number(typedQuery?.id) - if (isNaN(id)) return - const data = await prisma.artist.findUnique({ where: { id } }) - if (data?.json) return JSON.parse(data.json) - break + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.Artist, params.id) + const fromAppleData = db.find(Tables.AppleMusicArtist, params.id) + const fromApple = fromAppleData?.json && JSON.parse(fromAppleData.json) + const fromNetease = data?.json && JSON.parse(data.json) + if (fromNetease && fromApple && fromApple !== 'no') { + fromNetease.artist.img1v1Url = fromApple.attributes.artwork.url + fromNetease.artist.briefDesc = fromApple.attributes.artistBio + } + return fromNetease ? fromNetease : undefined } case CacheAPIs.ArtistAlbum: { - const typedQuery = query as CacheAPIsParams[CacheAPIs.ArtistAlbum] - const id = Number(typedQuery?.id) - if (isNaN(id)) return + if (isNaN(Number(params?.id))) return - const artistAlbums = await prisma.artistAlbum.findUnique({ where: { id } }) - if (!artistAlbums?.hotAlbums) return - const ids = artistAlbums.hotAlbums.split(',').map(Number) + const artistAlbumsRaw = db.find(Tables.ArtistAlbum, params.id) + if (!artistAlbumsRaw?.json) return + const artistAlbums = JSON.parse(artistAlbumsRaw.json) - const albumsRaw = await prisma.album.findMany({ - where: { id: { in: ids } }, - }) - if (albumsRaw.length !== ids.length) return + const albumsRaw = db.findMany(Tables.Album, artistAlbums.hotAlbums) + if (albumsRaw.length !== artistAlbums.hotAlbums.length) return const albums = albumsRaw.map(a => JSON.parse(a.json)) - return { - hotAlbums: ids.map((id: number) => albums.find(a => a.id === id)), - } + + artistAlbums.hotAlbums = artistAlbums.hotAlbums.map((id: number) => + albums.find(a => a.id === id) + ) + return artistAlbums } case CacheAPIs.Lyric: { - const typedQuery = query as CacheAPIsParams[CacheAPIs.Lyric] - const id = Number(typedQuery?.id) - if (isNaN(id)) return - const data = await prisma.lyrics.findUnique({ where: { id } }) + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.Lyrics, params.id) if (data?.json) return JSON.parse(data.json) break } case CacheAPIs.CoverColor: { - // if (isNaN(Number(params?.id))) return - // return db.find(Tables.CoverColor, params.id)?.color + if (isNaN(Number(params?.id))) return + return db.find(Tables.CoverColor, params.id)?.color + } + case CacheAPIs.Artist: { + if (!params.ids?.length) return + const artists = db.findMany(Tables.Artist, params.ids) + if (artists.length !== params.ids.length) return + const result = artists.map(a => JSON.parse(a.json)) + result.sort((a, b) => { + const indexA: number = params.ids.indexOf(a.artist.id) + const indexB: number = params.ids.indexOf(b.artist.id) + return indexA - indexB + }) + return result } case CacheAPIs.AppleMusicAlbum: { - const typedQuery = query as CacheAPIsParams[CacheAPIs.AppleMusicAlbum] - const id = Number(typedQuery?.id) - if (isNaN(id)) return - const data = await prisma.appleMusicAlbum.findUnique({ where: { id } }) - if (data?.json) return JSON.parse(data.json) + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.AppleMusicAlbum, params.id) + if (data?.json && data.json !== 'no') return JSON.parse(data.json) break } case CacheAPIs.AppleMusicArtist: { - const typedQuery = query as CacheAPIsParams[CacheAPIs.AppleMusicArtist] - const id = Number(typedQuery?.id) - if (isNaN(id)) return - const data = await prisma.appleMusicArtist.findUnique({ where: { id } }) - if (data?.json) return JSON.parse(data.json) + if (isNaN(Number(params?.id))) return + const data = db.find(Tables.AppleMusicArtist, params.id) + if (data?.json && data.json !== 'no') return JSON.parse(data.json) break } } - return } - async getAudio(filename: string, reply: FastifyReply) { - if (!filename) { + getAudio(fileName: string, reply: FastifyReply) { + if (!fileName) { return reply.status(400).send({ error: 'No filename provided' }) } - const id = Number(filename.split('-')[0]) + const id = Number(fileName.split('-')[0]) try { - const path = `${app.getPath('userData')}/audio_cache/${filename}` + const path = `${app.getPath('userData')}/audio_cache/${fileName}` const audio = fs.readFileSync(path) if (audio.byteLength === 0) { - prisma.audio.delete({ where: { id } }) + db.delete(Tables.Audio, id) fs.unlinkSync(path) return reply.status(404).send({ error: 'Audio not found' }) } - await prisma.audio.update({ where: { id }, data: { updatedAt: new Date() } }) + db.update(Tables.Audio, id, { queriedAt: Date.now() }) reply .status(206) .header('Accept-Ranges', 'bytes') @@ -264,10 +276,7 @@ class Cache { } } - async setAudio( - buffer: Buffer, - { id, url, bitrate }: { id: number; url: string; bitrate: number } - ) { + async setAudio(buffer: Buffer, { id, url }: { id: number; url: string }) { const path = `${app.getPath('userData')}/audio_cache` try { @@ -277,8 +286,8 @@ class Cache { } const meta = await musicMetadata.parseBuffer(buffer) - const bitRate = ~~((meta.format.bitrate || bitrate || 0) / 1000) - const format = + const bitRate = meta?.format?.codec === 'OPUS' ? 165000 : meta.format.bitrate ?? 0 + const type = { 'MPEG 1 Layer 3': 'mp3', 'Ogg Vorbis': 'ogg', @@ -287,23 +296,25 @@ class Cache { OPUS: 'opus', }[meta.format.codec ?? ''] ?? 'unknown' - let source = 'unknown' + let source: TablesStructures[Tables.Audio]['source'] = 'unknown' if (url.includes('googlevideo.com')) source = 'youtube' if (url.includes('126.net')) source = 'netease' - fs.writeFile(`${path}/${id}-${bitRate}.${format}`, buffer, async error => { + fs.writeFile(`${path}/${id}-${bitRate}.${type}`, buffer, error => { if (error) { return log.error(`[cache] cacheAudio failed: ${error}`) } + log.info(`Audio file ${id}-${bitRate}.${type} cached!`) - const row = { id, bitRate, format, source } - await prisma.audio.upsert({ - where: { id }, - create: row, - update: row, + db.upsert(Tables.Audio, { + id, + bitRate, + format: type as TablesStructures[Tables.Audio]['format'], + source, + queriedAt: Date.now(), }) - log.info(`Audio file ${id}-${bitRate}.${format} cached!`) + log.info(`[cache] cacheAudio ${id}-${bitRate}.${type}`) }) } } diff --git a/packages/desktop/main/db.ts b/packages/desktop/main/db.ts new file mode 100644 index 0000000..1a02e48 --- /dev/null +++ b/packages/desktop/main/db.ts @@ -0,0 +1,243 @@ +import path from 'path' +import { app } from 'electron' +import fs from 'fs' +import SQLite3 from 'better-sqlite3' +import log from './log' +import { createFileIfNotExist, dirname } from './utils' +import { isProd } from './env' +import pkg from '../../../package.json' +import { compare, validate } from 'compare-versions' +import os from 'os' + +export const enum Tables { + Track = 'Track', + Album = 'Album', + Artist = 'Artist', + Playlist = 'Playlist', + ArtistAlbum = 'ArtistAlbum', + Lyrics = 'Lyrics', + Audio = 'Audio', + AccountData = 'AccountData', + CoverColor = 'CoverColor', + AppData = 'AppData', + AppleMusicAlbum = 'AppleMusicAlbum', + AppleMusicArtist = 'AppleMusicArtist', +} +interface CommonTableStructure { + id: number + json: string + updatedAt: number +} +export interface TablesStructures { + [Tables.Track]: CommonTableStructure + [Tables.Album]: CommonTableStructure + [Tables.Artist]: CommonTableStructure + [Tables.Playlist]: CommonTableStructure + [Tables.ArtistAlbum]: CommonTableStructure + [Tables.Lyrics]: CommonTableStructure + [Tables.AccountData]: { + id: string + json: string + updatedAt: number + } + [Tables.Audio]: { + id: number + bitRate: number + format: 'mp3' | 'flac' | 'ogg' | 'wav' | 'm4a' | 'aac' | 'unknown' | 'opus' + source: + | 'unknown' + | 'netease' + | 'migu' + | 'kuwo' + | 'kugou' + | 'youtube' + | 'qq' + | 'bilibili' + | 'joox' + queriedAt: number + } + [Tables.CoverColor]: { + id: number + color: string + queriedAt: number + } + [Tables.AppData]: { + id: 'appVersion' | 'skippedVersion' + value: string + } + [Tables.AppleMusicAlbum]: CommonTableStructure + [Tables.AppleMusicArtist]: CommonTableStructure +} + +type TableNames = keyof TablesStructures + +const readSqlFile = (filename: string) => { + return fs.readFileSync(path.join(dirname, `./migrations/${filename}`), 'utf8') +} + +class DB { + sqlite: SQLite3.Database + dbFilePath: string = path.resolve(app.getPath('userData'), './api_cache/db.sqlite') + + constructor() { + log.info('[db] Initializing database...') + + try { + createFileIfNotExist(this.dbFilePath) + + this.sqlite = new SQLite3(this.dbFilePath, { + nativeBinding: this.getBinPath(), + }) + this.sqlite.pragma('auto_vacuum = FULL') + this.initTables() + this.migrate() + + log.info('[db] Database initialized.') + } catch (e) { + log.error('[db] Database initialization failed.') + log.error(e) + } + } + + private getBinPath() { + console + const devBinPath = path.resolve( + app.getPath('userData'), + `../../bin/better_sqlite3_${os.platform}_${os.arch}.node` + ) + const prodBinPaths = { + darwin: path.resolve(app.getPath('exe'), `../../Resources/bin/better_sqlite3.node`), + win32: path.resolve(app.getPath('exe'), `../resources/bin/better_sqlite3.node`), + linux: '', + } + return isProd + ? prodBinPaths[os.platform as unknown as 'darwin' | 'win32' | 'linux'] + : devBinPath + } + + initTables() { + log.info('[db] Initializing database tables...') + const init = readSqlFile('init.sql') + this.sqlite.exec(init) + this.sqlite.pragma('journal_mode=WAL') + log.info('[db] Database tables initialized.') + } + + migrate() { + log.info('[db] Migrating database..') + + const key = 'appVersion' + const appVersion = this.find(Tables.AppData, key) + const updateAppVersionInDB = () => { + this.upsert(Tables.AppData, { + id: key, + value: pkg.version, + }) + } + + if (!appVersion?.value) { + updateAppVersionInDB() + return + } + + const sqlFiles = fs.readdirSync(path.join(dirname, './migrations')) + sqlFiles.forEach((sqlFile: string) => { + const version = sqlFile.split('.').shift() || '' + if (!validate(version)) return + if (compare(version, pkg.version, '>')) { + const file = readSqlFile(sqlFile) + this.sqlite.exec(file) + } + }) + + updateAppVersionInDB() + + log.info('[db] Database migrated.') + } + + find( + table: T, + key: TablesStructures[T]['id'] + ): TablesStructures[T] | undefined { + return this.sqlite.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`).get(key) + } + + findMany( + table: T, + keys: TablesStructures[T]['id'][] + ): TablesStructures[T][] { + const idsQuery = keys.map(key => `id = ${key}`).join(' OR ') + return this.sqlite.prepare(`SELECT * FROM ${table} WHERE ${idsQuery}`).all() + } + + findAll(table: T): TablesStructures[T][] { + return this.sqlite.prepare(`SELECT * FROM ${table}`).all() + } + + create(table: T, data: TablesStructures[T], skipWhenExist: boolean = true) { + if (skipWhenExist && db.find(table, data.id)) return + return this.sqlite.prepare(`INSERT INTO ${table} VALUES (?)`).run(data) + } + + createMany( + table: T, + data: TablesStructures[T][], + skipWhenExist: boolean = true + ) { + const valuesQuery = Object.keys(data[0]) + .map(key => `:${key}`) + .join(', ') + const insert = this.sqlite.prepare( + `INSERT ${skipWhenExist ? 'OR IGNORE' : ''} INTO ${table} VALUES (${valuesQuery})` + ) + const insertMany = this.sqlite.transaction((rows: any[]) => { + rows.forEach((row: any) => insert.run(row)) + }) + insertMany(data) + } + + update( + table: T, + key: TablesStructures[T]['id'], + data: Partial + ) { + // TODO: + } + + upsert(table: T, data: TablesStructures[T]) { + const valuesQuery = Object.keys(data) + .map(key => `:${key}`) + .join(', ') + return this.sqlite.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`).run(data) + } + + upsertMany(table: T, data: TablesStructures[T][]) { + const valuesQuery = Object.keys(data[0]) + .map(key => `:${key}`) + .join(', ') + const upsert = this.sqlite.prepare(`INSERT OR REPLACE INTO ${table} VALUES (${valuesQuery})`) + const upsertMany = this.sqlite.transaction((rows: any[]) => { + rows.forEach((row: any) => upsert.run(row)) + }) + upsertMany(data) + } + + delete(table: T, key: TablesStructures[T]['id']) { + return this.sqlite.prepare(`DELETE FROM ${table} WHERE id = ?`).run(key) + } + + deleteMany(table: T, keys: TablesStructures[T]['id'][]) { + const idsQuery = keys.map(key => `id = ${key}`).join(' OR ') + return this.sqlite.prepare(`DELETE FROM ${table} WHERE ${idsQuery}`).run() + } + + truncate(table: T) { + return this.sqlite.prepare(`DELETE FROM ${table}`).run() + } + + vacuum() { + return this.sqlite.prepare('VACUUM').run() + } +} + +export const db = new DB() diff --git a/packages/desktop/main/index.ts b/packages/desktop/main/index.ts index c53bf93..b524b87 100644 --- a/packages/desktop/main/index.ts +++ b/packages/desktop/main/index.ts @@ -12,7 +12,6 @@ import { createMenu } from './menu' import { isDev, isWindows, isLinux, isMac, appName } from './env' import store from './store' import initAppServer from './appServer/appServer' -import { initDatabase } from './prisma' class Main { win: BrowserWindow | null = null @@ -35,7 +34,6 @@ class Main { app.whenReady().then(async () => { log.info('[index] App ready') - await initDatabase() await initAppServer() this.createWindow() this.handleAppEvents() diff --git a/packages/desktop/main/ipcMain.ts b/packages/desktop/main/ipcMain.ts index c5a45cd..7e880d1 100644 --- a/packages/desktop/main/ipcMain.ts +++ b/packages/desktop/main/ipcMain.ts @@ -12,7 +12,7 @@ import { Thumbar } from './windowsTaskbar' import fastFolderSize from 'fast-folder-size' import path from 'path' import prettyBytes from 'pretty-bytes' -import prisma from './prisma' +import { db, Tables } from './db' const on = ( channel: T, @@ -204,7 +204,7 @@ function initOtherIpcMain() { * 退出登陆 */ handle(IpcChannels.Logout, async () => { - await prisma.accountData.deleteMany({}) + await db.truncate(Tables.AccountData) return true }) diff --git a/packages/desktop/main/prisma.ts b/packages/desktop/main/prisma.ts deleted file mode 100644 index 73c771b..0000000 --- a/packages/desktop/main/prisma.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { app } from 'electron' -import path from 'path' -import { PrismaClient } from '../prisma/client' -import { isDev, isWindows } from './env' -import log from './log' -import { createFileIfNotExist, dirname, isFileExist } from './utils' -import fs from 'fs' -import { dialog } from 'electron' - -export const dbPath = path.join(app.getPath('userData'), 'r3play.db') -export const dbUrl = 'file:' + (isWindows ? '' : '//') + dbPath -log.info('[prisma] dbUrl', dbUrl) - -const extraResourcesPath = app.getAppPath().replace('app.asar', '') // impacted by extraResources setting in electron-builder.yml -function getPlatformName(): string { - const isDarwin = process.platform === 'darwin' - if (isDarwin && process.arch === 'arm64') { - return process.platform + 'Arm64' - } - - return process.platform -} -const platformName = getPlatformName() -export const platformToExecutables: any = { - win32: { - migrationEngine: 'node_modules/@prisma/engines/migration-engine-windows.exe', - queryEngine: 'node_modules/@prisma/engines/query_engine-windows.dll.node', - }, - linux: { - migrationEngine: 'node_modules/@prisma/engines/migration-engine-debian-openssl-1.1.x', - queryEngine: 'node_modules/@prisma/engines/libquery_engine-debian-openssl-1.1.x.so.node', - }, - darwin: { - migrationEngine: 'node_modules/@prisma/engines/migration-engine-darwin', - queryEngine: 'node_modules/@prisma/engines/libquery_engine-darwin.dylib.node', - }, - darwinArm64: { - migrationEngine: 'node_modules/@prisma/engines/migration-engine-darwin-arm64', - queryEngine: 'node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node', - }, -} -export const queryEnginePath = path.join( - extraResourcesPath, - platformToExecutables[platformName].queryEngine -) - -log.info('[prisma] dbUrl', dbUrl) - -// Hacky, but putting this here because otherwise at query time the Prisma client -// gives an error "Environment variable not found: DATABASE_URL" despite us passing -// the dbUrl into the prisma client constructor in datasources.db.url -process.env.DATABASE_URL = dbUrl - -createFileIfNotExist(dbPath) - -// @ts-expect-error -let prisma: PrismaClient = null -try { - prisma = new PrismaClient({ - log: isDev ? ['info', 'warn', 'error'] : ['error'], - datasources: { - db: { - url: dbUrl, - }, - }, - // see https://github.com/prisma/prisma/discussions/5200 - // @ts-expect-error internal prop - // __internal: { - // engine: { - // binaryPath: queryEnginePath, - // }, - // }, - }) - log.info('[prisma] prisma initialized') -} catch (e) { - log.error('[prisma] failed to init prisma', e) - dialog.showErrorBox('Failed to init prisma', String(e)) - app.exit() -} - -export const initDatabase = async () => { - try { - const initSQLFile = fs - .readFileSync(path.join(dirname, 'migrations/init.sql'), 'utf-8') - .toString() - const tables = initSQLFile.split(';') - await Promise.all( - tables.map(sql => { - if (!sql.trim()) return - return prisma.$executeRawUnsafe(sql.trim()).catch(() => { - log.error('[prisma] failed to execute init sql >>> ', sql.trim()) - }) - }) - ) - } catch (e) { - dialog.showErrorBox('Failed to init prisma database', String(e)) - app.exit() - } - log.info('[prisma] database initialized') -} - -export default prisma diff --git a/packages/desktop/migrations/init.sql b/packages/desktop/migrations/init.sql index 444fca2..9ef96ac 100644 --- a/packages/desktop/migrations/init.sql +++ b/packages/desktop/migrations/init.sql @@ -1,76 +1,77 @@ CREATE TABLE IF NOT EXISTS "AccountData" ( - "id" TEXT NOT NULL PRIMARY KEY, + "id" TEXT NOT NULL, "json" TEXT NOT NULL, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "AppData" ( - "id" TEXT NOT NULL PRIMARY KEY, - "value" TEXT NOT NULL + "id" TEXT NOT NULL, + "value" TEXT NOT NULL, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "Track" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "json" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "Album" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "json" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "Artist" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "json" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); - CREATE TABLE IF NOT EXISTS "ArtistAlbum" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "hotAlbums" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "Playlist" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "json" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "Audio" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "bitRate" INTEGER NOT NULL, "format" TEXT NOT NULL, "source" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, + "queriedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "Lyrics" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "json" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "AppleMusicAlbum" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "json" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS "AppleMusicArtist" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL, "json" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) ); diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 005ed16..47ed484 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -6,7 +6,7 @@ "main": "./main/index.js", "author": "*", "scripts": { - "postinstall": "prisma generate", + "postinstall": "tsx scripts/build.sqlite3.ts", "dev": "tsx scripts/build.main.ts --watch", "build": "tsx scripts/build.main.ts", "pack": "electron-builder build -c .electron-builder.config.js", @@ -14,9 +14,7 @@ "test:types": "tsc --noEmit --project ./tsconfig.json", "test": "vitest", "test:ui": "vitest --ui", - "test:coverage": "vitest run --coverage", - "prisma:generate": "prisma generate", - "prisma:db-push": "prisma db push" + "test:coverage": "vitest run --coverage" }, "engines": { "node": ">=16.0.0" @@ -26,11 +24,10 @@ "@fastify/http-proxy": "^8.4.0", "@fastify/multipart": "^7.4.0", "@fastify/static": "^6.6.1", - "@prisma/client": "^4.8.1", - "@prisma/engines": "^4.9.0", "@sentry/electron": "^3.0.7", "@yimura/scraper": "^1.2.4", "NeteaseCloudMusicApi": "^4.8.9", + "better-sqlite3": "8.1.0", "change-case": "^4.1.2", "compare-versions": "^4.1.3", "electron-log": "^4.4.8", @@ -39,22 +36,21 @@ "fastify": "^4.5.3", "http-proxy-agent": "^5.0.0", "pretty-bytes": "^6.0.0", - "prisma": "^4.8.1", "ytdl-core": "^4.11.2" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.3", "@vitest/ui": "^0.20.3", "axios": "^1.2.1", "cross-env": "^7.0.3", "dotenv": "^16.0.3", - "electron": "^22.1.0", + "electron": "^23.1.1", "electron-builder": "23.6.0", "electron-devtools-installer": "^3.2.0", "electron-rebuild": "^3.2.9", - "electron-releases": "^3.1171.0", "esbuild": "^0.16.10", - "minimist": "^1.2.7", - "music-metadata": "^8.1.0", + "minimist": "^1.2.8", + "music-metadata": "^8.1.3", "open-cli": "^7.1.0", "ora": "^6.1.2", "picocolors": "^1.0.0", diff --git a/packages/desktop/prisma/schema.prisma b/packages/desktop/prisma/schema.prisma deleted file mode 100644 index 9ed7403..0000000 --- a/packages/desktop/prisma/schema.prisma +++ /dev/null @@ -1,86 +0,0 @@ -generator client { - provider = "prisma-client-js" - output = "./client" - binaryTargets = ["native", "darwin", "darwin-arm64"] -} - -datasource db { - provider = "sqlite" - url = env("DATABASE_URL") -} - -model AccountData { - id String @id @unique - json String - updatedAt DateTime @updatedAt -} - -model AppData { - id String @id @unique - value String -} - -model Track { - id Int @id @unique - json String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model Album { - id Int @id @unique - json String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model Artist { - id Int @id @unique - json String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model ArtistAlbum { - id Int @id @unique - hotAlbums String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model Playlist { - id Int @id @unique - json String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model Audio { - id Int @id @unique - bitRate Int - format String - source String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model Lyrics { - id Int @id @unique - json String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model AppleMusicAlbum { - id Int @id @unique - json String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model AppleMusicArtist { - id Int @id @unique - json String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} diff --git a/packages/desktop/scripts/build.main.ts b/packages/desktop/scripts/build.main.ts index 2abe334..2bb19f6 100644 --- a/packages/desktop/scripts/build.main.ts +++ b/packages/desktop/scripts/build.main.ts @@ -35,6 +35,7 @@ const options = { ...builtinModules.filter(x => !/^_|^(internal|v8|node-inspect)\/|\//.test(x)), 'electron', 'NeteaseCloudMusicApi', + 'better-sqlite3', ], } diff --git a/packages/desktop/scripts/build.sqlite3.ts b/packages/desktop/scripts/build.sqlite3.ts new file mode 100644 index 0000000..7b2eb86 --- /dev/null +++ b/packages/desktop/scripts/build.sqlite3.ts @@ -0,0 +1,163 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { rebuild } = require('electron-rebuild') +const fs = require('fs') +const minimist = require('minimist') +const pc = require('picocolors') +const pkg = require(`${process.cwd()}/package.json`) +const axios = require('axios') +const { execSync } = require('child_process') +const { resolve } = require('path') +const { promisify } = require('util') +const stream = require('stream') + +type Arch = typeof process.arch + +const isWindows = process.platform === 'win32' +const isMac = process.platform === 'darwin' +const isLinux = process.platform === 'linux' + +const argv = minimist(process.argv.slice(2)) +const electronVersion = pkg.devDependencies.electron.replaceAll('^', '') +const betterSqlite3Version = pkg.dependencies['better-sqlite3'].replaceAll('^', '') + +const projectDir = resolve(process.cwd(), '../../') +const tmpDir = resolve(projectDir, `./tmp/better-sqlite3`) +const binDir = resolve(projectDir, `./tmp/bin`) +console.log(pc.cyan(`projectDir=${projectDir}`)) +console.log(pc.cyan(`binDir=${binDir}`)) + +const finished = promisify(stream.finished) + +if (!fs.existsSync(binDir)) { + console.log(pc.cyan(`Creating dist/binary directory: ${binDir}`)) + fs.mkdirSync(binDir, { + recursive: true, + }) +} + +// Get Electron Module Version +let electronModuleVersion = '' +async function getElectronModuleVersion() { + const releases = await axios.get('https://releases.electronjs.org/releases.json') + if (!releases.data) { + console.error(pc.red('Can not get electron releases')) + process.exit(1) + } + electronModuleVersion = releases.data.find(r => r.version.includes(electronVersion))?.modules + if (!electronModuleVersion) { + console.error(pc.red('Can not find electron module version in electron-releases')) + process.exit(1) + } + console.log(pc.cyan(`electronModuleVersion=${electronModuleVersion}`)) +} + +// Download better-sqlite library from GitHub Release +async function download(arch: Arch) { + console.log(pc.cyan(`Downloading ${arch} binary...`)) + if (!electronModuleVersion) { + console.log(pc.red('No electron module version found! Skip download.')) + return false + } + const fileName = `better-sqlite3-v${betterSqlite3Version}-electron-v${electronModuleVersion}-${process.platform}-${arch}` + const zipFileName = `${fileName}.tar.gz` + const url = `https://github.com/JoshuaWise/better-sqlite3/releases/download/v${betterSqlite3Version}/${zipFileName}` + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir, { + recursive: true, + }) + } + + try { + await axios({ + method: 'get', + url, + responseType: 'stream', + }).then(response => { + const writer = fs.createWriteStream(resolve(tmpDir, `./${zipFileName}`)) + response.data.pipe(writer) + return finished(writer) + }) + } catch (e) { + console.log(pc.red('Download failed! Skip download.', e)) + return false + } + + try { + execSync(`tar -xvzf ${tmpDir}/${zipFileName} -C ${tmpDir}`) + } catch (e) { + console.log(pc.red('Extract failed! Skip extract.', e)) + return false + } + + try { + fs.copyFileSync( + resolve(tmpDir, './build/Release/better_sqlite3.node'), + resolve(binDir, `./better_sqlite3_${process.platform}_${arch}.node`) + ) + } catch (e) { + console.log(pc.red('Copy failed! Skip copy.', e)) + return false + } + + try { + fs.rmSync(resolve(tmpDir, `./build`), { recursive: true, force: true }) + } catch (e) { + console.log(pc.red('Delete failed! Skip delete.')) + return false + } + + return true +} + +// Build better-sqlite library on this device +async function build(arch: Arch) { + const downloaded = await download(arch) + if (downloaded) { + return + } + + console.log(pc.cyan(`Building for ${arch}...`)) + await rebuild({ + projectRootPath: projectDir, + buildPath: process.cwd(), + electronVersion, + arch, + onlyModules: ['better-sqlite3'], + force: true, + }) + .then(() => { + console.info('Build succeeded') + + const from = resolve( + projectDir, + `./node_modules/better-sqlite3/build/Release/better_sqlite3.node` + ) + const to = resolve(binDir, `./better_sqlite3_${process.platform}_${arch}.node`) + console.info(`copy ${from} to ${to}`) + fs.copyFileSync(from, to) + }) + .catch(e => { + console.error(pc.red('Build failed!')) + console.error(pc.red(e)) + }) +} + +async function main() { + await getElectronModuleVersion() + if (argv.x64 || argv.arm64 || argv.arm) { + if (argv.x64) await build('x64') + if (argv.arm64) await build('arm64') + } else { + if (isWindows) { + await build('x64') + } else if (isMac) { + await build('x64') + await build('arm64') + } else if (isLinux) { + await build('x64') + await build('arm64') + } + } +} + +main() diff --git a/packages/desktop/scripts/copySQLite3.js b/packages/desktop/scripts/copySQLite3.js new file mode 100644 index 0000000..5d6d90b --- /dev/null +++ b/packages/desktop/scripts/copySQLite3.js @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require('path') +const pc = require('picocolors') +const fs = require('fs') + +const archs = ['ia32', 'x64', 'armv7l', 'arm64', 'universal'] + +const projectDir = path.resolve(process.cwd(), '../../') +const binDir = `${projectDir}/tmp/bin` +console.log(pc.cyan(`projectDir=${projectDir}`)) +console.log(pc.cyan(`binDir=${binDir}`)) + +exports.default = async function (context) { + // console.log(context) + const platform = context.electronPlatformName + const arch = archs?.[context.arch] + + // Mac + if (platform === 'darwin') { + if (arch === 'universal') return // Skip universal we already copy binary for x64 and arm64 + if (arch !== 'x64' && arch !== 'arm64') return // Skip other archs + + const from = `${binDir}/better_sqlite3_darwin_${arch}.node` + const to = `${context.appOutDir}/${context.packager.appInfo.productFilename}.app/Contents/Resources/bin/better_sqlite3.node` + console.info(`copy ${from} to ${to}`) + + const toFolder = to.replace('/better_sqlite3.node', '') + if (!fs.existsSync(toFolder)) { + fs.mkdirSync(toFolder, { + recursive: true, + }) + } + + try { + fs.copyFileSync(from, to) + } catch (e) { + console.log(pc.red('Copy failed! Process stopped.')) + throw e + } + } + + if (platform === 'win32') { + if (arch !== 'x64') return // Skip other archs + + const from = `${binDir}/better_sqlite3_win32_${arch}.node` + const to = `${context.appOutDir}/resources/bin/better_sqlite3.node` + console.info(`copy ${from} to ${to}`) + + const toFolder = to.replace('/better_sqlite3.node', '') + if (!fs.existsSync(toFolder)) { + fs.mkdirSync(toFolder, { + recursive: true, + }) + } + + try { + fs.copyFileSync(from, to) + } catch (e) { + console.log(pc.red('Copy failed! Process stopped.')) + throw e + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c44e01..52c8a3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,36 +26,34 @@ importers: '@fastify/http-proxy': ^8.4.0 '@fastify/multipart': ^7.4.0 '@fastify/static': ^6.6.1 - '@prisma/client': ^4.8.1 - '@prisma/engines': ^4.9.0 '@sentry/electron': ^3.0.7 + '@types/better-sqlite3': ^7.6.3 '@vitest/ui': ^0.20.3 '@yimura/scraper': ^1.2.4 NeteaseCloudMusicApi: ^4.8.9 axios: ^1.2.1 + better-sqlite3: 8.1.0 change-case: ^4.1.2 compare-versions: ^4.1.3 cross-env: ^7.0.3 dotenv: ^16.0.3 - electron: ^22.1.0 + electron: ^23.1.1 electron-builder: 23.6.0 electron-devtools-installer: ^3.2.0 electron-log: ^4.4.8 electron-rebuild: ^3.2.9 - electron-releases: ^3.1171.0 electron-store: ^8.1.0 esbuild: ^0.16.10 fast-folder-size: ^1.7.1 fastify: ^4.5.3 http-proxy-agent: ^5.0.0 - minimist: ^1.2.7 - music-metadata: ^8.1.0 + minimist: ^1.2.8 + music-metadata: ^8.1.3 open-cli: ^7.1.0 ora: ^6.1.2 picocolors: ^1.0.0 prettier: '*' pretty-bytes: ^6.0.0 - prisma: ^4.8.1 tsx: '*' typescript: '*' vitest: ^0.20.3 @@ -66,11 +64,10 @@ importers: '@fastify/http-proxy': 8.4.0 '@fastify/multipart': 7.4.0 '@fastify/static': 6.6.1 - '@prisma/client': 4.8.1_prisma@4.8.1 - '@prisma/engines': 4.9.0 '@sentry/electron': 3.0.7 '@yimura/scraper': 1.2.4 NeteaseCloudMusicApi: 4.8.9 + better-sqlite3: 8.1.0 change-case: 4.1.2 compare-versions: 4.1.3 electron-log: 4.4.8 @@ -79,21 +76,20 @@ importers: fastify: 4.5.3 http-proxy-agent: 5.0.0 pretty-bytes: 6.0.0 - prisma: 4.8.1 ytdl-core: 4.11.2 devDependencies: + '@types/better-sqlite3': 7.6.3 '@vitest/ui': 0.20.3 axios: 1.2.1 cross-env: 7.0.3 dotenv: 16.0.3 - electron: 22.2.0 + electron: 23.1.1 electron-builder: 23.6.0 electron-devtools-installer: 3.2.0 electron-rebuild: 3.2.9 - electron-releases: 3.1171.0 esbuild: 0.16.10 - minimist: 1.2.7 - music-metadata: 8.1.0 + minimist: 1.2.8 + music-metadata: 8.1.3 open-cli: 7.1.0 ora: 6.1.2 picocolors: 1.0.0 @@ -2174,11 +2170,6 @@ packages: resolution: {integrity: sha512-93tctjNXcIS+i/e552IO6tqw17sX8liivv8WX9lDMCpEEe3ci+nT9F+1oHtAafqruXLepKF80i/D20Mm+ESlOw==} requiresBuild: true - /@prisma/engines/4.9.0: - resolution: {integrity: sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==} - requiresBuild: true - dev: false - /@remix-run/router/1.2.1: resolution: {integrity: sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ==} engines: {node: '>=14'} @@ -2677,12 +2668,18 @@ packages: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} dev: true + /@types/better-sqlite3/7.6.3: + resolution: {integrity: sha512-YS64N9SNDT/NAvou3QNdzAu3E2om/W/0dhORimtPGLef+zSK5l1vDzfsWb4xgXOgfhtOI5ZDTRxnvRPb22AIVQ==} + dependencies: + '@types/node': 18.11.17 + dev: true + /@types/cacheable-request/6.0.2: resolution: {integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==} dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 18.6.4 + '@types/node': 18.11.17 '@types/responselike': 1.0.0 dev: true @@ -2847,7 +2844,7 @@ packages: /@types/responselike/1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 18.6.4 + '@types/node': 18.11.17 dev: true /@types/scheduler/0.16.2: @@ -3473,7 +3470,14 @@ packages: /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true + + /better-sqlite3/8.1.0: + resolution: {integrity: sha512-p1m09H+Oi8R9TPj810pdNswMFuVgRNgCJEWypp6jlkOgSwMIrNyuj3hW78xEuBRGok5RzeaUW8aBtTWF3l/TQA==} + requiresBuild: true + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.1 + dev: false /big-integer/1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} @@ -3495,13 +3499,18 @@ packages: chainsaw: 0.1.0 dev: false + /bindings/1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + dependencies: + file-uri-to-path: 1.0.0 + dev: false + /bl/4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.0 - dev: true /bl/5.0.0: resolution: {integrity: sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==} @@ -3638,7 +3647,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true /buffer/6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -3936,6 +3944,10 @@ packages: optionalDependencies: fsevents: 2.3.2 + /chownr/1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + /chownr/2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -4105,7 +4117,7 @@ packages: resolution: {integrity: sha512-FOz6gc7VRNFV+SYjGv0ZPfgG1afGqJfKg2/aVtiDP6kF1ZPpo7NvUOGHuVNQ2YgMoCVglEUo0O3WzcD63yczVA==} dependencies: leven: 3.1.0 - minimist: 1.2.7 + minimist: 1.2.8 dev: false /common-tags/1.8.2: @@ -4456,7 +4468,6 @@ packages: engines: {node: '>=10'} dependencies: mimic-response: 3.1.0 - dev: true /deep-eql/3.0.1: resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==} @@ -4472,6 +4483,11 @@ packages: type-detect: 4.0.8 dev: true + /deep-extend/0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -4578,7 +4594,6 @@ packages: /detect-libc/2.0.1: resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} engines: {node: '>=8'} - dev: true /detect-node/2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} @@ -4592,7 +4607,7 @@ packages: dependencies: acorn-node: 1.8.2 defined: 1.0.0 - minimist: 1.2.7 + minimist: 1.2.8 /didyoumean/1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -4817,7 +4832,7 @@ packages: compare-version: 0.1.2 debug: 2.6.9 isbinaryfile: 3.0.3 - minimist: 1.2.7 + minimist: 1.2.8 plist: 3.0.6 transitivePeerDependencies: - supports-color @@ -4861,10 +4876,6 @@ packages: - supports-color dev: true - /electron-releases/3.1171.0: - resolution: {integrity: sha512-4KhfrPNTh0e5joJtMmfDw954hUriEaa/vx3sLHgFfbkVdIC9QI6bnVirVPQou57LeNNKSgJHalUb+DK3EX4RBw==} - dev: true - /electron-store/8.1.0: resolution: {integrity: sha512-2clHg/juMjOH0GT9cQ6qtmIvK183B39ZXR0bUoPwKwYHJsEF3quqyDzMFUAu+0OP8ijmN2CbPRAelhNbWUbzwA==} dependencies: @@ -4876,8 +4887,8 @@ packages: resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==} dev: true - /electron/22.2.0: - resolution: {integrity: sha512-puRZSF2vWJ4pz3oetL5Td8LcuivTWz3MoAk/gjImHSN1B/2VJNEQlw1jGdkte+ppid2craOswE2lmCOZ7SwF1g==} + /electron/23.1.1: + resolution: {integrity: sha512-junV1NzPx5T8Mx9+o8fMWK1Q5WOtG5vggiM09PGYg/6zTcGDL3DjGVHeUa/97gx7ErXL37DrFnxNg6+ePWprDg==} engines: {node: '>= 12.20.55'} hasBin: true requiresBuild: true @@ -5634,6 +5645,11 @@ packages: - supports-color dev: true + /expand-template/2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + /express-fileupload/1.4.0: resolution: {integrity: sha512-RjzLCHxkv3umDeZKeFeMg8w7qe0V09w3B7oGZprr/oO2H/ISCgNzuqzn7gV3HRWb37GjRk429CCpSLS2KNTqMQ==} engines: {node: '>=12.0.0'} @@ -5898,6 +5914,10 @@ packages: token-types: 5.0.1 dev: true + /file-uri-to-path/1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + dev: false + /file-uri-to-path/2.0.0: resolution: {integrity: sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==} engines: {node: '>= 6'} @@ -6053,6 +6073,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /fs-constants/1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + /fs-extra/10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -6223,6 +6247,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /github-from-package/0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -6656,6 +6684,10 @@ packages: /inherits/2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /ini/1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + /inline-style-prefixer/6.0.1: resolution: {integrity: sha512-AsqazZ8KcRzJ9YPN1wMH2aNM7lkWQ8tSPrW5uDk1ziYwiAPWSZnUsC7lfZq+BDqLqz0B4Pho5wscWcJzVvRzDQ==} dependencies: @@ -7171,7 +7203,7 @@ packages: resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} hasBin: true dependencies: - minimist: 1.2.7 + minimist: 1.2.8 dev: true /json5/2.2.1: @@ -7671,7 +7703,6 @@ packages: /mimic-response/3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - dev: true /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} @@ -7709,8 +7740,8 @@ packages: kind-of: 6.0.3 dev: true - /minimist/1.2.7: - resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + /minimist/1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} /minipass-collect/1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} @@ -7774,11 +7805,15 @@ packages: is-extendable: 1.0.1 dev: true + /mkdirp-classic/0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + /mkdirp/0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true dependencies: - minimist: 1.2.7 + minimist: 1.2.8 /mkdirp/1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} @@ -7824,8 +7859,8 @@ packages: - supports-color dev: false - /music-metadata/8.1.0: - resolution: {integrity: sha512-Jyleh/645rPISMo+bZ3jua5QbFVL32xA+x3KoJPZeRHcPTVizCVUtAqJLtLVaPPQrIrLjgagv3DQlpP67lb3Dw==} + /music-metadata/8.1.3: + resolution: {integrity: sha512-Wo+gR2tt8vbricyaK/sMt/4o/YEjStJOD806iZzmLtM/9GrBvhkIDOijt9J8vdRkH4rgYDI4xNu8dp0O0vDryw==} engines: {node: ^14.13.1 || >=16.0.0} dependencies: '@tokenizer/token': 0.3.0 @@ -7881,6 +7916,10 @@ packages: - supports-color dev: true + /napi-build-utils/1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false + /natural-compare/1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -7906,7 +7945,6 @@ packages: engines: {node: '>=10'} dependencies: semver: 7.3.7 - dev: true /node-addon-api/1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} @@ -8381,7 +8419,7 @@ packages: fast-safe-stringify: 2.1.1 help-me: 4.0.1 joycon: 3.1.1 - minimist: 1.2.7 + minimist: 1.2.8 on-exit-leak-free: 1.0.0 pino-abstract-transport: 1.0.0 pump: 3.0.0 @@ -8561,6 +8599,25 @@ packages: posthtml-render: 1.4.0 dev: true + /prebuild-install/7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.1 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.24.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + /prelude-ls/1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -8746,6 +8803,16 @@ packages: unpipe: 1.0.0 dev: false + /rc/1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -9443,6 +9510,18 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /simple-concat/1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get/4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + /simple-update-notifier/1.1.0: resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} engines: {node: '>=8.10.0'} @@ -9820,6 +9899,11 @@ packages: min-indent: 1.0.1 dev: true + /strip-json-comments/2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false + /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -9975,6 +10059,26 @@ packages: transitivePeerDependencies: - ts-node + /tar-fs/2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + + /tar-stream/2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + /tar/6.1.11: resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==} engines: {node: '>= 10'} @@ -10288,6 +10392,12 @@ packages: fsevents: 2.3.2 dev: true + /tunnel-agent/0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /tunnel/0.0.6: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} @@ -10956,7 +11066,7 @@ packages: axios: 0.27.2 joi: 17.7.0 lodash: 4.17.21 - minimist: 1.2.7 + minimist: 1.2.8 rxjs: 7.8.0 transitivePeerDependencies: - debug