commit 9392c1077906959e7c2721891938cc75c615b47d Author: leiurayer <1432593898@qq.com> Date: Sun Jul 16 01:04:01 2023 +0800 init v2.0.x diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d79fa5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +src/Config/Settings_debug.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + 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. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU 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. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbd78f7 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# 哔哩下载姬 + +

+ + GitHub Repo stars + + + GitHub forks + + + GitHub issues + + + GitHub + +

+ +![index.png](https://s2.loli.net/2022/06/04/dOsqtfBXceRgrj2.png) + +哔哩下载姬(DownKyi)是一个简单易用的哔哩哔哩视频下载工具,具有简洁的界面,流畅的操作逻辑。哔哩下载姬可以下载几乎所有的B站视频,并输出mp4格式的文件;采用Aria下载器多线程下载,采用FFmpeg对视频进行混流、提取音视频等操作。 + +[更多详情](src/README.md) + +## 下载 + +

+ + GitHub release (latest by date) + + + GitHub Release Date + + + GitHub all releases + +

+ +[更新日志](CHANGELOG.md) + +## 问题 + +- Aria下载失败:检查aria2c.exe是否可以正常工作和是否允许通过防火墙;或者尝试切换端口号。 +- 内建下载器失败:请提issue,也欢迎pr。 +- 下载时卡在“混流中”:检查ffmpeg.exe是否可以正常工作。 +- 去水印:宽/高为水印的尺寸,X/Y为水印在图像中的位置(以左上角为原点),这四个数据可通过Photoshop获得。 + +## 赞助 + +如果这个项目对您有很大帮助,并且您希望支持该项目的开发和维护,请随时扫描一下二维码进行捐赠。非常感谢您的捐款,谢谢! + +![Alipay.png](https://s2.loli.net/2022/06/04/6LpfinSa5FoZmNB.png)![WeChat.png](https://s2.loli.net/2022/06/04/2yotOSvwmahPdXU.png) + +## 开发 + +### x86 & x64 + +发布的压缩包中aria2c.exe和ffmpeg.exe均为32位,如果需要请用下面链接中的文件替换。 + +- [aria2-1.36.0-win-32bit](third_party/aria2-1.36.0-win-32bit-build1.zip) +- [aria2-1.36.0-win-64bit](third_party/aria2-1.36.0-win-64bit-build1.zip) +- [FFmpeg](https://github.com/leiurayer/FFmpeg-Builds/releases/tag/latest) + +### 相关项目 + +- [哔哩哔哩-API收集整理](https://github.com/SocialSisterYi/bilibili-API-collect):B站API归档 +- [Prism](https://github.com/PrismLibrary/Prism):MVVM框架 +- [WebPSharp](https://github.com/leiurayer/WebPSharp):WebP格式图片支持,[NuGet程序包](third_party/WebPSharp.0.5.1.nupkg) + +## 免责申明 + +1. 本软件只提供视频解析,不提供任何资源上传、存储到服务器的功能。 +2. 本软件仅解析来自B站的内容,不会对解析到的音视频进行二次编码,部分视频会进行有限的格式转换、拼接等操作。 +3. 本软件解析得到的所有内容均来自B站UP主上传、分享,其版权均归原作者所有。内容提供者、上传者(UP主)应对其提供、上传的内容承担全部责任。 +4. **本软件提供的所有内容,仅可用作学习交流使用,未经原作者授权,禁止用于其他用途。请在下载24小时内删除。为尊重作者版权,请前往资源的原始发布网站观看,支持原创,谢谢。** +5. 因使用本软件产生的版权问题,软件作者概不负责。 diff --git a/images/Alipay.png b/images/Alipay.png new file mode 100644 index 0000000..f33b7db Binary files /dev/null and b/images/Alipay.png differ diff --git a/images/WeChat.png b/images/WeChat.png new file mode 100644 index 0000000..8ef8a02 Binary files /dev/null and b/images/WeChat.png differ diff --git a/images/app/index.png b/images/app/index.png new file mode 100644 index 0000000..a950b9a Binary files /dev/null and b/images/app/index.png differ diff --git a/src/.gitattributes b/src/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/src/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/src/BiliSharp.UnitTest/Api/Login/TestLoginInfo.cs b/src/BiliSharp.UnitTest/Api/Login/TestLoginInfo.cs new file mode 100644 index 0000000..84c50d0 --- /dev/null +++ b/src/BiliSharp.UnitTest/Api/Login/TestLoginInfo.cs @@ -0,0 +1,35 @@ +using BiliSharp.Api.Login; + +namespace BiliSharp.UnitTest.Api.Login; + +public class TestLoginInfo +{ + [Fact] + public void TestGetNavigationInfo_Default() + { + Cookies.GetMyCookies(); + + long mid = 42018135; + var info = LoginInfo.GetNavigationInfo(); + Assert.Equal(mid, info.Data.Mid); + } + + [Fact] + public void TestGetLoginInfoStat_Default() + { + Cookies.GetMyCookies(); + + var stat = LoginInfo.GetLoginInfoStat(); + Assert.NotNull(stat); + } + + [Fact] + public void TestGetMyCoin_Default() + { + Cookies.GetMyCookies(); + + var coin = LoginInfo.GetMyCoin(); + Assert.NotNull(coin); + } + +} \ No newline at end of file diff --git a/src/BiliSharp.UnitTest/Api/Login/TestLoginNotice.cs b/src/BiliSharp.UnitTest/Api/Login/TestLoginNotice.cs new file mode 100644 index 0000000..1130ec4 --- /dev/null +++ b/src/BiliSharp.UnitTest/Api/Login/TestLoginNotice.cs @@ -0,0 +1,16 @@ +using BiliSharp.Api.Login; + +namespace BiliSharp.UnitTest.Api.Login; + +public class TestLoginNotice +{ + [Fact] + public void TestGetLoginNotice_Default() + { + Cookies.GetMyCookies(); + + long mid = 42018135; + var notice = LoginNotice.GetLoginNotice(mid); + Assert.Equal(mid, notice.Data.Mid); + } +} \ No newline at end of file diff --git a/src/BiliSharp.UnitTest/Api/Login/TestLoginQR.cs b/src/BiliSharp.UnitTest/Api/Login/TestLoginQR.cs new file mode 100644 index 0000000..4c21328 --- /dev/null +++ b/src/BiliSharp.UnitTest/Api/Login/TestLoginQR.cs @@ -0,0 +1,23 @@ +using BiliSharp.Api.Login; + +namespace BiliSharp.UnitTest.Api.Login; + +public class TestLoginQR +{ + [Fact] + public void TestGenerateQRCode_Default() + { + var qrcode = LoginQR.GenerateQRCode(); + Assert.NotNull(qrcode.Data); + } + + [Fact] + public void TestPollQRCode_Default() + { + var qrcode = LoginQR.GenerateQRCode(); + Assert.NotNull(qrcode.Data); + + var poll = LoginQR.PollQRCode(qrcode.Data.QrcodeKey); + Assert.NotNull(poll.Data); + } +} \ No newline at end of file diff --git a/src/BiliSharp.UnitTest/Api/Sign/TestWbiSign.cs b/src/BiliSharp.UnitTest/Api/Sign/TestWbiSign.cs new file mode 100644 index 0000000..93231b8 --- /dev/null +++ b/src/BiliSharp.UnitTest/Api/Sign/TestWbiSign.cs @@ -0,0 +1,87 @@ +using BiliSharp.Api.Sign; + +namespace BiliSharp.UnitTest.Api.Sign +{ + public class TestWbiSign + { + [Fact] + public void TestParametersToQuery_Default() + { + var parameters = new Dictionary + { + { "fourk", 1 }, + { "fnver", 0 }, + { "fnval", 4048 }, + { "cid", 405595939 }, + { "qn", 120 }, + { "bvid", "BV1Sg411F7cb" }, + { "aid", 505421421 } + }; + string expected = "fourk=1&fnver=0&fnval=4048&cid=405595939&qn=120&bvid=BV1Sg411F7cb&aid=505421421"; + + string query = WbiSign.ParametersToQuery(parameters); + Assert.Equal(expected, query); + } + + [Fact] + public void TestParametersToQuery_Empty() + { + var parameters = new Dictionary(); + string query = WbiSign.ParametersToQuery(parameters); + Assert.Equal("", query); + } + + [Fact] + public void TestKeys_Default() + { + var key1 = WbiSign.GetKey(); + Assert.NotNull(key1); + + string imgKey = "34478ba821254d9d93542680e3b86100"; + string subKey = "7e16a90d190a4355a78fd00b32a38de6"; + var keys = new Tuple(imgKey, subKey); + WbiSign.SetKey(keys); + + var key2 = WbiSign.GetKey(); + Assert.Equal(imgKey, key2.Item1); + Assert.Equal(subKey, key2.Item2); + } + + [Fact] + public void TestEncodeWbi_Default() + { + var parameters = new Dictionary + { + { "fourk", 1 }, + { "fnver", 0 }, + { "fnval", 4048 }, + { "cid", 405595939 }, + { "qn", 120 }, + { "bvid", "BV1Sg411F7cb" }, + { "aid", 505421421 } + }; + + var wbi = WbiSign.EncodeWbi(parameters); + Assert.NotNull(wbi["w_rid"]); + } + + [Fact] + public void TestEncodeWbi_Default2() + { + var parameters = new Dictionary + { + { "fourk", 1 }, + { "fnver", 0 }, + { "fnval", 4048 }, + { "cid", 405595939 }, + { "qn", 120 }, + { "bvid", "BV1Sg411F7cb" }, + { "aid", 505421421 } + }; + + var wbi = WbiSign.EncodeWbi(parameters, "653657f524a547ac981ded72ea172057", "6e4909c702f846728e64f6007736a338"); + Assert.NotNull(wbi["w_rid"]); + } + + } +} \ No newline at end of file diff --git a/src/BiliSharp.UnitTest/Api/User/TestNickname.cs b/src/BiliSharp.UnitTest/Api/User/TestNickname.cs new file mode 100644 index 0000000..9712b06 --- /dev/null +++ b/src/BiliSharp.UnitTest/Api/User/TestNickname.cs @@ -0,0 +1,25 @@ +using BiliSharp.Api.User; + +namespace BiliSharp.UnitTest.Api.User +{ + public class TestNickname + { + [Fact] + public void TestCheckNickname_Default() + { + Equal("downkyi", 0); + Equal("maozedong", 40002); + Equal("//", 40004); + Equal("test0000000000000", 40005); + Equal("0", 40006); + Equal("test", 40014); + } + + private static void Equal(string nickname, int code) + { + var response = Nickname.CheckNickname(nickname); + Assert.Equal(code, response.Code); + } + + } +} \ No newline at end of file diff --git a/src/BiliSharp.UnitTest/Api/User/TestUserInfo.cs b/src/BiliSharp.UnitTest/Api/User/TestUserInfo.cs new file mode 100644 index 0000000..5b65f53 --- /dev/null +++ b/src/BiliSharp.UnitTest/Api/User/TestUserInfo.cs @@ -0,0 +1,70 @@ +using BiliSharp.Api.Login; +using BiliSharp.Api.Sign; +using BiliSharp.Api.User; + +namespace BiliSharp.UnitTest.Api.User +{ + public class TestUserInfo + { + [Fact] + public void TestGetUserInfo_Default() + { + Cookies.GetMyCookies(); + + // 设置wbi keys + var info = LoginInfo.GetNavigationInfo(); + var imgKey = info.Data.WbiImg.ImgUrl.Split('/').ToList().Last().Split('.')[0]; + var subKey = info.Data.WbiImg.SubUrl.Split('/').ToList().Last().Split('.')[0]; + var keys = new Tuple(imgKey, subKey); + WbiSign.SetKey(keys); + + long mid = 42018135; + var userInfo = UserInfo.GetUserInfo(mid); + + Assert.Equal(mid, userInfo.Data.Mid); + } + + [Fact] + public void TestGetUserCard_Default() + { + Cookies.GetMyCookies(); + + long mid = 42018135; + var userCard = UserInfo.GetUserCard(mid); + + Assert.Equal(mid.ToString(), userCard.Data.Card.Mid); + } + + [Fact] + public void TestGetMyInfo_Default() + { + Cookies.GetMyCookies(); + + long mid = 42018135; + var myInfo = UserInfo.GetMyInfo(); + + Assert.Equal(mid, myInfo.Data.Mid); + } + + [Fact] + public void TestGetUserCards_Default() + { + Cookies.GetMyCookies(); + + // https://api.vc.bilibili.com/account/v1/user/cards?uids=314521322,206840230,49246269 + List ids = new() + { + 314521322, + 206840230, + 49246269 + }; + var users = UserInfo.GetUserCards(ids); + + foreach (var user in users.Data) + { + Assert.Contains(user.Mid, ids); + } + } + + } +} \ No newline at end of file diff --git a/src/BiliSharp.UnitTest/BiliSharp.UnitTest.csproj b/src/BiliSharp.UnitTest/BiliSharp.UnitTest.csproj new file mode 100644 index 0000000..51bfa56 --- /dev/null +++ b/src/BiliSharp.UnitTest/BiliSharp.UnitTest.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/BiliSharp.UnitTest/Cookies.cs b/src/BiliSharp.UnitTest/Cookies.cs new file mode 100644 index 0000000..f437dda --- /dev/null +++ b/src/BiliSharp.UnitTest/Cookies.cs @@ -0,0 +1,43 @@ +using System.Net; + +namespace BiliSharp.UnitTest; + +public static class Cookies +{ + /// + /// 解析从浏览器获取的cookies,用于设置cookie + /// + /// + /// + public static CookieContainer ParseCookieByString(string str) + { + var cookieContainer = new CookieContainer(); + + var cookies = str.Replace(" ", "").Split(";"); + foreach (var cookie in cookies) + { + DateTime dateTime = DateTime.Now; + dateTime = dateTime.AddMonths(12); + + var temp = cookie.Split("="); + var name = temp[0]; + var value = temp[1]; + + // 添加cookie + cookieContainer.Add(new Cookie(name, value, "/", ".bilibili.com") { Expires = dateTime }); + } + return cookieContainer; + } + + public static void GetMyCookies() + { + string cookiesStr = "DedeUserID=42018135; " + + "DedeUserID__ckMd5=44e22fa30fe34ac4; " + + "SESSDATA=32c16297%2C1700815953%2Cb11cd%2A51; " + + "bili_jct=98dbd091dc07d8f9b69ba3845974e7c8; " + + "sid=6vomjg3u"; + var cookies = ParseCookieByString(cookiesStr); + BiliManager.Instance().SetCookies(cookies); + } + +} diff --git a/src/BiliSharp.UnitTest/Usings.cs b/src/BiliSharp.UnitTest/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/BiliSharp.UnitTest/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/BiliSharp/Api/Login/LoginInfo.cs b/src/BiliSharp/Api/Login/LoginInfo.cs new file mode 100644 index 0000000..6f204c2 --- /dev/null +++ b/src/BiliSharp/Api/Login/LoginInfo.cs @@ -0,0 +1,40 @@ +using BiliSharp.Api.Models.Login; + +namespace BiliSharp.Api.Login +{ + /// + /// 登录基本信息 + /// + public static class LoginInfo + { + /// + /// 导航栏用户信息 + /// + /// + public static NavigationLoginInfo GetNavigationInfo() + { + string url = "https://api.bilibili.com/x/web-interface/nav"; + return Utils.GetData(url); + } + + /// + /// 登录用户状态数(双端) + /// + /// + public static LoginInfoStat GetLoginInfoStat() + { + string url = "https://api.bilibili.com/x/web-interface/nav/stat"; + return Utils.GetData(url); + } + + /// + /// 获取硬币数 + /// + /// + public static MyCoin GetMyCoin() + { + string url = "https://account.bilibili.com/site/getCoin"; + return Utils.GetData(url); + } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Login/LoginNotice.cs b/src/BiliSharp/Api/Login/LoginNotice.cs new file mode 100644 index 0000000..c896248 --- /dev/null +++ b/src/BiliSharp/Api/Login/LoginNotice.cs @@ -0,0 +1,18 @@ +namespace BiliSharp.Api.Login +{ + /// + /// 登录记录 + /// + public static class LoginNotice + { + /// + /// 查询登录记录 + /// + /// + public static Models.Login.LoginNotice GetLoginNotice(long mid) + { + string url = $"https://api.bilibili.com/x/safecenter/login_notice?mid={mid}"; + return Utils.GetData(url); + } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Login/LoginQR.cs b/src/BiliSharp/Api/Login/LoginQR.cs new file mode 100644 index 0000000..e5fc29b --- /dev/null +++ b/src/BiliSharp/Api/Login/LoginQR.cs @@ -0,0 +1,30 @@ +using BiliSharp.Api.Models.Login; + +namespace BiliSharp.Api.Login +{ + /// + /// 二维码登录 + /// + public static class LoginQR + { + /// + /// 申请二维码(web端) + /// + /// + public static LoginQRCode GenerateQRCode() + { + string url = "https://passport.bilibili.com/x/passport-login/web/qrcode/generate"; + return Utils.GetData(url); + } + + /// + /// 扫码登录(web端) + /// + /// + public static LoginQRCodePoll PollQRCode(string qrcodeKey) + { + string url = $"https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qrcodeKey}"; + return Utils.GetData(url); + } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/Login/LoginInfoStat.cs b/src/BiliSharp/Api/Models/Login/LoginInfoStat.cs new file mode 100644 index 0000000..eaa79cc --- /dev/null +++ b/src/BiliSharp/Api/Models/Login/LoginInfoStat.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.Login +{ + /// + /// https://api.bilibili.com/x/web-interface/nav/stat + /// + public class LoginInfoStat + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("ttl")] + public int Ttl { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public LoginInfoStatData Data { get; set; } + } + + /// + /// + /// + public class LoginInfoStatData + { + /// + /// + /// + [JsonPropertyName("following")] + public int Following { get; set; } + + /// + /// + /// + [JsonPropertyName("follower")] + public int Follower { get; set; } + + /// + /// + /// + [JsonPropertyName("dynamic_count")] + public int DynamicCount { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/Login/LoginNotice.cs b/src/BiliSharp/Api/Models/Login/LoginNotice.cs new file mode 100644 index 0000000..453c1bf --- /dev/null +++ b/src/BiliSharp/Api/Models/Login/LoginNotice.cs @@ -0,0 +1,76 @@ +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.Login +{ + /// + /// https://api.bilibili.com/x/safecenter/login_notice?mid= + /// + public class LoginNotice + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("ttl")] + public int Ttl { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public LoginNoticeData Data { get; set; } + } + + /// + /// + /// + public class LoginNoticeData + { + /// + /// + /// + [JsonPropertyName("mid")] + public long Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("device_name")] + public string DeviceName { get; set; } + + /// + /// + /// + [JsonPropertyName("login_type")] + public string LoginType { get; set; } + + /// + /// + /// + [JsonPropertyName("login_time")] + public string LoginTime { get; set; } + + /// + /// + /// + [JsonPropertyName("location")] + public string Location { get; set; } + + /// + /// + /// + [JsonPropertyName("ip")] + public string Ip { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/Login/LoginQRCode.cs b/src/BiliSharp/Api/Models/Login/LoginQRCode.cs new file mode 100644 index 0000000..40921fb --- /dev/null +++ b/src/BiliSharp/Api/Models/Login/LoginQRCode.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.Login +{ + /// + /// https://passport.bilibili.com/x/passport-login/web/qrcode/generate + /// + public class LoginQRCode + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("ttl")] + public int Ttl { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public LoginQRCodeData Data { get; set; } + } + + /// + /// + /// + public class LoginQRCodeData + { + /// + /// + /// + [JsonPropertyName("url")] + public string Url { get; set; } + + /// + /// + /// + [JsonPropertyName("qrcode_key")] + public string QrcodeKey { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/Login/LoginQRCodePoll.cs b/src/BiliSharp/Api/Models/Login/LoginQRCodePoll.cs new file mode 100644 index 0000000..e183d69 --- /dev/null +++ b/src/BiliSharp/Api/Models/Login/LoginQRCodePoll.cs @@ -0,0 +1,70 @@ +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.Login +{ + /// + /// https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key= + /// + public class LoginQRCodePoll + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("ttl")] + public int Ttl { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public LoginQRCodePollData Data { get; set; } + } + + /// + /// + /// + public class LoginQRCodePollData + { + /// + /// + /// + [JsonPropertyName("url")] + public string Url { get; set; } + + /// + /// + /// + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } + + /// + /// + /// + [JsonPropertyName("timestamp")] + public long Timestamp { get; set; } + + /// + /// + /// + [JsonPropertyName("code")] + public long Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/Login/MyCoin.cs b/src/BiliSharp/Api/Models/Login/MyCoin.cs new file mode 100644 index 0000000..b7a9d7a --- /dev/null +++ b/src/BiliSharp/Api/Models/Login/MyCoin.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.Login +{ + /// + /// https://account.bilibili.com/site/getCoin + /// + public class MyCoin + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("status")] + public bool Status { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public MyCoinData Data { get; set; } + } + + /// + /// + /// + public class MyCoinData + { + /// + /// + /// + [JsonPropertyName("money")] + public int Money { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/Login/NavigationLoginInfo.cs b/src/BiliSharp/Api/Models/Login/NavigationLoginInfo.cs new file mode 100644 index 0000000..6475320 --- /dev/null +++ b/src/BiliSharp/Api/Models/Login/NavigationLoginInfo.cs @@ -0,0 +1,556 @@ +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.Login +{ + /// + /// https://api.bilibili.com/x/web-interface/nav + /// + public class NavigationLoginInfo + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("ttl")] + public int Ttl { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public NavigationLoginInfoData Data { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoData + { + /// + /// + /// + [JsonPropertyName("isLogin")] + public bool Islogin { get; set; } + + /// + /// + /// + [JsonPropertyName("email_verified")] + public int EmailVerified { get; set; } + + /// + /// + /// + [JsonPropertyName("face")] + public string Face { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft")] + public int FaceNft { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft_type")] + public int FaceNftType { get; set; } + + /// + /// + /// + [JsonPropertyName("level_info")] + public NavigationLoginInfoDataLevelInfo LevelInfo { get; set; } + + /// + /// + /// + [JsonPropertyName("mid")] + public long Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("mobile_verified")] + public int MobileVerified { get; set; } + + /// + /// + /// + [JsonPropertyName("money")] + public int Money { get; set; } + + /// + /// + /// + [JsonPropertyName("moral")] + public int Moral { get; set; } + + /// + /// + /// + [JsonPropertyName("official")] + public NavigationLoginInfoDataOfficial Official { get; set; } + + /// + /// + /// + [JsonPropertyName("officialVerify")] + public NavigationLoginInfoDataOfficialverify Officialverify { get; set; } + + /// + /// + /// + [JsonPropertyName("pendant")] + public NavigationLoginInfoDataPendant Pendant { get; set; } + + /// + /// + /// + [JsonPropertyName("scores")] + public int Scores { get; set; } + + /// + /// + /// + [JsonPropertyName("uname")] + public string Uname { get; set; } + + /// + /// + /// + [JsonPropertyName("vipDueDate")] + public long Vipduedate { get; set; } + + /// + /// + /// + [JsonPropertyName("vipStatus")] + public int Vipstatus { get; set; } + + /// + /// + /// + [JsonPropertyName("vipType")] + public int Viptype { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_pay_type")] + public int VipPayType { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_theme_type")] + public int VipThemeType { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_label")] + public NavigationLoginInfoDataVipLabel VipLabel { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_avatar_subscript")] + public int VipAvatarSubscript { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_nickname_color")] + public string VipNicknameColor { get; set; } + + /// + /// + /// + [JsonPropertyName("vip")] + public NavigationLoginInfoDataVip Vip { get; set; } + + /// + /// + /// + [JsonPropertyName("wallet")] + public NavigationLoginInfoDataWallet Wallet { get; set; } + + /// + /// + /// + [JsonPropertyName("has_shop")] + public bool HasShop { get; set; } + + /// + /// + /// + [JsonPropertyName("shop_url")] + public string ShopUrl { get; set; } + + /// + /// + /// + [JsonPropertyName("allowance_count")] + public int AllowanceCount { get; set; } + + /// + /// + /// + [JsonPropertyName("answer_status")] + public int AnswerStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("is_senior_member")] + public int IsSeniorMember { get; set; } + + /// + /// + /// + [JsonPropertyName("wbi_img")] + public NavigationLoginInfoDataWbiImg WbiImg { get; set; } + + /// + /// + /// + [JsonPropertyName("is_jury")] + public bool IsJury { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoDataLevelInfo + { + /// + /// + /// + [JsonPropertyName("current_level")] + public int CurrentLevel { get; set; } + + /// + /// + /// + [JsonPropertyName("current_min")] + public int CurrentMin { get; set; } + + /// + /// + /// + [JsonPropertyName("current_exp")] + public int CurrentExp { get; set; } + + /// + /// + /// + [JsonPropertyName("next_exp")] + public object NextExp { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoDataOfficial + { + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("desc")] + public string Desc { get; set; } + + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoDataOfficialverify + { + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// + /// + [JsonPropertyName("desc")] + public string Desc { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoDataPendant + { + /// + /// + /// + [JsonPropertyName("pid")] + public int Pid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("expire")] + public int Expire { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance")] + public string ImageEnhance { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance_frame")] + public string ImageEnhanceFrame { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoDataVipLabel + { + /// + /// + /// + [JsonPropertyName("path")] + public string Path { get; set; } + + /// + /// + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// + /// + [JsonPropertyName("label_theme")] + public string LabelTheme { get; set; } + + /// + /// + /// + [JsonPropertyName("text_color")] + public string TextColor { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_style")] + public int BgStyle { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_color")] + public string BgColor { get; set; } + + /// + /// + /// + [JsonPropertyName("border_color")] + public string BorderColor { get; set; } + + /// + /// + /// + [JsonPropertyName("use_img_label")] + public bool UseImgLabel { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans")] + public string ImgLabelUriHans { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant")] + public string ImgLabelUriHant { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans_static")] + public string ImgLabelUriHansStatic { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant_static")] + public string ImgLabelUriHantStatic { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoDataVip + { + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// + /// + [JsonPropertyName("status")] + public int Status { get; set; } + + /// + /// + /// + [JsonPropertyName("due_date")] + public long DueDate { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_pay_type")] + public int VipPayType { get; set; } + + /// + /// + /// + [JsonPropertyName("theme_type")] + public int ThemeType { get; set; } + + /// + /// + /// + [JsonPropertyName("label")] + public NavigationLoginInfoDataVipLabel Label { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript")] + public int AvatarSubscript { get; set; } + + /// + /// + /// + [JsonPropertyName("nickname_color")] + public string NicknameColor { get; set; } + + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript_url")] + public string AvatarSubscriptUrl { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_status")] + public int TvVipStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_pay_type")] + public int TvVipPayType { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoDataWallet + { + /// + /// + /// + [JsonPropertyName("mid")] + public long Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("bcoin_balance")] + public int BcoinBalance { get; set; } + + /// + /// + /// + [JsonPropertyName("coupon_balance")] + public int CouponBalance { get; set; } + + /// + /// + /// + [JsonPropertyName("coupon_due_time")] + public int CouponDueTime { get; set; } + } + + /// + /// + /// + public class NavigationLoginInfoDataWbiImg + { + /// + /// + /// + [JsonPropertyName("img_url")] + public string ImgUrl { get; set; } + + /// + /// + /// + [JsonPropertyName("sub_url")] + public string SubUrl { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/User/MyInfo.cs b/src/BiliSharp/Api/Models/User/MyInfo.cs new file mode 100644 index 0000000..cfb71cd --- /dev/null +++ b/src/BiliSharp/Api/Models/User/MyInfo.cs @@ -0,0 +1,652 @@ +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.User +{ + /// + /// https://api.bilibili.com/x/space/myinfo + /// + public class MyInfo + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("ttl")] + public int Ttl { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public MyInfoData Data { get; set; } + } + + /// + /// + /// + public class MyInfoData + { + /// + /// + /// + [JsonPropertyName("mid")] + public long Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("sex")] + public string Sex { get; set; } + + /// + /// + /// + [JsonPropertyName("face")] + public string Face { get; set; } + + /// + /// + /// + [JsonPropertyName("sign")] + public string Sign { get; set; } + + /// + /// + /// + [JsonPropertyName("rank")] + public int Rank { get; set; } + + /// + /// + /// + [JsonPropertyName("level")] + public int Level { get; set; } + + /// + /// + /// + [JsonPropertyName("jointime")] + public int Jointime { get; set; } + + /// + /// + /// + [JsonPropertyName("moral")] + public int Moral { get; set; } + + /// + /// + /// + [JsonPropertyName("silence")] + public int Silence { get; set; } + + /// + /// + /// + [JsonPropertyName("email_status")] + public int EmailStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("tel_status")] + public int TelStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("identification")] + public int Identification { get; set; } + + /// + /// + /// + [JsonPropertyName("vip")] + public MyInfoDataVip Vip { get; set; } + + /// + /// + /// + [JsonPropertyName("pendant")] + public MyInfoDataPendant Pendant { get; set; } + + /// + /// + /// + [JsonPropertyName("nameplate")] + public MyInfoDataNameplate Nameplate { get; set; } + + /// + /// + /// + [JsonPropertyName("official")] + public MyInfoDataOfficial Official { get; set; } + + /// + /// + /// + [JsonPropertyName("birthday")] + public long Birthday { get; set; } + + /// + /// + /// + [JsonPropertyName("is_tourist")] + public int IsTourist { get; set; } + + /// + /// + /// + [JsonPropertyName("is_fake_account")] + public int IsFakeAccount { get; set; } + + /// + /// + /// + [JsonPropertyName("pin_prompting")] + public int PinPrompting { get; set; } + + /// + /// + /// + [JsonPropertyName("is_deleted")] + public int IsDeleted { get; set; } + + /// + /// + /// + [JsonPropertyName("in_reg_audit")] + public int InRegAudit { get; set; } + + /// + /// + /// + [JsonPropertyName("is_rip_user")] + public bool IsRipUser { get; set; } + + /// + /// + /// + [JsonPropertyName("profession")] + public MyInfoDataProfession Profession { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft")] + public int FaceNft { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft_new")] + public int FaceNftNew { get; set; } + + /// + /// + /// + [JsonPropertyName("is_senior_member")] + public int IsSeniorMember { get; set; } + + /// + /// + /// + [JsonPropertyName("honours")] + public MyInfoDataHonours Honours { get; set; } + + /// + /// + /// + [JsonPropertyName("digital_id")] + public string DigitalId { get; set; } + + /// + /// + /// + [JsonPropertyName("digital_type")] + public int DigitalType { get; set; } + + /// + /// + /// + [JsonPropertyName("level_exp")] + public MyInfoDataLevelExp LevelExp { get; set; } + + /// + /// + /// + [JsonPropertyName("coins")] + public int Coins { get; set; } + + /// + /// + /// + [JsonPropertyName("following")] + public int Following { get; set; } + + /// + /// + /// + [JsonPropertyName("follower")] + public int Follower { get; set; } + } + + /// + /// + /// + public class MyInfoDataVip + { + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// + /// + [JsonPropertyName("status")] + public int Status { get; set; } + + /// + /// + /// + [JsonPropertyName("due_date")] + public long DueDate { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_pay_type")] + public int VipPayType { get; set; } + + /// + /// + /// + [JsonPropertyName("theme_type")] + public int ThemeType { get; set; } + + /// + /// + /// + [JsonPropertyName("label")] + public MyInfoDataVipLabel Label { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript")] + public int AvatarSubscript { get; set; } + + /// + /// + /// + [JsonPropertyName("nickname_color")] + public string NicknameColor { get; set; } + + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript_url")] + public string AvatarSubscriptUrl { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_status")] + public int TvVipStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_pay_type")] + public int TvVipPayType { get; set; } + } + + /// + /// + /// + public class MyInfoDataVipLabel + { + /// + /// + /// + [JsonPropertyName("path")] + public string Path { get; set; } + + /// + /// + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// + /// + [JsonPropertyName("label_theme")] + public string LabelTheme { get; set; } + + /// + /// + /// + [JsonPropertyName("text_color")] + public string TextColor { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_style")] + public int BgStyle { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_color")] + public string BgColor { get; set; } + + /// + /// + /// + [JsonPropertyName("border_color")] + public string BorderColor { get; set; } + + /// + /// + /// + [JsonPropertyName("use_img_label")] + public bool UseImgLabel { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans")] + public string ImgLabelUriHans { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant")] + public string ImgLabelUriHant { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans_static")] + public string ImgLabelUriHansStatic { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant_static")] + public string ImgLabelUriHantStatic { get; set; } + } + + /// + /// + /// + public class MyInfoDataPendant + { + /// + /// + /// + [JsonPropertyName("pid")] + public int Pid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("expire")] + public int Expire { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance")] + public string ImageEnhance { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance_frame")] + public string ImageEnhanceFrame { get; set; } + } + + /// + /// + /// + public class MyInfoDataNameplate + { + /// + /// + /// + [JsonPropertyName("nid")] + public int Nid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("image_small")] + public string ImageSmall { get; set; } + + /// + /// + /// + [JsonPropertyName("level")] + public string Level { get; set; } + + /// + /// + /// + [JsonPropertyName("condition")] + public string Condition { get; set; } + } + + /// + /// + /// + public class MyInfoDataOfficial + { + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("desc")] + public string Desc { get; set; } + + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + } + + /// + /// + /// + public class MyInfoDataProfession + { + /// + /// + /// + [JsonPropertyName("id")] + public int Id { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("show_name")] + public string ShowName { get; set; } + + /// + /// + /// + [JsonPropertyName("is_show")] + public int IsShow { get; set; } + + /// + /// + /// + [JsonPropertyName("category_one")] + public string CategoryOne { get; set; } + + /// + /// + /// + [JsonPropertyName("realname")] + public string Realname { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("department")] + public string Department { get; set; } + } + + /// + /// + /// + public class MyInfoDataHonours + { + /// + /// + /// + [JsonPropertyName("mid")] + public long Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("colour")] + public MyInfoDataHonoursColour Colour { get; set; } + + /// + /// + /// + [JsonPropertyName("tags")] + public object Tags { get; set; } + } + + /// + /// + /// + public class MyInfoDataHonoursColour + { + /// + /// + /// + [JsonPropertyName("dark")] + public string Dark { get; set; } + + /// + /// + /// + [JsonPropertyName("normal")] + public string Normal { get; set; } + } + + /// + /// + /// + public class MyInfoDataLevelExp + { + /// + /// + /// + [JsonPropertyName("current_level")] + public int CurrentLevel { get; set; } + + /// + /// + /// + [JsonPropertyName("current_min")] + public int CurrentMin { get; set; } + + /// + /// + /// + [JsonPropertyName("current_exp")] + public int CurrentExp { get; set; } + + /// + /// + /// + [JsonPropertyName("next_exp")] + public int NextExp { get; set; } + + /// + /// + /// + [JsonPropertyName("level_up")] + public long LevelUp { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/User/Nickname.cs b/src/BiliSharp/Api/Models/User/Nickname.cs new file mode 100644 index 0000000..8c08569 --- /dev/null +++ b/src/BiliSharp/Api/Models/User/Nickname.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.User +{ + /// + /// https://passport.bilibili.com/web/generic/check/nickname?nickName=hahaha + /// + public class Nickname + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/User/UserCard.cs b/src/BiliSharp/Api/Models/User/UserCard.cs new file mode 100644 index 0000000..090b0c3 --- /dev/null +++ b/src/BiliSharp/Api/Models/User/UserCard.cs @@ -0,0 +1,599 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.User +{ + /// + /// https://api.bilibili.com/x/web-interface/card?mid=314521322&photo=true + /// + public class UserCard + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("ttl")] + public int Ttl { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public UserCardData Data { get; set; } + } + + /// + /// + /// + public class UserCardData + { + /// + /// + /// + [JsonPropertyName("card")] + public UserCardDataCard Card { get; set; } + + /// + /// + /// + [JsonPropertyName("space")] + public UserCardDataSpace Space { get; set; } + + /// + /// + /// + [JsonPropertyName("following")] + public bool Following { get; set; } + + /// + /// + /// + [JsonPropertyName("archive_count")] + public int ArchiveCount { get; set; } + + /// + /// + /// + [JsonPropertyName("article_count")] + public int ArticleCount { get; set; } + + /// + /// + /// + [JsonPropertyName("follower")] + public long Follower { get; set; } + + /// + /// + /// + [JsonPropertyName("like_num")] + public long LikeNum { get; set; } + } + + /// + /// + /// + public class UserCardDataCard + { + /// + /// + /// + [JsonPropertyName("mid")] + public string Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("approve")] + public bool Approve { get; set; } + + /// + /// + /// + [JsonPropertyName("sex")] + public string Sex { get; set; } + + /// + /// + /// + [JsonPropertyName("rank")] + public string Rank { get; set; } + + /// + /// + /// + [JsonPropertyName("face")] + public string Face { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft")] + public int FaceNft { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft_type")] + public int FaceNftType { get; set; } + + /// + /// + /// + [JsonPropertyName("DisplayRank")] + public string Displayrank { get; set; } + + /// + /// + /// + [JsonPropertyName("regtime")] + public int Regtime { get; set; } + + /// + /// + /// + [JsonPropertyName("spacesta")] + public int Spacesta { get; set; } + + /// + /// + /// + [JsonPropertyName("birthday")] + public string Birthday { get; set; } + + /// + /// + /// + [JsonPropertyName("place")] + public string Place { get; set; } + + /// + /// + /// + [JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// + /// + [JsonPropertyName("article")] + public int Article { get; set; } + + /// + /// + /// + [JsonPropertyName("attentions")] + public List Attentions { get; set; } + + /// + /// + /// + [JsonPropertyName("fans")] + public long Fans { get; set; } + + /// + /// + /// + [JsonPropertyName("friend")] + public int Friend { get; set; } + + /// + /// + /// + [JsonPropertyName("attention")] + public int Attention { get; set; } + + /// + /// + /// + [JsonPropertyName("sign")] + public string Sign { get; set; } + + /// + /// + /// + [JsonPropertyName("level_info")] + public UserCardDataCardLevelInfo LevelInfo { get; set; } + + /// + /// + /// + [JsonPropertyName("pendant")] + public UserCardDataCardPendant Pendant { get; set; } + + /// + /// + /// + [JsonPropertyName("nameplate")] + public UserCardDataCardNameplate Nameplate { get; set; } + + /// + /// + /// + [JsonPropertyName("Official")] + public UserCardDataCardOfficial Official { get; set; } + + /// + /// + /// + [JsonPropertyName("official_verify")] + public UserCardDataCardOfficialVerify OfficialVerify { get; set; } + + /// + /// + /// + [JsonPropertyName("vip")] + public UserCardDataCardVip Vip { get; set; } + + /// + /// + /// + [JsonPropertyName("is_senior_member")] + public int IsSeniorMember { get; set; } + } + + /// + /// + /// + public class UserCardDataCardLevelInfo + { + /// + /// + /// + [JsonPropertyName("current_level")] + public int CurrentLevel { get; set; } + + /// + /// + /// + [JsonPropertyName("current_min")] + public int CurrentMin { get; set; } + + /// + /// + /// + [JsonPropertyName("current_exp")] + public int CurrentExp { get; set; } + + /// + /// + /// + [JsonPropertyName("next_exp")] + public int NextExp { get; set; } + } + + /// + /// + /// + public class UserCardDataCardPendant + { + /// + /// + /// + [JsonPropertyName("pid")] + public int Pid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("expire")] + public int Expire { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance")] + public string ImageEnhance { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance_frame")] + public string ImageEnhanceFrame { get; set; } + } + + /// + /// + /// + public class UserCardDataCardNameplate + { + /// + /// + /// + [JsonPropertyName("nid")] + public int Nid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("image_small")] + public string ImageSmall { get; set; } + + /// + /// + /// + [JsonPropertyName("level")] + public string Level { get; set; } + + /// + /// + /// + [JsonPropertyName("condition")] + public string Condition { get; set; } + } + + /// + /// + /// + public class UserCardDataCardOfficial + { + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("desc")] + public string Desc { get; set; } + + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + } + + /// + /// + /// + public class UserCardDataCardOfficialVerify + { + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// + /// + [JsonPropertyName("desc")] + public string Desc { get; set; } + } + + /// + /// + /// + public class UserCardDataCardVip + { + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// + /// + [JsonPropertyName("status")] + public int Status { get; set; } + + /// + /// + /// + [JsonPropertyName("due_date")] + public long DueDate { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_pay_type")] + public int VipPayType { get; set; } + + /// + /// + /// + [JsonPropertyName("theme_type")] + public int ThemeType { get; set; } + + /// + /// + /// + [JsonPropertyName("label")] + public UserCardDataCardVipLabel Label { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript")] + public int AvatarSubscript { get; set; } + + /// + /// + /// + [JsonPropertyName("nickname_color")] + public string NicknameColor { get; set; } + + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript_url")] + public string AvatarSubscriptUrl { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_status")] + public int TvVipStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_pay_type")] + public int TvVipPayType { get; set; } + + /// + /// + /// + [JsonPropertyName("vipType")] + public int Viptype { get; set; } + + /// + /// + /// + [JsonPropertyName("vipStatus")] + public int Vipstatus { get; set; } + } + + /// + /// + /// + public class UserCardDataCardVipLabel + { + /// + /// + /// + [JsonPropertyName("path")] + public string Path { get; set; } + + /// + /// + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// + /// + [JsonPropertyName("label_theme")] + public string LabelTheme { get; set; } + + /// + /// + /// + [JsonPropertyName("text_color")] + public string TextColor { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_style")] + public int BgStyle { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_color")] + public string BgColor { get; set; } + + /// + /// + /// + [JsonPropertyName("border_color")] + public string BorderColor { get; set; } + + /// + /// + /// + [JsonPropertyName("use_img_label")] + public bool UseImgLabel { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans")] + public string ImgLabelUriHans { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant")] + public string ImgLabelUriHant { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans_static")] + public string ImgLabelUriHansStatic { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant_static")] + public string ImgLabelUriHantStatic { get; set; } + } + + /// + /// + /// + public class UserCardDataSpace + { + /// + /// + /// + [JsonPropertyName("s_img")] + public string SImg { get; set; } + + /// + /// + /// + [JsonPropertyName("l_img")] + public string LImg { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/User/UserCards.cs b/src/BiliSharp/Api/Models/User/UserCards.cs new file mode 100644 index 0000000..2ce52fe --- /dev/null +++ b/src/BiliSharp/Api/Models/User/UserCards.cs @@ -0,0 +1,443 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.User +{ + /// + /// https://api.vc.bilibili.com/account/v1/user/cards?uids=314521322,206840230,49246269 + /// + public class UserCards + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("msg")] + public string Msg { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public List Data { get; set; } + } + + /// + /// + /// + public class UserCardsData + { + /// + /// + /// + [JsonPropertyName("mid")] + public long Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("sex")] + public string Sex { get; set; } + + /// + /// + /// + [JsonPropertyName("face")] + public string Face { get; set; } + + /// + /// + /// + [JsonPropertyName("sign")] + public string Sign { get; set; } + + /// + /// + /// + [JsonPropertyName("rank")] + public int Rank { get; set; } + + /// + /// + /// + [JsonPropertyName("level")] + public int Level { get; set; } + + /// + /// + /// + [JsonPropertyName("silence")] + public int Silence { get; set; } + + /// + /// + /// + [JsonPropertyName("vip")] + public UserCardsDataVip Vip { get; set; } + + /// + /// + /// + [JsonPropertyName("pendant")] + public UserCardsDataPendant Pendant { get; set; } + + /// + /// + /// + [JsonPropertyName("nameplate")] + public UserCardsDataNameplate Nameplate { get; set; } + + /// + /// + /// + [JsonPropertyName("official")] + public UserCardsDataOfficial Official { get; set; } + + /// + /// + /// + [JsonPropertyName("birthday")] + public long Birthday { get; set; } + + /// + /// + /// + [JsonPropertyName("is_fake_account")] + public int IsFakeAccount { get; set; } + + /// + /// + /// + [JsonPropertyName("is_deleted")] + public int IsDeleted { get; set; } + + /// + /// + /// + [JsonPropertyName("in_reg_audit")] + public int InRegAudit { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft")] + public int FaceNft { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft_new")] + public int FaceNftNew { get; set; } + + /// + /// + /// + [JsonPropertyName("is_senior_member")] + public int IsSeniorMember { get; set; } + + /// + /// + /// + [JsonPropertyName("digital_id")] + public string DigitalId { get; set; } + + /// + /// + /// + [JsonPropertyName("digital_type")] + public int DigitalType { get; set; } + } + + /// + /// + /// + public class UserCardsDataVip + { + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// + /// + [JsonPropertyName("status")] + public int Status { get; set; } + + /// + /// + /// + [JsonPropertyName("due_date")] + public long DueDate { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_pay_type")] + public int VipPayType { get; set; } + + /// + /// + /// + [JsonPropertyName("theme_type")] + public int ThemeType { get; set; } + + /// + /// + /// + [JsonPropertyName("label")] + public UserCardsDataVipLabel Label { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript")] + public int AvatarSubscript { get; set; } + + /// + /// + /// + [JsonPropertyName("nickname_color")] + public string NicknameColor { get; set; } + + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript_url")] + public string AvatarSubscriptUrl { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_status")] + public int TvVipStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_pay_type")] + public int TvVipPayType { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_due_date")] + public long TvDueDate { get; set; } + } + + /// + /// + /// + public class UserCardsDataVipLabel + { + /// + /// + /// + [JsonPropertyName("path")] + public string Path { get; set; } + + /// + /// + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// + /// + [JsonPropertyName("label_theme")] + public string LabelTheme { get; set; } + + /// + /// + /// + [JsonPropertyName("text_color")] + public string TextColor { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_style")] + public int BgStyle { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_color")] + public string BgColor { get; set; } + + /// + /// + /// + [JsonPropertyName("border_color")] + public string BorderColor { get; set; } + + /// + /// + /// + [JsonPropertyName("use_img_label")] + public bool UseImgLabel { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans")] + public string ImgLabelUriHans { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant")] + public string ImgLabelUriHant { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans_static")] + public string ImgLabelUriHansStatic { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant_static")] + public string ImgLabelUriHantStatic { get; set; } + } + + /// + /// + /// + public class UserCardsDataPendant + { + /// + /// + /// + [JsonPropertyName("pid")] + public int Pid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("expire")] + public int Expire { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance")] + public string ImageEnhance { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance_frame")] + public string ImageEnhanceFrame { get; set; } + } + + /// + /// + /// + public class UserCardsDataNameplate + { + /// + /// + /// + [JsonPropertyName("nid")] + public int Nid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("image_small")] + public string ImageSmall { get; set; } + + /// + /// + /// + [JsonPropertyName("level")] + public string Level { get; set; } + + /// + /// + /// + [JsonPropertyName("condition")] + public string Condition { get; set; } + } + + /// + /// + /// + public class UserCardsDataOfficial + { + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("desc")] + public string Desc { get; set; } + + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Models/User/UserSpaceInfo.cs b/src/BiliSharp/Api/Models/User/UserSpaceInfo.cs new file mode 100644 index 0000000..9cdd58b --- /dev/null +++ b/src/BiliSharp/Api/Models/User/UserSpaceInfo.cs @@ -0,0 +1,827 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace BiliSharp.Api.Models.User +{ + /// + /// https://api.bilibili.com/x/space/acc/info?mid= + /// + public class UserSpaceInfo + { + /// + /// + /// + [JsonPropertyName("code")] + public int Code { get; set; } + + /// + /// + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// + /// + [JsonPropertyName("ttl")] + public int Ttl { get; set; } + + /// + /// + /// + [JsonPropertyName("data")] + public UserSpaceInfoData Data { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoData + { + /// + /// + /// + [JsonPropertyName("mid")] + public long Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("sex")] + public string Sex { get; set; } + + /// + /// + /// + [JsonPropertyName("face")] + public string Face { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft")] + public int FaceNft { get; set; } + + /// + /// + /// + [JsonPropertyName("face_nft_type")] + public int FaceNftType { get; set; } + + /// + /// + /// + [JsonPropertyName("sign")] + public string Sign { get; set; } + + /// + /// + /// + [JsonPropertyName("rank")] + public int Rank { get; set; } + + /// + /// + /// + [JsonPropertyName("level")] + public int Level { get; set; } + + /// + /// + /// + [JsonPropertyName("jointime")] + public int Jointime { get; set; } + + /// + /// + /// + [JsonPropertyName("moral")] + public int Moral { get; set; } + + /// + /// + /// + [JsonPropertyName("silence")] + public int Silence { get; set; } + + /// + /// + /// + [JsonPropertyName("coins")] + public int Coins { get; set; } + + /// + /// + /// + [JsonPropertyName("fans_badge")] + public bool FansBadge { get; set; } + + /// + /// + /// + [JsonPropertyName("fans_medal")] + public UserSpaceInfoDataFansMedal FansMedal { get; set; } + + /// + /// + /// + [JsonPropertyName("official")] + public UserSpaceInfoDataOfficial Official { get; set; } + + /// + /// + /// + [JsonPropertyName("vip")] + public UserSpaceInfoDataVip Vip { get; set; } + + /// + /// + /// + [JsonPropertyName("pendant")] + public UserSpaceInfoDataPendant Pendant { get; set; } + + /// + /// + /// + [JsonPropertyName("nameplate")] + public UserSpaceInfoDataNameplate Nameplate { get; set; } + + /// + /// + /// + [JsonPropertyName("user_honour_info")] + public UserSpaceInfoDataUserHonourInfo UserHonourInfo { get; set; } + + /// + /// + /// + [JsonPropertyName("is_followed")] + public bool IsFollowed { get; set; } + + /// + /// + /// + [JsonPropertyName("top_photo")] + public string TopPhoto { get; set; } + + /// + /// + /// + [JsonPropertyName("theme")] + public UserSpaceInfoDataTheme Theme { get; set; } + + /// + /// + /// + [JsonPropertyName("sys_notice")] + public UserSpaceInfoDataSysNotice SysNotice { get; set; } + + /// + /// + /// + [JsonPropertyName("live_room")] + public UserSpaceInfoDataLiveRoom LiveRoom { get; set; } + + /// + /// + /// + [JsonPropertyName("birthday")] + public string Birthday { get; set; } + + /// + /// + /// + [JsonPropertyName("school")] + public UserSpaceInfoDataSchool School { get; set; } + + /// + /// + /// + [JsonPropertyName("profession")] + public UserSpaceInfoDataProfession Profession { get; set; } + + /// + /// + /// + [JsonPropertyName("tags")] + public object Tags { get; set; } + + /// + /// + /// + [JsonPropertyName("series")] + public UserSpaceInfoDataSeries Series { get; set; } + + /// + /// + /// + [JsonPropertyName("is_senior_member")] + public int IsSeniorMember { get; set; } + + /// + /// + /// + [JsonPropertyName("mcn_info")] + public object McnInfo { get; set; } + + /// + /// + /// + [JsonPropertyName("gaia_res_type")] + public int GaiaResType { get; set; } + + /// + /// + /// + [JsonPropertyName("gaia_data")] + public object GaiaData { get; set; } + + /// + /// + /// + [JsonPropertyName("is_risk")] + public bool IsRisk { get; set; } + + /// + /// + /// + [JsonPropertyName("elec")] + public UserSpaceInfoDataElec Elec { get; set; } + + /// + /// + /// + [JsonPropertyName("contract")] + public UserSpaceInfoDataContract Contract { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataFansMedal + { + /// + /// + /// + [JsonPropertyName("show")] + public bool Show { get; set; } + + /// + /// + /// + [JsonPropertyName("wear")] + public bool Wear { get; set; } + + /// + /// + /// + [JsonPropertyName("medal")] + public object Medal { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataOfficial + { + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("desc")] + public string Desc { get; set; } + + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataVip + { + /// + /// + /// + [JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// + /// + [JsonPropertyName("status")] + public int Status { get; set; } + + /// + /// + /// + [JsonPropertyName("due_date")] + public long DueDate { get; set; } + + /// + /// + /// + [JsonPropertyName("vip_pay_type")] + public int VipPayType { get; set; } + + /// + /// + /// + [JsonPropertyName("theme_type")] + public int ThemeType { get; set; } + + /// + /// + /// + [JsonPropertyName("label")] + public UserSpaceInfoDataVipLabel Label { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript")] + public int AvatarSubscript { get; set; } + + /// + /// + /// + [JsonPropertyName("nickname_color")] + public string NicknameColor { get; set; } + + /// + /// + /// + [JsonPropertyName("role")] + public int Role { get; set; } + + /// + /// + /// + [JsonPropertyName("avatar_subscript_url")] + public string AvatarSubscriptUrl { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_status")] + public int TvVipStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("tv_vip_pay_type")] + public int TvVipPayType { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataVipLabel + { + /// + /// + /// + [JsonPropertyName("path")] + public string Path { get; set; } + + /// + /// + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// + /// + [JsonPropertyName("label_theme")] + public string LabelTheme { get; set; } + + /// + /// + /// + [JsonPropertyName("text_color")] + public string TextColor { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_style")] + public int BgStyle { get; set; } + + /// + /// + /// + [JsonPropertyName("bg_color")] + public string BgColor { get; set; } + + /// + /// + /// + [JsonPropertyName("border_color")] + public string BorderColor { get; set; } + + /// + /// + /// + [JsonPropertyName("use_img_label")] + public bool UseImgLabel { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans")] + public string ImgLabelUriHans { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant")] + public string ImgLabelUriHant { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hans_static")] + public string ImgLabelUriHansStatic { get; set; } + + /// + /// + /// + [JsonPropertyName("img_label_uri_hant_static")] + public string ImgLabelUriHantStatic { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataPendant + { + /// + /// + /// + [JsonPropertyName("pid")] + public int Pid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("expire")] + public int Expire { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance")] + public string ImageEnhance { get; set; } + + /// + /// + /// + [JsonPropertyName("image_enhance_frame")] + public string ImageEnhanceFrame { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataNameplate + { + /// + /// + /// + [JsonPropertyName("nid")] + public int Nid { get; set; } + + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("image")] + public string Image { get; set; } + + /// + /// + /// + [JsonPropertyName("image_small")] + public string ImageSmall { get; set; } + + /// + /// + /// + [JsonPropertyName("level")] + public string Level { get; set; } + + /// + /// + /// + [JsonPropertyName("condition")] + public string Condition { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataUserHonourInfo + { + /// + /// + /// + [JsonPropertyName("mid")] + public int Mid { get; set; } + + /// + /// + /// + [JsonPropertyName("colour")] + public object Colour { get; set; } + + /// + /// + /// + [JsonPropertyName("tags")] + public List Tags { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataTheme + { } + + /// + /// + /// + public class UserSpaceInfoDataSysNotice + { } + + /// + /// + /// + public class UserSpaceInfoDataLiveRoom + { + /// + /// + /// + [JsonPropertyName("roomStatus")] + public int Roomstatus { get; set; } + + /// + /// + /// + [JsonPropertyName("liveStatus")] + public int Livestatus { get; set; } + + /// + /// + /// + [JsonPropertyName("url")] + public string Url { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("cover")] + public string Cover { get; set; } + + /// + /// + /// + [JsonPropertyName("roomid")] + public long Roomid { get; set; } + + /// + /// + /// + [JsonPropertyName("roundStatus")] + public int Roundstatus { get; set; } + + /// + /// + /// + [JsonPropertyName("broadcast_type")] + public int BroadcastType { get; set; } + + /// + /// + /// + [JsonPropertyName("watched_show")] + public UserSpaceInfoDataLiveRoomWatchedShow WatchedShow { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataLiveRoomWatchedShow + { + /// + /// + /// + [JsonPropertyName("switch")] + public bool Switch { get; set; } + + /// + /// + /// + [JsonPropertyName("num")] + public int Num { get; set; } + + /// + /// + /// + [JsonPropertyName("text_small")] + public string TextSmall { get; set; } + + /// + /// + /// + [JsonPropertyName("text_large")] + public string TextLarge { get; set; } + + /// + /// + /// + [JsonPropertyName("icon")] + public string Icon { get; set; } + + /// + /// + /// + [JsonPropertyName("icon_location")] + public string IconLocation { get; set; } + + /// + /// + /// + [JsonPropertyName("icon_web")] + public string IconWeb { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataSchool + { + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataProfession + { + /// + /// + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// + /// + [JsonPropertyName("department")] + public string Department { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("is_show")] + public int IsShow { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataSeries + { + /// + /// + /// + [JsonPropertyName("user_upgrade_status")] + public int UserUpgradeStatus { get; set; } + + /// + /// + /// + [JsonPropertyName("show_upgrade_window")] + public bool ShowUpgradeWindow { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataElec + { + /// + /// + /// + [JsonPropertyName("show_info")] + public UserSpaceInfoDataElecShowInfo ShowInfo { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataElecShowInfo + { + /// + /// + /// + [JsonPropertyName("show")] + public bool Show { get; set; } + + /// + /// + /// + [JsonPropertyName("state")] + public int State { get; set; } + + /// + /// + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// + /// + [JsonPropertyName("icon")] + public string Icon { get; set; } + + /// + /// + /// + [JsonPropertyName("jump_url")] + public string JumpUrl { get; set; } + } + + /// + /// + /// + public class UserSpaceInfoDataContract + { + /// + /// + /// + [JsonPropertyName("is_display")] + public bool IsDisplay { get; set; } + + /// + /// + /// + [JsonPropertyName("is_follow_display")] + public bool IsFollowDisplay { get; set; } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/Sign/WbiSign.cs b/src/BiliSharp/Api/Sign/WbiSign.cs new file mode 100644 index 0000000..43e4937 --- /dev/null +++ b/src/BiliSharp/Api/Sign/WbiSign.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; + +namespace BiliSharp.Api.Sign +{ + public static class WbiSign + { + private static Tuple Keys; + + /// + /// 打乱重排实时口令 + /// + /// + /// + private static string GetMixinKey(string origin) + { + int[] mixinKeyEncTab = new int[] + { + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39,12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, + 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, + 36, 20, 34, 44, 52 + }; + + var temp = new StringBuilder(); + foreach (var i in mixinKeyEncTab) + { + temp.Append(origin[i]); + } + return temp.ToString()[..32]; + } + + /// + /// 将字典参数转为字符串 + /// + /// + /// + public static string ParametersToQuery(Dictionary parameters) + { + var keys = parameters.Keys.ToList(); + var queryList = new List(); + foreach (var item in keys) + { + var value = parameters[item]; + queryList.Add($"{item}={value}"); + } + return string.Join("&", queryList); + } + + /// + /// 返回imgKey,subKey + /// + /// + public static Tuple GetKey() + { + // 当Keys为null时,返回一个硬编码的key, + // 这个key无效,只为了保证不报错 + return Keys ?? new Tuple("653657f524a547ac981ded72ea172057", "6e4909c702f846728e64f6007736a338"); + } + + /// + /// 设置imgKey,subKey + /// + /// + public static void SetKey(Tuple keys) + { + Keys = keys; + } + + /// + /// Wbi签名,返回所有参数字典 + /// + /// + /// + public static Dictionary EncodeWbi(Dictionary parameters) + { + return EncodeWbi(parameters, GetKey().Item1, GetKey().Item2); + } + + /// + /// Wbi签名,返回所有参数字典 + /// + /// + /// + /// + /// + public static Dictionary EncodeWbi(Dictionary parameters, string imgKey, string subKey) + { + var mixinKey = GetMixinKey(imgKey + subKey); + + var chrFilter = new Regex("[!'()*]"); + + var newParameters = new Dictionary + { + { "wts", (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds } + }; + + foreach (var para in parameters) + { + var key = para.Key; + var value = para.Value.ToString(); + + var encodedValue = chrFilter.Replace(value, ""); + + newParameters.Add(Uri.EscapeDataString(key), Uri.EscapeDataString(encodedValue)); + } + + var keys = newParameters.Keys.ToList(); + keys.Sort(); + + var queryList = new List(); + foreach (var item in keys) + { + var value = newParameters[item]; + queryList.Add($"{item}={value}"); + } + + var queryString = string.Join("&", queryList); + var md5Hasher = MD5.Create(); + var hashStr = queryString + mixinKey; + var hashedQueryString = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(hashStr)); + var wbiSign = BitConverter.ToString(hashedQueryString).Replace("-", "").ToLower(); + + newParameters.Add("w_rid", wbiSign); + return newParameters; + } + + } +} diff --git a/src/BiliSharp/Api/User/Nickname.cs b/src/BiliSharp/Api/User/Nickname.cs new file mode 100644 index 0000000..ddcab9e --- /dev/null +++ b/src/BiliSharp/Api/User/Nickname.cs @@ -0,0 +1,19 @@ +namespace BiliSharp.Api.User +{ + /// + /// 检查昵称是否可注册 + /// + public static class Nickname + { + /// + /// 检查昵称 + /// + /// + /// + public static Models.User.Nickname CheckNickname(string nickname) + { + string url = $"https://passport.bilibili.com/web/generic/check/nickname?nickName={nickname}"; + return Utils.GetData(url); + } + } +} \ No newline at end of file diff --git a/src/BiliSharp/Api/User/UserInfo.cs b/src/BiliSharp/Api/User/UserInfo.cs new file mode 100644 index 0000000..b89f3e2 --- /dev/null +++ b/src/BiliSharp/Api/User/UserInfo.cs @@ -0,0 +1,66 @@ +using BiliSharp.Api.Models.User; +using BiliSharp.Api.Sign; +using System.Collections.Generic; + +namespace BiliSharp.Api.User +{ + /// + /// 用户基本信息 + /// + public static class UserInfo + { + /// + /// 用户空间详细信息 + /// + /// + public static UserSpaceInfo GetUserInfo(long mid) + { + var parameters = new Dictionary + { + { "mid", mid } + }; + string query = WbiSign.ParametersToQuery(WbiSign.EncodeWbi(parameters)); + string url = $"https://api.bilibili.com/x/space/wbi/acc/info?{query}"; + return Utils.GetData(url); + } + + /// + /// 用户名片信息 + /// + /// + /// + /// + public static UserCard GetUserCard(long mid, bool isPhoto = false) + { + string url = $"https://api.bilibili.com/x/web-interface/card?mid={mid}&photo={isPhoto}"; + return Utils.GetData(url); + } + + /// + /// 登录用户空间详细信息 + /// + /// + public static MyInfo GetMyInfo() + { + string url = "https://api.bilibili.com/x/space/myinfo"; + return Utils.GetData(url); + } + + /// + /// 多用户详细信息 + /// + /// + /// + public static UserCards GetUserCards(List ids) + { + string url = "https://api.vc.bilibili.com/account/v1/user/cards?uids="; + foreach (long id in ids) + { + url += $"{id},"; + } + url = url.TrimEnd(','); + + return Utils.GetData(url); + } + } +} \ No newline at end of file diff --git a/src/BiliSharp/BiliManager.cs b/src/BiliSharp/BiliManager.cs new file mode 100644 index 0000000..2aaabf2 --- /dev/null +++ b/src/BiliSharp/BiliManager.cs @@ -0,0 +1,53 @@ +using System.Net; + +namespace BiliSharp +{ + public class BiliManager + { + private static readonly BiliManager _instance = new BiliManager(); + private string _userAgent; + private CookieContainer _cookies; + + private BiliManager() { } + + public static BiliManager Instance() + { + return _instance; + } + + /// + /// 设置cookies + /// + /// + /// + public BiliManager SetCookies(CookieContainer cookies) + { + _cookies = cookies; + return this; + } + + /// + /// 获取cookies + /// + /// + public CookieContainer GetCookies() { return _cookies; } + + /// + /// 设置userAgent + /// + /// + /// + public BiliManager SetUserAgent(string userAgent) + { + _userAgent = userAgent; + return this; + } + + /// + /// 获取userAgent + /// + /// + public string GetUserAgent() { return _userAgent; } + + } +} \ No newline at end of file diff --git a/src/BiliSharp/BiliSharp.csproj b/src/BiliSharp/BiliSharp.csproj new file mode 100644 index 0000000..c675825 --- /dev/null +++ b/src/BiliSharp/BiliSharp.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.1 + + + + + + + diff --git a/src/BiliSharp/Utils.cs b/src/BiliSharp/Utils.cs new file mode 100644 index 0000000..7ff2142 --- /dev/null +++ b/src/BiliSharp/Utils.cs @@ -0,0 +1,29 @@ +using System; +using System.Net; +using System.Text.Json; + +namespace BiliSharp +{ + internal static class Utils + { + internal static T GetData(string url) + { + string referer = "https://www.bilibili.com"; + string userAgent = BiliManager.Instance().GetUserAgent(); + CookieContainer cookies = BiliManager.Instance().GetCookies(); + + string response = WebClient.RequestWeb(url, referer, userAgent, cookies); + + try + { + var obj = JsonSerializer.Deserialize(response); + return obj; + } + catch (Exception) + { + return default; + } + } + + } +} \ No newline at end of file diff --git a/src/BiliSharp/WebClient.cs b/src/BiliSharp/WebClient.cs new file mode 100644 index 0000000..9d8a0c5 --- /dev/null +++ b/src/BiliSharp/WebClient.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Text; + +namespace BiliSharp +{ + internal static class WebClient + { + /// + /// 发送get或post请求 + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal static string RequestWeb(string url, string referer = null, string userAgent = null, CookieContainer cookies = null, string method = "GET", Dictionary parameters = null, int retry = 3) + { + // 重试次数 + if (retry <= 0) { return ""; } + + // post请求,发送参数 + if (method == "POST" && parameters != null) + { + var builder = new StringBuilder(); + int i = 0; + foreach (var item in parameters) + { + if (i > 0) + { + builder.Append('&'); + } + + builder.AppendFormat("{0}={1}", item.Key, item.Value); + i++; + } + + url += "?" + builder.ToString(); + } + + try + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.Method = method; + request.Timeout = 60 * 1000; + + request.ContentType = "application/json,text/html,application/xhtml+xml,application/xml;charset=UTF-8"; + request.Headers["accept-language"] = "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"; + request.Headers["accept-encoding"] = "gzip, deflate, br"; + + // userAgent + if (userAgent != null) + { + request.UserAgent = userAgent; + } + else + { + request.UserAgent = "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"; + } + + // referer + if (referer != null) + { + request.Referer = referer; + } + + // 构造cookie + if (!url.Contains("getLogin")) + { + //request.Headers["origin"] = "https://www.bilibili.com"; + + if (cookies != null) + { + request.CookieContainer = cookies; + } + } + + string html = string.Empty; + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + if (response.ContentEncoding.ToLower().Contains("gzip")) + { + using var stream = new GZipStream(response.GetResponseStream(), CompressionMode.Decompress); + using var reader = new StreamReader(stream, Encoding.UTF8); + html = reader.ReadToEnd(); + } + else if (response.ContentEncoding.ToLower().Contains("deflate")) + { + using var stream = new DeflateStream(response.GetResponseStream(), CompressionMode.Decompress); + using var reader = new StreamReader(stream, Encoding.UTF8); + html = reader.ReadToEnd(); + } + else if (response.ContentEncoding.ToLower().Contains("br")) + { + using var stream = new BrotliStream(response.GetResponseStream(), CompressionMode.Decompress); + using var reader = new StreamReader(stream, Encoding.UTF8); + html = reader.ReadToEnd(); + } + else + { + using var stream = response.GetResponseStream(); + using var reader = new StreamReader(stream, Encoding.UTF8); + html = reader.ReadToEnd(); + } + } + + return html; + } + catch (Exception) + { + return RequestWeb(url, referer, userAgent, cookies, method, parameters, retry - 1); + } + } + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/BiliLocator.cs b/src/Downkyi.Core/Bili/BiliLocator.cs new file mode 100644 index 0000000..4464eae --- /dev/null +++ b/src/Downkyi.Core/Bili/BiliLocator.cs @@ -0,0 +1,24 @@ +namespace Downkyi.Core.Bili; + +public static class BiliLocator +{ + private static ILogin _login; + public static ILogin Login + { + get + { + _login ??= new Web.Login(); + return _login; + } + } + + private static IUser _user; + public static IUser User + { + get + { + _user ??= new Web.User(); + return _user; + } + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Cookies.cs b/src/Downkyi.Core/Bili/Cookies.cs new file mode 100644 index 0000000..42f46b2 --- /dev/null +++ b/src/Downkyi.Core/Bili/Cookies.cs @@ -0,0 +1,144 @@ +using Downkyi.Core.Utils; +using System.Collections; +using System.Net; +using System.Web; + +namespace Downkyi.Core.Bili; + +public static class Cookies +{ + /// + /// 写入cookies到磁盘 + /// + /// + /// + /// + public static bool WriteCookiesToDisk(string file, CookieContainer cookieJar) + { + return ObjectHelper.WriteObjectToDisk(file, cookieJar); + } + + /// + /// 从磁盘读取cookie + /// + /// + /// + public static CookieContainer ReadCookiesFromDisk(string file) + { + return (CookieContainer)ObjectHelper.ReadObjectFromDisk(file); + } + + /// + /// 将CookieContainer中的所有的Cookie读出来 + /// + /// + /// + public static List GetAllCookies(CookieContainer cc) + { + var lstCookies = new List(); + + Hashtable table = (Hashtable)cc.GetType().InvokeMember("m_domainTable", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.GetField | + System.Reflection.BindingFlags.Instance, null, cc, Array.Empty()); + + foreach (object pathList in table.Values) + { + SortedList lstCookieCol = (SortedList)pathList.GetType().InvokeMember("m_list", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.GetField + | System.Reflection.BindingFlags.Instance, null, pathList, Array.Empty()); + foreach (CookieCollection colCookies in lstCookieCol.Values) + { + foreach (Cookie c in colCookies.Cast()) + { + lstCookies.Add(c); + } + } + } + + return lstCookies; + } + + /// + /// 返回cookies的字符串 + /// + /// + public static string GetCookiesString(CookieContainer cc) + { + var cookies = GetAllCookies(cc); + + string cookie = string.Empty; + foreach (var item in cookies) + { + cookie += item.ToString() + ";"; + } + return cookie.TrimEnd(';'); + } + + /// + /// 解析二维码登录返回的url,用于设置cookie + /// + /// + /// + public static CookieContainer ParseCookieByUrl(string url) + { + var cookieContainer = new CookieContainer(); + + if (url == null || url == "") { return cookieContainer; } + + string[] strList = url.Split('?'); + if (strList.Length < 2) { return cookieContainer; } + + string[] strList2 = strList[1].Split('&'); + if (strList2.Length == 0) { return cookieContainer; } + + // 获取expires + string expires = strList2.FirstOrDefault(it => it.Contains("Expires")).Split('=')[1]; + DateTime dateTime = DateTime.Now; + dateTime = dateTime.AddSeconds(int.Parse(expires)); + + foreach (var item in strList2) + { + string[] strList3 = item.Split('='); + if (strList3.Length < 2) { continue; } + + string name = strList3[0]; + string value = strList3[1]; + value = HttpUtility.UrlEncode(value); + + // 不需要 + if (name == "Expires" || name == "gourl") { continue; } + + // 添加cookie + cookieContainer.Add(new Cookie(name, value, "/", ".bilibili.com") { Expires = dateTime }); + + } + + return cookieContainer; + } + + /// + /// 解析从浏览器获取的cookies,用于设置cookie + /// + /// + /// + public static CookieContainer ParseCookieByString(string str) + { + var cookieContainer = new CookieContainer(); + + var cookies = str.Replace(" ", "").Split(";"); + foreach (var cookie in cookies) + { + DateTime dateTime = DateTime.Now; + dateTime = dateTime.AddMonths(12); + + var temp = cookie.Split("="); + var name = temp[0]; + var value = temp[1]; + + // 添加cookie + cookieContainer.Add(new Cookie(name, value, "/", ".bilibili.com") { Expires = dateTime }); + } + return cookieContainer; + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/ILogin.cs b/src/Downkyi.Core/Bili/ILogin.cs new file mode 100644 index 0000000..7cf6a6a --- /dev/null +++ b/src/Downkyi.Core/Bili/ILogin.cs @@ -0,0 +1,25 @@ +using Downkyi.Core.Bili.Models; + +namespace Downkyi.Core.Bili; + +public interface ILogin +{ + /// + /// 申请二维码 + /// + /// (url, key) + Tuple GetQRCodeUrl(); + + /// + /// 扫码登录 + /// + /// + /// + QRCodeStatus PollQRCode(string qrcodeKey); + + /// + /// 导航栏用户信息 + /// + /// + NavigationInfo GetNavigationInfo(); +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/IUser.cs b/src/Downkyi.Core/Bili/IUser.cs new file mode 100644 index 0000000..e98f65d --- /dev/null +++ b/src/Downkyi.Core/Bili/IUser.cs @@ -0,0 +1,5 @@ +namespace Downkyi.Core.Bili; + +public interface IUser +{ +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Models/NavigationInfo.cs b/src/Downkyi.Core/Bili/Models/NavigationInfo.cs new file mode 100644 index 0000000..927ebc2 --- /dev/null +++ b/src/Downkyi.Core/Bili/Models/NavigationInfo.cs @@ -0,0 +1,10 @@ +namespace Downkyi.Core.Bili.Models; + +public class NavigationInfo +{ + public long Mid { get; set; } + public string Name { get; set; } + public string Header { get; set; } + public int VipStatus { get; set; } // 会员开通状态 // 0:无;1:有 + public bool IsLogin { get; set; } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Models/QRCodeStatus.cs b/src/Downkyi.Core/Bili/Models/QRCodeStatus.cs new file mode 100644 index 0000000..05f4e27 --- /dev/null +++ b/src/Downkyi.Core/Bili/Models/QRCodeStatus.cs @@ -0,0 +1,17 @@ +namespace Downkyi.Core.Bili.Models; + +public class QRCodeStatus +{ + public string Url { get; set; } + public string Token { get; set; } + public long Timestamp { get; set; } + + /// + /// 0:扫码登录成功 + /// 86038:二维码已失效 + /// 86090:二维码已扫码未确认 + /// 86101:未扫码 + /// + public long Code { get; set; } + public string Message { get; set; } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Models/Quality.cs b/src/Downkyi.Core/Bili/Models/Quality.cs new file mode 100644 index 0000000..aad41c6 --- /dev/null +++ b/src/Downkyi.Core/Bili/Models/Quality.cs @@ -0,0 +1,7 @@ +namespace Downkyi.Core.Bili.Models; + +public class Quality +{ + public string Name { get; set; } + public int Id { get; set; } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Utils/BvId.cs b/src/Downkyi.Core/Bili/Utils/BvId.cs new file mode 100644 index 0000000..4951631 --- /dev/null +++ b/src/Downkyi.Core/Bili/Utils/BvId.cs @@ -0,0 +1,60 @@ +namespace Downkyi.Core.Bili.Utils; + +public static class BvId +{ + private const string tableStr = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"; //码表 + private static readonly char[] table = tableStr.ToCharArray(); + + private static readonly char[] tr = new char[124]; //反查码表 + private const ulong Xor = 177451812; //固定异或值 + private const ulong add = 8728348608; //固定加法值 + private static readonly int[] s = { 11, 10, 3, 8, 4, 6 }; //位置编码表 + + static BvId() + { + Tr_init(); + } + + //初始化反查码表 + private static void Tr_init() + { + for (int i = 0; i < 58; i++) + tr[table[i]] = (char)i; + } + + /// + /// bvid转avid + /// + /// + /// + public static ulong Bv2Av(string bvid) + { + char[] bv = bvid.ToCharArray(); + + ulong r = 0; + ulong av; + for (int i = 0; i < 6; i++) + r += tr[bv[s[i]]] * (ulong)Math.Pow(58, i); + av = (r - add) ^ Xor; + return av; + } + + /// + /// avid转bvid + /// + /// + /// + public static string Av2Bv(ulong av) + { + //编码结果 + string res = "BV1 4 1 7 "; + char[] result = res.ToCharArray(); + + av = (av ^ Xor) + add; + for (int i = 0; i < 6; i++) + result[s[i]] = table[av / (ulong)Math.Pow(58, i) % 58]; + var bv = new string(result); + return bv; + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Utils/DanmakuSender.cs b/src/Downkyi.Core/Bili/Utils/DanmakuSender.cs new file mode 100644 index 0000000..4cffd80 --- /dev/null +++ b/src/Downkyi.Core/Bili/Utils/DanmakuSender.cs @@ -0,0 +1,151 @@ +namespace Downkyi.Core.Bili.Utils; + +public static class DanmakuSender +{ + private const uint CRCPOLYNOMIAL = 0xEDB88320; + private static readonly uint[] crctable = new uint[256]; + + static DanmakuSender() + { + CreateTable(); + } + + private static void CreateTable() + { + for (int i = 0; i < 256; i++) + { + uint crcreg = (uint)i; + + for (int j = 0; j < 8; j++) + { + if ((crcreg & 1) != 0) + { + crcreg = CRCPOLYNOMIAL ^ (crcreg >> 1); + } + else + { + crcreg >>= 1; + } + } + crctable[i] = crcreg; + } + } + + private static uint Crc32(string userId) + { + uint crcstart = 0xFFFFFFFF; + for (int i = 0; i < userId.Length; i++) + { + uint index = (uint)(crcstart ^ (int)userId[i]) & 255; + crcstart = (crcstart >> 8) ^ crctable[index]; + } + return crcstart; + } + + private static uint Crc32LastIndex(string userId) + { + uint index = 0; + uint crcstart = 0xFFFFFFFF; + for (int i = 0; i < userId.Length; i++) + { + index = (uint)((crcstart ^ (int)userId[i]) & 255); + crcstart = (crcstart >> 8) ^ crctable[index]; + } + return index; + } + + private static int GetCrcIndex(long t) + { + for (int i = 0; i < 256; i++) + { + if ((crctable[i] >> 24) == t) + { + return i; + } + } + return -1; + } + + private static object[] DeepCheck(int i, int[] index) + { + object[] resultArray = new object[2]; + + string result = ""; + uint tc;// = 0x00; + var hashcode = Crc32(i.ToString()); + tc = (uint)(hashcode & 0xff ^ index[2]); + + if (!(tc <= 57 && tc >= 48)) + { + resultArray[0] = 0; + return resultArray; + } + + result += (tc - 48).ToString(); + hashcode = crctable[index[2]] ^ (hashcode >> 8); + tc = (uint)(hashcode & 0xff ^ index[1]); + + if (!(tc <= 57 && tc >= 48)) + { + resultArray[0] = 0; + return resultArray; + } + + result += (tc - 48).ToString(); + hashcode = crctable[index[1]] ^ (hashcode >> 8); + tc = (uint)(hashcode & 0xff ^ index[0]); + + if (!(tc <= 57 && tc >= 48)) + { + resultArray[0] = 0; + return resultArray; + } + + result += (tc - 48).ToString(); + //hashcode = crctable[index[0]] ^ (hashcode >> 8); + + resultArray[0] = 1; + resultArray[1] = result; + return resultArray; + } + + /// + /// 查询弹幕发送者 + /// + /// + /// + public static string FindDanmakuSender(string userId) + { + object[] deepCheckData = new object[2]; + + int[] index = new int[4]; + uint ht = (uint)Convert.ToInt32($"0x{userId}", 16); + ht ^= 0xffffffff; + + int i; + for (i = 3; i > -1; i--) + { + index[3 - i] = GetCrcIndex(ht >> (i * 8)); + uint snum = crctable[index[3 - i]]; + ht ^= snum >> ((3 - i) * 8); + } + for (i = 0; i < 100000000; i++) + { + uint lastindex = Crc32LastIndex(i.ToString()); + if (lastindex == index[3]) + { + deepCheckData = DeepCheck(i, index); + if ((int)deepCheckData[0] != 0) + { + break; + } + } + } + if (i == 100000000) + { + return "-1"; + } + return $"{i}{deepCheckData[1]}"; + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Utils/ParseEntrance.cs b/src/Downkyi.Core/Bili/Utils/ParseEntrance.cs new file mode 100644 index 0000000..4ad1f93 --- /dev/null +++ b/src/Downkyi.Core/Bili/Utils/ParseEntrance.cs @@ -0,0 +1,556 @@ +using Downkyi.Core.Utils.Validator; +using System.Text.RegularExpressions; + +namespace Downkyi.Core.Bili.Utils; + +/// +/// 解析输入的字符串 +/// 支持的格式有: +/// av号:av170001, AV170001, https://www.bilibili.com/video/av170001 +/// BV号:BV17x411w7KC, https://www.bilibili.com/video/BV17x411w7KC, https://b23.tv/BV17x411w7KC +/// 番剧(电影、电视剧)ss号:ss32982, SS32982, https://www.bilibili.com/bangumi/play/ss32982 +/// 番剧(电影、电视剧)ep号:ep317925, EP317925, https://www.bilibili.com/bangumi/play/ep317925 +/// 番剧(电影、电视剧)md号:md28228367, MD28228367, https://www.bilibili.com/bangumi/media/md28228367 +/// 课程ss号:https://www.bilibili.com/cheese/play/ss205 +/// 课程ep号:https://www.bilibili.com/cheese/play/ep3489 +/// 收藏夹:ml1329019876, ML1329019876, https://www.bilibili.com/medialist/detail/ml1329019876, https://www.bilibili.com/medialist/play/ml1329019876/ +/// 用户空间:uid928123, UID928123, uid:928123, UID:928123, https://space.bilibili.com/928123 +/// +public static class ParseEntrance +{ + public static readonly string WwwUrl = "https://www.bilibili.com"; + public static readonly string ShareWwwUrl = "https://www.bilibili.com/s"; + public static readonly string ShortUrl = "https://b23.tv/"; + public static readonly string MobileUrl = "https://m.bilibili.com"; + + public static readonly string SpaceUrl = "https://space.bilibili.com"; + + public static readonly string VideoUrl = $"{WwwUrl}/video/"; + public static readonly string BangumiUrl = $"{WwwUrl}/bangumi/play/"; + public static readonly string BangumiMediaUrl = $"{WwwUrl}/bangumi/media/"; + public static readonly string CheeseUrl = $"{WwwUrl}/cheese/play/"; + public static readonly string FavoritesUrl1 = $"{WwwUrl}/medialist/detail/"; + public static readonly string FavoritesUrl2 = $"{WwwUrl}/medialist/play/"; + + #region 视频 + + /// + /// 是否为av id + /// + /// + /// + public static bool IsAvId(string input) + { + return IsIntId(input, "av"); + } + + /// + /// 是否为av url + /// + /// + /// + public static bool IsAvUrl(string input) + { + string id = GetVideoId(input); + return IsAvId(id); + } + + /// + /// 获取av id + /// + /// + /// + public static long GetAvId(string input) + { + if (IsAvId(input)) + { + return Number.GetInt(input.Remove(0, 2)); + } + else if (IsAvUrl(input)) + { + return Number.GetInt(GetVideoId(input).Remove(0, 2)); + } + else + { + return -1; + } + } + + /// + /// 是否为bv id + /// + /// + /// + public static bool IsBvId(string input) + { + return input.StartsWith("BV") && input.Length == 12; + } + + /// + /// 是否为bv url + /// + /// + /// + public static bool IsBvUrl(string input) + { + string id = GetVideoId(input); + return IsBvId(id); + } + + /// + /// 获取bv id + /// + /// + /// + public static string GetBvId(string input) + { + if (IsBvId(input)) + { + return input; + } + else if (IsBvUrl(input)) + { + return GetVideoId(input); + } + else + { + return ""; + } + } + + #endregion + + #region 番剧(电影、电视剧) + + /// + /// 是否为番剧season id + /// + /// + /// + public static bool IsBangumiSeasonId(string input) + { + return IsIntId(input, "ss"); + } + + /// + /// 是否为番剧season url + /// + /// + /// + public static bool IsBangumiSeasonUrl(string input) + { + string id = GetBangumiId(input); + return IsBangumiSeasonId(id); + } + + /// + /// 获取番剧season id + /// + /// + /// + public static long GetBangumiSeasonId(string input) + { + if (IsBangumiSeasonId(input)) + { + return Number.GetInt(input.Remove(0, 2)); + } + else if (IsBangumiSeasonUrl(input)) + { + return Number.GetInt(GetBangumiId(input).Remove(0, 2)); + } + else + { + return -1; + } + } + + /// + /// 是否为番剧episode id + /// + /// + /// + public static bool IsBangumiEpisodeId(string input) + { + return IsIntId(input, "ep"); + } + + /// + /// 是否为番剧episode url + /// + /// + /// + public static bool IsBangumiEpisodeUrl(string input) + { + string id = GetBangumiId(input); + return IsBangumiEpisodeId(id); + } + + /// + /// 获取番剧episode id + /// + /// + /// + public static long GetBangumiEpisodeId(string input) + { + if (IsBangumiEpisodeId(input)) + { + return Number.GetInt(input.Remove(0, 2)); + } + else if (IsBangumiEpisodeUrl(input)) + { + return Number.GetInt(GetBangumiId(input).Remove(0, 2)); + } + else + { + return -1; + } + } + + /// + /// 是否为番剧media id + /// + /// + /// + public static bool IsBangumiMediaId(string input) + { + return IsIntId(input, "md"); + } + + /// + /// 是否为番剧media url + /// + /// + /// + public static bool IsBangumiMediaUrl(string input) + { + string id = GetBangumiId(input); + return IsBangumiMediaId(id); + } + + /// + /// 获取番剧media id + /// + /// + /// + public static long GetBangumiMediaId(string input) + { + if (IsBangumiMediaId(input)) + { + return Number.GetInt(input.Remove(0, 2)); + } + else if (IsBangumiMediaUrl(input)) + { + return Number.GetInt(GetBangumiId(input).Remove(0, 2)); + } + else + { + return -1; + } + } + + #endregion + + #region 课程 + + /// + /// 是否为课程season url + /// + /// + /// + public static bool IsCheeseSeasonUrl(string input) + { + string id = GetCheeseId(input); + return IsIntId(id, "ss"); + } + + /// + /// 获取课程season id + /// + /// + /// + public static long GetCheeseSeasonId(string input) + { + return IsCheeseSeasonUrl(input) ? Number.GetInt(GetCheeseId(input).Remove(0, 2)) : -1; + } + + /// + /// 是否为课程episode url + /// + /// + /// + public static bool IsCheeseEpisodeUrl(string input) + { + string id = GetCheeseId(input); + return IsIntId(id, "ep"); + } + + /// + /// 获取课程episode id + /// + /// + /// + public static long GetCheeseEpisodeId(string input) + { + return IsCheeseEpisodeUrl(input) ? Number.GetInt(GetCheeseId(input).Remove(0, 2)) : -1; + } + + #endregion + + #region 收藏夹 + + /// + /// 是否为收藏夹id + /// + /// + /// + public static bool IsFavoritesId(string input) + { + return IsIntId(input, "ml"); + } + + /// + /// 是否为收藏夹url + /// + /// + /// + public static bool IsFavoritesUrl(string input) + { + return IsFavoritesUrl1(input) || IsFavoritesUrl2(input); + } + + /// + /// 是否为收藏夹url1 + /// + /// + /// + private static bool IsFavoritesUrl1(string input) + { + string favoritesId = GetId(input, FavoritesUrl1); + return IsFavoritesId(favoritesId); + } + + /// + /// 是否为收藏夹ur2 + /// + /// + /// + private static bool IsFavoritesUrl2(string input) + { + string favoritesId = GetId(input, FavoritesUrl2); + return IsFavoritesId(favoritesId.Split('/')[0]); + } + + /// + /// 获取收藏夹id + /// + /// + /// + public static long GetFavoritesId(string input) + { + if (IsFavoritesId(input)) + { + return Number.GetInt(input.Remove(0, 2)); + } + else if (IsFavoritesUrl1(input)) + { + return Number.GetInt(GetId(input, FavoritesUrl1).Remove(0, 2)); + } + else if (IsFavoritesUrl2(input)) + { + return Number.GetInt(GetId(input, FavoritesUrl2).Remove(0, 2).Split('/')[0]); + } + else + { + return -1; + } + } + + #endregion + + #region 用户空间 + + /// + /// 是否为用户id + /// + /// + /// + public static bool IsUserId(string input) + { + if (input.ToLower().StartsWith("uid:")) + { + return Regex.IsMatch(input.Remove(0, 4), @"^\d+$"); + } + else if (input.ToLower().StartsWith("uid")) + { + return Regex.IsMatch(input.Remove(0, 3), @"^\d+$"); + } + else { return false; } + } + + /// + /// 是否为用户空间url + /// + /// + /// + public static bool IsUserUrl(string input) + { + if (!IsUrl(input)) { return false; } + + if (input.Contains("space.bilibili.com")) + { + return true; + } + else + { + return false; + } + } + + /// + /// 获取用户mid + /// + /// + /// + public static long GetUserId(string input) + { + if (input.ToLower().StartsWith("uid:")) + { + return Number.GetInt(input.Remove(0, 4)); + } + else if (input.ToLower().StartsWith("uid")) + { + return Number.GetInt(input.Remove(0, 3)); + } + else if (IsUserUrl(input)) + { + string url = EnableHttps(input); + url = DeleteUrlParam(url); + var match = Regex.Match(url, @"\d+"); + if (match.Success) + { + return long.Parse(match.Value); + } + else + { + return -1; + } + } + else + { + return -1; + } + } + + #endregion + + /// + /// 是否为网址 + /// + /// + /// + private static bool IsUrl(string input) + { + return input.StartsWith("http://") || input.StartsWith("https://"); + } + + /// + /// 将http转为https + /// + /// + private static string EnableHttps(string url) + { + if (!IsUrl(url)) { return null; } + + return url.Replace("http://", "https://"); + } + + /// + /// 去除url中的参数 + /// + /// + /// + private static string DeleteUrlParam(string url) + { + string[] strList = url.Split('?'); + + return strList[0].EndsWith("/") ? strList[0].TrimEnd('/') : strList[0]; + } + + /// + /// 从url中获取视频id(avid/bvid) + /// + /// + /// + private static string GetVideoId(string input) + { + return GetId(input, VideoUrl); + } + + /// + /// 从url中获取番剧id(ss/ep/md) + /// + /// + /// + private static string GetBangumiId(string input) + { + string id = GetId(input, BangumiUrl); + if (id != "") { return id; } + return GetId(input, BangumiMediaUrl); + } + + /// + /// 从url中获取课程id(ss/ep) + /// + /// + /// + private static string GetCheeseId(string input) + { + return GetId(input, CheeseUrl); + } + + /// + /// 是否为数字型id + /// + /// + /// + /// + private static bool IsIntId(string input, string prefix) + { + if (input.ToLower().StartsWith(prefix)) + { + return Regex.IsMatch(input.Remove(0, 2), @"^\d+$"); + } + return false; + } + + /// + /// 从url中获取id + /// + /// + /// + /// + private static string GetId(string input, string baseUrl) + { + if (!IsUrl(input)) { return ""; } + + string url = EnableHttps(input); + url = DeleteUrlParam(url); + + url = url.Replace(ShareWwwUrl, WwwUrl); + url = url.Replace(MobileUrl, WwwUrl); + + if (url.Contains("b23.tv/ss") || url.Contains("b23.tv/ep")) + { + url = url.Replace(ShortUrl, BangumiUrl); + } + else + { + url = url.Replace(ShortUrl, VideoUrl); + } + + if (!url.StartsWith(baseUrl)) { return ""; } + + return url.Replace(baseUrl, ""); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Utils/QualityList.cs b/src/Downkyi.Core/Bili/Utils/QualityList.cs new file mode 100644 index 0000000..b9e30a1 --- /dev/null +++ b/src/Downkyi.Core/Bili/Utils/QualityList.cs @@ -0,0 +1,73 @@ +using Downkyi.Core.Bili.Models; + +namespace Downkyi.Core.Bili.Utils; + +public static class QualityList +{ + private static readonly List resolutions = new() + { + new Quality { Name = "360P 流畅", Id = 16 }, + new Quality { Name = "480P 清晰", Id = 32 }, + new Quality { Name = "720P 高清", Id = 64 }, + new Quality { Name = "720P 60帧", Id = 74 }, + new Quality { Name = "1080P 高清", Id = 80 }, + new Quality { Name = "1080P 高码率", Id = 112 }, + new Quality { Name = "1080P 60帧", Id = 116 }, + new Quality { Name = "4K 超清", Id = 120 }, + new Quality { Name = "HDR 真彩", Id = 125 }, + new Quality { Name = "杜比视界", Id = 126 }, + new Quality { Name = "超高清 8K", Id = 127 }, + }; + + private static readonly List codecIds = new() + { + new Quality { Name = "H.264/AVC", Id = 7 }, + new Quality { Name = "H.265/HEVC", Id = 12 }, + new Quality { Name = "AV1", Id = 13 }, + }; + + private static readonly List qualities = new() + { + new Quality { Name = "低质量", Id = 30216 }, + new Quality { Name = "中质量", Id = 30232 }, + new Quality { Name = "高质量", Id = 30280 }, + new Quality { Name = "Dolby Atmos", Id = 30250 }, + new Quality { Name = "Hi-Res无损", Id = 30251 }, + }; + + /// + /// 获取支持的视频画质 + /// + /// + public static List GetResolutions() + { + // 使用深复制, + // 保证外部修改list后, + // 不会影响其他调用处 + return new List(resolutions); + } + + /// + /// 获取视频编码代码 + /// + /// + public static List GetCodecIds() + { + // 使用深复制, + // 保证外部修改list后, + // 不会影响其他调用处 + return new List(codecIds); + } + + /// + /// 获取支持的视频音质 + /// + /// + public static List GetAudioQualities() + { + // 使用深复制, + // 保证外部修改list后, + // 不会影响其他调用处 + return new List(qualities); + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Web/Login.cs b/src/Downkyi.Core/Bili/Web/Login.cs new file mode 100644 index 0000000..c445cff --- /dev/null +++ b/src/Downkyi.Core/Bili/Web/Login.cs @@ -0,0 +1,72 @@ +using BiliSharp; +using BiliSharp.Api.Login; +using Downkyi.Core.Bili.Models; +using Downkyi.Core.Settings; + +namespace Downkyi.Core.Bili.Web; + +public class Login : ILogin +{ + /// + /// 申请二维码(web端) + /// + /// (url, key) + public Tuple GetQRCodeUrl() + { + string userAgent = SettingsManager.GetInstance().GetUserAgent(); + BiliManager.Instance().SetUserAgent(userAgent); + + var qrcode = LoginQR.GenerateQRCode(); + if (qrcode == null || qrcode.Data == null) { return null; } + + return Tuple.Create(qrcode.Data.Url, qrcode.Data.QrcodeKey); + } + + /// + /// 扫码登录(web端) + /// + /// + public QRCodeStatus PollQRCode(string qrcodeKey) + { + string userAgent = SettingsManager.GetInstance().GetUserAgent(); + BiliManager.Instance().SetUserAgent(userAgent); + + var qrcode = LoginQR.PollQRCode(qrcodeKey); + if (qrcode == null || qrcode.Data == null) { return null; } + + return new QRCodeStatus() + { + Url = qrcode.Data.Url, + Token = qrcode.Data.RefreshToken, + Timestamp = qrcode.Data.Timestamp, + Code = qrcode.Data.Code, + Message = qrcode.Data.Message + }; + } + + /// + /// 导航栏用户信息 + /// + /// + public NavigationInfo GetNavigationInfo() + { + string userAgent = SettingsManager.GetInstance().GetUserAgent(); + BiliManager.Instance().SetUserAgent(userAgent); + BiliManager.Instance().SetCookies(LoginHelper.GetLoginInfoCookies()); + + var origin = LoginInfo.GetNavigationInfo(); + if (origin == null || origin.Data == null) + { + return null; + } + + return new NavigationInfo() + { + Mid = origin.Data.Mid, + Name = origin.Data.Uname, + Header = origin.Data.Face, + VipStatus = origin.Data.Vipstatus, + IsLogin = origin.Data.Islogin + }; + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Web/LoginHelper.cs b/src/Downkyi.Core/Bili/Web/LoginHelper.cs new file mode 100644 index 0000000..c0d8943 --- /dev/null +++ b/src/Downkyi.Core/Bili/Web/LoginHelper.cs @@ -0,0 +1,137 @@ +using Downkyi.Core.Settings; +using Downkyi.Core.Settings.Models; +using Downkyi.Core.Utils.Encryptor; +using System.Net; + +namespace Downkyi.Core.Bili.Web; + +public static class LoginHelper +{ + // 本地位置 + private static readonly string LOCAL_LOGIN_INFO = Storage.StorageManager.GetLogin(); + + private static readonly EncryptorFile encryptor = new(); + + /// + /// 保存登录的cookies到文件 + /// + /// + /// + public static bool SaveLoginInfoCookies(string url) + { + CookieContainer cookieContainer = Cookies.ParseCookieByUrl(url); + + return SaveLoginInfoCookies(cookieContainer); + } + + /// + /// 保存cookies到文件 + /// + /// + /// + public static bool SaveLoginInfoCookies(CookieContainer cookieContainer) + { + string tempFile = LOCAL_LOGIN_INFO + "-" + Guid.NewGuid().ToString("N"); + + bool isSucceed = Cookies.WriteCookiesToDisk(tempFile, cookieContainer); + if (isSucceed) + { + try + { + encryptor.EncryptFile(tempFile, LOCAL_LOGIN_INFO); + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + return false; + } + } + + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return isSucceed; + } + + /// + /// 获得登录的cookies + /// + /// + public static CookieContainer GetLoginInfoCookies() + { + string tempFile = LOCAL_LOGIN_INFO + "-" + Guid.NewGuid().ToString("N"); + + if (File.Exists(LOCAL_LOGIN_INFO)) + { + try + { + encryptor.DecryptFile(LOCAL_LOGIN_INFO, tempFile); + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return null; + } + } + else { return null; } + + CookieContainer cookies = Cookies.ReadCookiesFromDisk(tempFile); + + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + return cookies; + } + + /// + /// 返回登录信息的cookies的字符串 + /// + /// + public static string GetLoginInfoCookiesString() + { + var cookieContainer = GetLoginInfoCookies(); + if (cookieContainer == null) + { + return ""; + } + + return Cookies.GetCookiesString(cookieContainer); + } + + /// + /// 注销登录 + /// + /// + public static bool Logout() + { + if (File.Exists(LOCAL_LOGIN_INFO)) + { + try + { + File.Delete(LOCAL_LOGIN_INFO); + + SettingsManager.GetInstance().SetUserInfo(new UserInfoSettings + { + Mid = -1, + Name = "", + IsLogin = false, + IsVip = false + }); + return true; + } + catch (IOException e) + { + Log.Log.Logger.Error(e); + return false; + } + } + return false; + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Bili/Web/User.cs b/src/Downkyi.Core/Bili/Web/User.cs new file mode 100644 index 0000000..be10494 --- /dev/null +++ b/src/Downkyi.Core/Bili/Web/User.cs @@ -0,0 +1,5 @@ +namespace Downkyi.Core.Bili.Web; + +public class User : IUser +{ +} \ No newline at end of file diff --git a/src/Downkyi.Core/Downkyi.Core.csproj b/src/Downkyi.Core/Downkyi.Core.csproj new file mode 100644 index 0000000..483796d --- /dev/null +++ b/src/Downkyi.Core/Downkyi.Core.csproj @@ -0,0 +1,22 @@ + + + net6.0 + enable + + + none + + + + + + + + + + + + + + + diff --git a/src/Downkyi.Core/Downloader/MultiThreadDownloader.cs b/src/Downkyi.Core/Downloader/MultiThreadDownloader.cs new file mode 100644 index 0000000..4eaf589 --- /dev/null +++ b/src/Downkyi.Core/Downloader/MultiThreadDownloader.cs @@ -0,0 +1,441 @@ +using System.ComponentModel; +using System.Net; + +namespace Downkyi.Core.Downloader; + +/// +/// 文件合并改变事件 +/// +/// +/// +public delegate void FileMergeProgressChangedEventHandler(object sender, int e); + +/// +/// 多线程下载器 +/// +public class MultiThreadDownloader +{ + #region 属性 + + private string _url; + private bool _rangeAllowed; + private readonly HttpWebRequest _request; + private Action _requestConfigure = req => req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36"; + + #endregion 属性 + + #region 公共属性 + + /// + /// RangeAllowed + /// + public bool RangeAllowed + { + get => _rangeAllowed; + set => _rangeAllowed = value; + } + + /// + /// 临时文件夹 + /// + public string TempFileDirectory { get; set; } + + /// + /// url地址 + /// + public string Url + { + get => _url; + set => _url = value; + } + + /// + /// 第几部分 + /// + public int NumberOfParts { get; set; } + + /// + /// 已接收字节数 + /// + public long TotalBytesReceived + { + get + { + try + { + lock (this) + { + return PartialDownloaderList.Where(t => t != null).Sum(t => t.TotalBytesRead); + } + } + catch + { + return 0; + } + } + } + + /// + /// 总进度 + /// + public float TotalProgress { get; private set; } + + /// + /// 文件大小 + /// + public long Size { get; private set; } + + /// + /// 下载速度 + /// + public float TotalSpeedInBytes + { + get + { + lock (this) + { + return PartialDownloaderList.Sum(t => t.SpeedInBytes); + } + } + } + + /// + /// 下载块 + /// + public List PartialDownloaderList { get; } + + /// + /// 文件路径 + /// + public string FilePath { get; set; } + + #endregion 公共属性 + + #region 变量 + + /// + /// 总下载进度更新事件 + /// + public event EventHandler TotalProgressChanged; + + /// + /// 文件合并完成事件 + /// + public event EventHandler FileMergedComplete; + + /// + /// 文件合并事件 + /// + public event FileMergeProgressChangedEventHandler FileMergeProgressChanged; + + private readonly AsyncOperation _aop; + + #endregion 变量 + + #region 下载管理器 + + /// + /// 多线程下载管理器 + /// + /// + /// + /// + /// + public MultiThreadDownloader(string sourceUrl, string tempDir, string savePath, int numOfParts) + { + _url = sourceUrl; + NumberOfParts = numOfParts; + TempFileDirectory = tempDir; + PartialDownloaderList = new List(); + _aop = AsyncOperationManager.CreateOperation(null); + FilePath = savePath; +#pragma warning disable SYSLIB0014 // 类型或成员已过时 + _request = WebRequest.Create(sourceUrl) as HttpWebRequest; +#pragma warning restore SYSLIB0014 // 类型或成员已过时 + } + + /// + /// 多线程下载管理器 + /// + /// + /// + /// + public MultiThreadDownloader(string sourceUrl, string savePath, int numOfParts) : this(sourceUrl, null, savePath, numOfParts) + { + TempFileDirectory = Environment.GetEnvironmentVariable("temp"); + } + + /// + /// 多线程下载管理器 + /// + /// + /// + public MultiThreadDownloader(string sourceUrl, int numOfParts) : this(sourceUrl, null, numOfParts) + { + } + + #endregion 下载管理器 + + #region 事件 + + private void temp_DownloadPartCompleted(object sender, EventArgs e) + { + WaitOrResumeAll(PartialDownloaderList, true); + + if (TotalBytesReceived == Size) + { + UpdateProgress(); + MergeParts(); + return; + } + + PartialDownloaderList.Sort((x, y) => (int)(y.RemainingBytes - x.RemainingBytes)); + var rem = PartialDownloaderList[0].RemainingBytes; + if (rem < 50 * 1024) + { + WaitOrResumeAll(PartialDownloaderList, false); + return; + } + + var from = PartialDownloaderList[0].CurrentPosition + rem / 2; + var to = PartialDownloaderList[0].To; + if (from > to) + { + WaitOrResumeAll(PartialDownloaderList, false); + return; + } + + PartialDownloaderList[0].To = from - 1; + WaitOrResumeAll(PartialDownloaderList, false); + var temp = new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), from, to, true); + temp.DownloadPartCompleted += temp_DownloadPartCompleted; + temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged; + lock (this) + { + PartialDownloaderList.Add(temp); + } + temp.Start(_requestConfigure); + } + + private void temp_DownloadPartProgressChanged(object sender, EventArgs e) + { + UpdateProgress(); + } + + private void UpdateProgress() + { + int pr = (int)(TotalBytesReceived * 1d / Size * 100); + if (TotalProgress != pr) + { + TotalProgress = pr; + if (TotalProgressChanged != null) + { + _aop.Post(state => TotalProgressChanged(this, EventArgs.Empty), null); + } + } + } + + #endregion 事件 + + #region 方法 + + private void CreateFirstPartitions() + { + Size = GetContentLength(ref _rangeAllowed, ref _url); + int maximumPart = (int)(Size / (25 * 1024)); + maximumPart = maximumPart == 0 ? 1 : maximumPart; + if (!_rangeAllowed) + { + NumberOfParts = 1; + } + else if (NumberOfParts > maximumPart) + { + NumberOfParts = maximumPart; + } + + for (int i = 0; i < NumberOfParts; i++) + { + var temp = CreateNew(i, NumberOfParts, Size); + temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged; + temp.DownloadPartCompleted += temp_DownloadPartCompleted; + lock (this) + { + PartialDownloaderList.Add(temp); + } + temp.Start(_requestConfigure); + } + } + + private void MergeParts() + { + var mergeOrderedList = PartialDownloaderList.OrderBy(x => x.From); + var dir = new FileInfo(FilePath).DirectoryName; + Directory.CreateDirectory(dir); + + + using (var fs = File.OpenWrite(FilePath)) + { + long totalBytesWrite = 0; + int mergeProgress = 0; + foreach (var item in mergeOrderedList) + { + using (var pdi = File.OpenRead(item.FullPath)) + { + byte[] buffer = new byte[4096]; + int read; + while ((read = pdi.Read(buffer, 0, buffer.Length)) > 0) + { + fs.Write(buffer, 0, read); + totalBytesWrite += read; + int temp = (int)(totalBytesWrite * 1d / Size * 100); + if (temp != mergeProgress && FileMergeProgressChanged != null) + { + mergeProgress = temp; + _aop.Post(state => FileMergeProgressChanged(this, temp), null); + } + } + } + + try + { + File.Delete(item.FullPath); + } + catch + { + // ignored + } + } + } + + if (FileMergedComplete != null) + { + _aop.Post(state => FileMergedComplete(state, EventArgs.Empty), this); + } + } + + private PartialDownloader CreateNew(int order, int parts, long contentLength) + { + var division = contentLength / parts; + var remaining = contentLength % parts; + var start = division * order; + var end = start + division - 1; + end += order == parts - 1 ? remaining : 0; + return new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString("N"), start, end, true); + } + + /// + /// 暂停或继续 + /// + /// + /// + public static void WaitOrResumeAll(List list, bool wait) + { + for (var index = 0; index < list.Count; index++) + { + if (wait) + { + list[index].Wait(); + } + else + { + list[index].ResumeAfterWait(); + } + } + } + + /// + /// 配置请求头 + /// + /// + public void Configure(Action config) + { + _requestConfigure = config; + } + + /// + /// 获取内容长度 + /// + /// + /// + /// + public long GetContentLength(ref bool rangeAllowed, ref string redirectedUrl) + { + _request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36"; + _request.ServicePoint.ConnectionLimit = 4; + _requestConfigure(_request); + + using (var resp = _request.GetResponse() as HttpWebResponse) + { + redirectedUrl = resp.ResponseUri.OriginalString; + var ctl = resp.ContentLength; + rangeAllowed = resp.Headers.AllKeys.Select((v, i) => new + { + HeaderName = v, + HeaderValue = resp.Headers[i] + }).Any(k => k.HeaderName.ToLower().Contains("range") && k.HeaderValue.ToLower().Contains("byte")); + _request.Abort(); + return ctl; + } + } + + #endregion 方法 + + #region 公共方法 + + /// + /// 暂停下载 + /// + public void Pause() + { + lock (this) + { + foreach (var t in PartialDownloaderList.Where(t => !t.Completed)) + { + t.Stop(); + } + } + + Thread.Sleep(200); + } + + /// + /// 开始下载 + /// + public void Start() + { + Task th = new Task(CreateFirstPartitions); + th.Start(); + } + + /// + /// 唤醒下载 + /// + public void Resume() + { + int count = PartialDownloaderList.Count; + for (int i = 0; i < count; i++) + { + if (PartialDownloaderList[i].Stopped) + { + var from = PartialDownloaderList[i].CurrentPosition + 1; + var to = PartialDownloaderList[i].To; + if (from > to) + { + continue; + } + + var temp = new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), from, to, _rangeAllowed); + temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged; + temp.DownloadPartCompleted += temp_DownloadPartCompleted; + lock (this) + { + PartialDownloaderList.Add(temp); + } + PartialDownloaderList[i].To = PartialDownloaderList[i].CurrentPosition; + temp.Start(_requestConfigure); + } + } + } + + #endregion 公共方法 +} \ No newline at end of file diff --git a/src/Downkyi.Core/Downloader/PartialDownloader.cs b/src/Downkyi.Core/Downloader/PartialDownloader.cs new file mode 100644 index 0000000..a832f6a --- /dev/null +++ b/src/Downkyi.Core/Downloader/PartialDownloader.cs @@ -0,0 +1,262 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Net; + +namespace Downkyi.Core.Downloader; + +/// +/// 部分下载器 +/// +public class PartialDownloader +{ + /// + /// 这部分完成事件 + /// + public event EventHandler DownloadPartCompleted; + + /// + /// 部分下载进度改变事件 + /// + public event EventHandler DownloadPartProgressChanged; + + /// + /// 部分下载停止事件 + /// + public event EventHandler DownloadPartStopped; + + private readonly AsyncOperation _aop = AsyncOperationManager.CreateOperation(null); + private readonly int[] _lastSpeeds; + private long _counter; + private long _to; + private long _totalBytesRead; + private bool _wait; + + /// + /// 下载已停止 + /// + public bool Stopped { get; private set; } + + /// + /// 下载已完成 + /// + public bool Completed { get; private set; } + + /// + /// 下载进度 + /// + public int Progress { get; private set; } + + /// + /// 下载目录 + /// + public string Directory { get; } + + /// + /// 文件名 + /// + public string FileName { get; } + + /// + /// 已读字节数 + /// + public long TotalBytesRead => _totalBytesRead; + + /// + /// 内容长度 + /// + public long ContentLength { get; private set; } + + /// + /// RangeAllowed + /// + public bool RangeAllowed { get; } + + /// + /// url + /// + public string Url { get; } + + /// + /// to + /// + public long To + { + get => _to; + set + { + _to = value; + ContentLength = _to - From + 1; + } + } + + /// + /// from + /// + public long From { get; } + + /// + /// 当前位置 + /// + public long CurrentPosition => From + _totalBytesRead - 1; + + /// + /// 剩余字节数 + /// + public long RemainingBytes => ContentLength - _totalBytesRead; + + /// + /// 完整路径 + /// + public string FullPath => Path.Combine(Directory, FileName); + + /// + /// 下载速度 + /// + public int SpeedInBytes + { + get + { + if (Completed) + { + return 0; + } + + int totalSpeeds = _lastSpeeds.Sum(); + return totalSpeeds / 10; + } + } + + /// + /// 部分块下载 + /// + /// + /// + /// + /// + /// + /// + public PartialDownloader(string url, string dir, string fileGuid, long from, long to, bool rangeAllowed) + { + From = from; + _to = to; + Url = url; + RangeAllowed = rangeAllowed; + FileName = fileGuid; + Directory = dir; + _lastSpeeds = new int[10]; + } + + private void DownloadProcedure(Action config) + { + using var file = new FileStream(FullPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete); + var sw = new Stopwatch(); +#pragma warning disable SYSLIB0014 // 类型或成员已过时 + if (WebRequest.Create(Url) is HttpWebRequest req) + { + req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36"; + req.AllowAutoRedirect = true; + req.MaximumAutomaticRedirections = 5; + req.ServicePoint.ConnectionLimit += 1; + req.ServicePoint.Expect100Continue = true; + req.ProtocolVersion = HttpVersion.Version11; + req.Proxy = WebRequest.GetSystemWebProxy(); + config(req); + if (RangeAllowed) + { + req.AddRange(From, _to); + } + + if (req.GetResponse() is HttpWebResponse resp) + { + ContentLength = resp.ContentLength; + if (ContentLength <= 0 || (RangeAllowed && ContentLength != _to - From + 1)) + { + throw new Exception("Invalid response content"); + } + + using var tempStream = resp.GetResponseStream(); + int bytesRead; + byte[] buffer = new byte[4096]; + sw.Start(); + while ((bytesRead = tempStream.Read(buffer, 0, buffer.Length)) > 0) + { + if (_totalBytesRead + bytesRead > ContentLength) + { + bytesRead = (int)(ContentLength - _totalBytesRead); + } + + file.Write(buffer, 0, bytesRead); + _totalBytesRead += bytesRead; + _lastSpeeds[_counter] = (int)(_totalBytesRead / Math.Ceiling(sw.Elapsed.TotalSeconds)); + _counter = (_counter >= 9) ? 0 : _counter + 1; + int tempProgress = (int)(_totalBytesRead * 100 / ContentLength); + if (Progress != tempProgress) + { + Progress = tempProgress; + _aop.Post(state => + { + DownloadPartProgressChanged?.Invoke(this, EventArgs.Empty); + }, null); + } + + if (Stopped || (RangeAllowed && _totalBytesRead == ContentLength)) + { + break; + } + } + } + + req.Abort(); + } +#pragma warning restore SYSLIB0014 // 类型或成员已过时 + + sw.Stop(); + if (!Stopped && DownloadPartCompleted != null) + { + _aop.Post(state => + { + Completed = true; + DownloadPartCompleted(this, EventArgs.Empty); + }, null); + } + + if (Stopped && DownloadPartStopped != null) + { + _aop.Post(state => DownloadPartStopped(this, EventArgs.Empty), null); + } + } + + /// + /// 启动下载 + /// + public void Start(Action config) + { + Stopped = false; + var procThread = new Thread(_ => DownloadProcedure(config)); + procThread.Start(); + } + + /// + /// 下载停止 + /// + public void Stop() + { + Stopped = true; + } + + /// + /// 暂停等待下载 + /// + public void Wait() + { + _wait = true; + } + + /// + /// 稍后唤醒 + /// + public void ResumeAfterWait() + { + _wait = false; + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/FFmpeg/FFmpegHelper.cs b/src/Downkyi.Core/FFmpeg/FFmpegHelper.cs new file mode 100644 index 0000000..ac6a878 --- /dev/null +++ b/src/Downkyi.Core/FFmpeg/FFmpegHelper.cs @@ -0,0 +1,243 @@ +using System.Diagnostics; + +namespace Downkyi.Core.FFmpeg; + +public static class FFmpegHelper +{ + private const string Tag = "FFmpegHelper"; + + private static readonly bool is64Bit = false; + private static readonly string exec = ""; + + static FFmpegHelper() + { + is64Bit = IntPtr.Size == 8; + + if (is64Bit) + { + exec = Path.Combine(Environment.CurrentDirectory, "ffmpeg.exe"); + } + else + { + exec = Path.Combine(Environment.CurrentDirectory, "ffmpeg.exe"); + } + } + + /// + /// 合并音频和视频 + /// + /// 音频 + /// 视频 + /// + public static bool MergeVideo(string video1, string video2, string destVideo) + { + string param = $"-y -i \"{video1}\" -i \"{video2}\" -strict -2 -acodec copy -vcodec copy -f mp4 \"{destVideo}\""; + if (video1 == null || !File.Exists(video1)) + { + param = $"-y -i \"{video2}\" -strict -2 -acodec copy -vcodec copy -f mp4 \"{destVideo}\""; + } + if (video2 == null || !File.Exists(video2)) + { + param = $"-y -i \"{video1}\" -strict -2 -acodec copy \"{destVideo}\""; + } + + // 支持flac格式音频 + //param += " -strict -2"; + + if (!File.Exists(video1) && !File.Exists(video2)) { return false; } + + // 如果存在 + try { File.Delete(destVideo); } + catch (IOException e) + { + Log.Log.Logger.Error(e); + return false; + } + + ExcuteProcess(exec, param, null, (s, e) => Console.WriteLine(e.Data)); + + try + { + if (video1 != null) { File.Delete(video1); } + if (video2 != null) { File.Delete(video2); } + } + catch (IOException e) + { + Log.Log.Logger.Error(e); + } + + return true; + } + + /// + /// 拼接多个视频 + /// + /// + /// + /// + /// + public static bool ConcatVideo(string workingDirectory, List flvFiles, string destVideo) + { + // contact的文件名,不包含路径 + string concatFileName = Guid.NewGuid().ToString("N") + "_concat.txt"; + try + { + string contact = ""; + foreach (string flv in flvFiles) + { + contact += $"file '{flv}'\n"; + } + + FileStream fileStream = new(workingDirectory + "/" + concatFileName, FileMode.Create); + StreamWriter streamWriter = new(fileStream); + //开始写入 + streamWriter.Write(contact); + //清空缓冲区 + streamWriter.Flush(); + //关闭流 + streamWriter.Close(); + fileStream.Close(); + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + return false; + } + + // ffmpeg -y -f concat -safe 0 -i filelist.txt -c copy output.mkv + // 加上-y,表示如果有同名文件,则默认覆盖 + string param = $"-y -f concat -safe 0 -i {concatFileName} -c copy \"{destVideo}\" -y"; + ExcuteProcess(exec, param, workingDirectory, (s, e) => Console.WriteLine(e.Data)); + + // 删除临时文件 + try + { + // 删除concat文件 + File.Delete(workingDirectory + "/" + concatFileName); + + foreach (string flv in flvFiles) + { + File.Delete(flv); + } + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + } + + return true; + } + + /// + /// 去水印,非常消耗cpu资源 + /// + /// + /// + /// + /// + /// + /// + /// + public static void Delogo(string video, string destVideo, int x, int y, int width, int height, Action action) + { + // ffmpeg -y -i "video.mp4" -vf delogo=x=1670:y=50:w=180:h=70:show=1 "delogo.mp4" + string param = $"-y -i \"{video}\" -vf delogo=x={x}:y={y}:w={width}:h={height}:show=0 \"{destVideo}\" -hide_banner"; + ExcuteProcess(exec, param, null, (s, e) => + { + Console.WriteLine(e.Data); + action.Invoke(e.Data!); + }); + } + + /// + /// 从一个视频中仅提取音频 + /// + /// 源视频 + /// 目标音频 + /// 输出信息 + public static void ExtractAudio(string video, string audio, Action action) + { + // 抽取音频命令 + // ffmpeg -i 3.mp4 -vn -y -acodec copy 3.aac + // ffmpeg -i 3.mp4 -vn -y -acodec copy 3.m4a + string param = $"-i \"{video}\" -vn -y -acodec copy \"{audio}\" -hide_banner"; + ExcuteProcess(exec, param, + null, (s, e) => + { + Console.WriteLine(e.Data); + action.Invoke(e.Data!); + }); + } + + /// + /// 从一个视频中仅提取视频 + /// + /// 源视频 + /// 目标视频 + /// 输出信息 + public static void ExtractVideo(string video, string destVideo, Action action) + { + // 提取视频 (Extract Video) + // ffmpeg -i Life.of.Pi.has.subtitles.mkv -vcodec copy –an videoNoAudioSubtitle.mp4 + string param = $"-i \"{video}\" -y -vcodec copy -an \"{destVideo}\" -hide_banner"; + ExcuteProcess(exec, param, + null, (s, e) => + { + Console.WriteLine(e.Data); + action.Invoke(e.Data!); + }); + } + + /// + /// 提取视频的帧,输出为图片 + /// + /// + /// + /// + public static void ExtractFrame(string video, string image, uint number) + { + // 提取帧 + // ffmpeg -i caiyilin.wmv -vframes 1 wm.bmp + string param = $"-i \"{video}\" -y -vframes {number} \"{image}\""; + ExcuteProcess(exec, param, null, (s, e) => Console.WriteLine(e.Data)); + } + + + /// + /// 执行一个控制台程序 + /// + /// 程序名称 + /// 参数 + /// 工作路径 + /// 输出重定向 + private static void ExcuteProcess(string exe, string arg, string workingDirectory, DataReceivedEventHandler output) + { + using var p = new Process(); + p.StartInfo.FileName = exe; + p.StartInfo.Arguments = arg; + + // 工作目录 + if (workingDirectory != null) + { + p.StartInfo.WorkingDirectory = workingDirectory; + } + + p.StartInfo.UseShellExecute = false; //输出信息重定向 + p.StartInfo.CreateNoWindow = true; + p.StartInfo.RedirectStandardError = true; + p.StartInfo.RedirectStandardOutput = true; + + // 将 StandardErrorEncoding 改为 UTF-8 才不会出现中文乱码 + p.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8; + p.StartInfo.StandardErrorEncoding = System.Text.Encoding.UTF8; + + p.OutputDataReceived += output; + p.ErrorDataReceived += output; + + p.Start(); //启动线程 + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + p.WaitForExit(); //等待进程结束 + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/FileName/FileName.cs b/src/Downkyi.Core/FileName/FileName.cs new file mode 100644 index 0000000..310b1a3 --- /dev/null +++ b/src/Downkyi.Core/FileName/FileName.cs @@ -0,0 +1,192 @@ +using System.Text.RegularExpressions; + +namespace Downkyi.Core.FileName; + +public class FileName +{ + private readonly List nameParts; + private string order = "ORDER"; + private string section = "SECTION"; + private string mainTitle = "MAIN_TITLE"; + private string pageTitle = "PAGE_TITLE"; + private string videoZone = "VIDEO_ZONE"; + private string audioQuality = "AUDIO_QUALITY"; + private string videoQuality = "VIDEO_QUALITY"; + private string videoCodec = "VIDEO_CODEC"; + + private string videoPublishTime = "VIDEO_PUBLISH_TIME"; + + private long avid = -1; + private string bvid = "BVID"; + private long cid = -1; + + private long upMid = -1; + private string upName = "UP_NAME"; + + private FileName(List nameParts) + { + this.nameParts = nameParts; + } + + public static FileName Builder(List nameParts) + { + return new FileName(nameParts); + } + + public FileName SetOrder(int order) + { + this.order = order.ToString(); + return this; + } + + public FileName SetOrder(int order, int count) + { + int length = Math.Abs(count).ToString().Length; + this.order = order.ToString("D" + length); + + return this; + } + + public FileName SetSection(string section) + { + this.section = section; + return this; + } + + public FileName SetMainTitle(string mainTitle) + { + this.mainTitle = mainTitle; + return this; + } + + public FileName SetPageTitle(string pageTitle) + { + this.pageTitle = pageTitle; + return this; + } + + public FileName SetVideoZone(string videoZone) + { + this.videoZone = videoZone; + return this; + } + + public FileName SetAudioQuality(string audioQuality) + { + this.audioQuality = audioQuality; + return this; + } + + public FileName SetVideoQuality(string videoQuality) + { + this.videoQuality = videoQuality; + return this; + } + + public FileName SetVideoCodec(string videoCodec) + { + this.videoCodec = videoCodec; + return this; + } + + public FileName SetVideoPublishTime(string videoPublishTime) + { + this.videoPublishTime = videoPublishTime; + return this; + } + + public FileName SetAvid(long avid) + { + this.avid = avid; + return this; + } + + public FileName SetBvid(string bvid) + { + this.bvid = bvid; + return this; + } + + public FileName SetCid(long cid) + { + this.cid = cid; + return this; + } + + public FileName SetUpMid(long upMid) + { + this.upMid = upMid; + return this; + } + + public FileName SetUpName(string upName) + { + this.upName = upName; + return this; + } + + public string RelativePath() + { + string path = string.Empty; + + foreach (FileNamePart part in nameParts) + { + switch (part) + { + case FileNamePart.ORDER: + path += order; + break; + case FileNamePart.SECTION: + path += section; + break; + case FileNamePart.MAIN_TITLE: + path += mainTitle; + break; + case FileNamePart.PAGE_TITLE: + path += pageTitle; + break; + case FileNamePart.VIDEO_ZONE: + path += videoZone; + break; + case FileNamePart.AUDIO_QUALITY: + path += audioQuality; + break; + case FileNamePart.VIDEO_QUALITY: + path += videoQuality; + break; + case FileNamePart.VIDEO_CODEC: + path += videoCodec; + break; + case FileNamePart.VIDEO_PUBLISH_TIME: + path += videoPublishTime; + break; + case FileNamePart.AVID: + path += avid; + break; + case FileNamePart.BVID: + path += bvid; + break; + case FileNamePart.CID: + path += cid; + break; + case FileNamePart.UP_MID: + path += upMid; + break; + case FileNamePart.UP_NAME: + path += upName; + break; + } + + if (((int)part) >= 100) + { + path += HyphenSeparated.Hyphen[(int)part]; + } + } + + // 避免连续多个斜杠 + path = Regex.Replace(path, @"//+", "/"); + // 避免以斜杠开头和结尾的情况 + return path.TrimEnd('/').TrimStart('/'); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/FileName/FileNamePart.cs b/src/Downkyi.Core/FileName/FileNamePart.cs new file mode 100644 index 0000000..50ed2eb --- /dev/null +++ b/src/Downkyi.Core/FileName/FileNamePart.cs @@ -0,0 +1,43 @@ +namespace Downkyi.Core.FileName; + +public enum FileNamePart +{ + // Video + ORDER = 1, + SECTION, + MAIN_TITLE, + PAGE_TITLE, + VIDEO_ZONE, + AUDIO_QUALITY, + VIDEO_QUALITY, + VIDEO_CODEC, + + VIDEO_PUBLISH_TIME, + + AVID, + BVID, + CID, + + UP_MID, + UP_NAME, + + // 斜杠 + SLASH = 100, + + // HyphenSeparated + UNDERSCORE = 101, // 下划线 + HYPHEN, // 连字符 + PLUS, // 加号 + COMMA, // 逗号 + PERIOD, // 句号 + AND, // and + NUMBER, // # + OPEN_PAREN, // 左圆括号 + CLOSE_PAREN, // 右圆括号 + OPEN_BRACKET, // 左方括号 + CLOSE_BRACKET, // 右方括号 + OPEN_BRACE, // 左花括号 + CLOSE_brace, // 右花括号 + BLANK, // 空白符 + +} \ No newline at end of file diff --git a/src/Downkyi.Core/FileName/HyphenSeparated.cs b/src/Downkyi.Core/FileName/HyphenSeparated.cs new file mode 100644 index 0000000..1d6bbb7 --- /dev/null +++ b/src/Downkyi.Core/FileName/HyphenSeparated.cs @@ -0,0 +1,28 @@ +namespace Downkyi.Core.FileName; + +/// +/// 文件名字段 +/// +public static class HyphenSeparated +{ + // 文件名的分隔符 + public static Dictionary Hyphen = new Dictionary() + { + { 100, "/" }, + { 101, "_" }, + { 102, "-" }, + { 103, "+" }, + { 104, "," }, + { 105, "." }, + { 106, "&" }, + { 107, "#" }, + { 108, "(" }, + { 109, ")" }, + { 110, "[" }, + { 111, "]" }, + { 112, "{" }, + { 113, "}" }, + { 114, " " }, + }; + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Logger/Log.cs b/src/Downkyi.Core/Logger/Log.cs new file mode 100644 index 0000000..8595d9f --- /dev/null +++ b/src/Downkyi.Core/Logger/Log.cs @@ -0,0 +1,47 @@ +using NLog; + +namespace Downkyi.Core.Log; + +public static class Log +{ + static Log() + { + var config = new NLog.Config.LoggingConfiguration(); + + // Targets where to log to Debugger + var logConsole = new NLog.Targets.ConsoleTarget("logConsole") + { + Layout = "${date:format=HH\\:mm\\:ss} [${level:padding=-5}] ${message}" + }; + + // Targets where to log to Debugger + var logDebugger = new NLog.Targets.DebuggerTarget("logDebugger") + { + Layout = "${date:format=HH\\:mm\\:ss} [${level:padding=-5}] ${message}" + }; + + // Targets where to log to File + var logFile = new NLog.Targets.FileTarget("logFile") + { + FileName = "${baseDir}/Logs/${shortdate}/${level}.log", + CreateDirs = true, + KeepFileOpen = true, + ArchiveAboveSize = 1024 * 1024, + ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Sequence, + MaxArchiveDays = 30, + Layout = "${longdate} [${level:uppercase=false:padding=-5}] ${message} ${onexception:${exception:format=tostring} ${newline} ${stacktrace} ${newline}" + }; + + // Rules for mapping loggers to targets + config.AddRule(LogLevel.Trace, LogLevel.Fatal, logConsole); + config.AddRule(LogLevel.Trace, LogLevel.Fatal, logDebugger); + config.AddRule(LogLevel.Debug, LogLevel.Fatal, logFile); + + // Apply config + LogManager.Configuration = config; + + LogManager.ThrowExceptions = false; + } + + public static readonly Logger Logger = LogManager.GetCurrentClassLogger(); +} diff --git a/src/Downkyi.Core/Settings/Enum/AfterDownloadOperation.cs b/src/Downkyi.Core/Settings/Enum/AfterDownloadOperation.cs new file mode 100644 index 0000000..745e3e5 --- /dev/null +++ b/src/Downkyi.Core/Settings/Enum/AfterDownloadOperation.cs @@ -0,0 +1,10 @@ +namespace Downkyi.Core.Settings.Enum; + +public enum AfterDownloadOperation +{ + NOT_SET = 0, + NONE = 1, + OPEN_FOLDER, + CLOSE_APP, + CLOSE_SYSTEM +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Enum/AllowStatus.cs b/src/Downkyi.Core/Settings/Enum/AllowStatus.cs new file mode 100644 index 0000000..d901f63 --- /dev/null +++ b/src/Downkyi.Core/Settings/Enum/AllowStatus.cs @@ -0,0 +1,8 @@ +namespace Downkyi.Core.Settings.Enum; + +public enum AllowStatus +{ + NONE = 0, + NO, + YES +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Enum/DanmakuLayoutAlgorithm.cs b/src/Downkyi.Core/Settings/Enum/DanmakuLayoutAlgorithm.cs new file mode 100644 index 0000000..749f7e9 --- /dev/null +++ b/src/Downkyi.Core/Settings/Enum/DanmakuLayoutAlgorithm.cs @@ -0,0 +1,8 @@ +namespace Downkyi.Core.Settings.Enum; + +public enum DanmakuLayoutAlgorithm +{ + NONE = 0, + ASYNC, + SYNC +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Enum/DownloadFinishedSort.cs b/src/Downkyi.Core/Settings/Enum/DownloadFinishedSort.cs new file mode 100644 index 0000000..611da79 --- /dev/null +++ b/src/Downkyi.Core/Settings/Enum/DownloadFinishedSort.cs @@ -0,0 +1,8 @@ +namespace Downkyi.Core.Settings.Enum; + +public enum DownloadFinishedSort +{ + NOT_SET = 0, + DOWNLOAD, + NUMBER +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Enum/Downloader.cs b/src/Downkyi.Core/Settings/Enum/Downloader.cs new file mode 100644 index 0000000..7bed2a6 --- /dev/null +++ b/src/Downkyi.Core/Settings/Enum/Downloader.cs @@ -0,0 +1,9 @@ +namespace Downkyi.Core.Settings.Enum; + +public enum Downloader +{ + NOT_SET = 0, + BUILT_IN, + ARIA, + CUSTOM_ARIA, +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Enum/OrderFormat.cs b/src/Downkyi.Core/Settings/Enum/OrderFormat.cs new file mode 100644 index 0000000..d56d260 --- /dev/null +++ b/src/Downkyi.Core/Settings/Enum/OrderFormat.cs @@ -0,0 +1,8 @@ +namespace Downkyi.Core.Settings.Enum; + +public enum OrderFormat +{ + NOT_SET = 0, + NATURAL, // 自然数 + LEADING_ZEROS, // 前导零填充 +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Enum/ParseScope.cs b/src/Downkyi.Core/Settings/Enum/ParseScope.cs new file mode 100644 index 0000000..ac9485d --- /dev/null +++ b/src/Downkyi.Core/Settings/Enum/ParseScope.cs @@ -0,0 +1,10 @@ +namespace Downkyi.Core.Settings.Enum; + +public enum ParseScope +{ + NOT_SET = 0, + NONE = 1, + SELECTED_ITEM, + CURRENT_SECTION, + ALL +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Enum/VideoCodecs.cs b/src/Downkyi.Core/Settings/Enum/VideoCodecs.cs new file mode 100644 index 0000000..3fd07ff --- /dev/null +++ b/src/Downkyi.Core/Settings/Enum/VideoCodecs.cs @@ -0,0 +1,9 @@ +namespace Downkyi.Core.Settings.Enum +{ + public enum VideoCodecs + { + NONE = 0, + AVC, + HEVC + } +} diff --git a/src/Downkyi.Core/Settings/Models/AboutSettings.cs b/src/Downkyi.Core/Settings/Models/AboutSettings.cs new file mode 100644 index 0000000..d930935 --- /dev/null +++ b/src/Downkyi.Core/Settings/Models/AboutSettings.cs @@ -0,0 +1,12 @@ +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings.Models; + +/// +/// 关于 +/// +public class AboutSettings +{ + public AllowStatus IsReceiveBetaVersion { get; set; } = AllowStatus.NONE; + public AllowStatus AutoUpdateWhenLaunch { get; set; } = AllowStatus.NONE; +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Models/AppSettings.cs b/src/Downkyi.Core/Settings/Models/AppSettings.cs new file mode 100644 index 0000000..9ef0182 --- /dev/null +++ b/src/Downkyi.Core/Settings/Models/AppSettings.cs @@ -0,0 +1,11 @@ +namespace Downkyi.Core.Settings.Models; + +public class AppSettings +{ + public BasicSettings Basic { get; set; } = new BasicSettings(); + public NetworkSettings Network { get; set; } = new NetworkSettings(); + public VideoSettings Video { get; set; } = new VideoSettings(); + public DanmakuSettings Danmaku { get; set; } = new DanmakuSettings(); + public AboutSettings About { get; set; } = new AboutSettings(); + public UserInfoSettings UserInfo { get; set; } = new UserInfoSettings(); +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Models/BasicSettings.cs b/src/Downkyi.Core/Settings/Models/BasicSettings.cs new file mode 100644 index 0000000..54ac206 --- /dev/null +++ b/src/Downkyi.Core/Settings/Models/BasicSettings.cs @@ -0,0 +1,16 @@ +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings.Models; + +/// +/// 基本 +/// +public class BasicSettings +{ + public AfterDownloadOperation AfterDownload { get; set; } = AfterDownloadOperation.NOT_SET; + public AllowStatus IsListenClipboard { get; set; } = AllowStatus.NONE; + public AllowStatus IsAutoParseVideo { get; set; } = AllowStatus.NONE; + public ParseScope ParseScope { get; set; } = ParseScope.NOT_SET; + public AllowStatus IsAutoDownloadAll { get; set; } = AllowStatus.NONE; + public DownloadFinishedSort DownloadFinishedSort { get; set; } = DownloadFinishedSort.NOT_SET; +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Models/DanmakuSettings.cs b/src/Downkyi.Core/Settings/Models/DanmakuSettings.cs new file mode 100644 index 0000000..841aa6c --- /dev/null +++ b/src/Downkyi.Core/Settings/Models/DanmakuSettings.cs @@ -0,0 +1,20 @@ +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings.Models; + +/// +/// 弹幕 +/// +public class DanmakuSettings +{ + public AllowStatus DanmakuTopFilter { get; set; } = AllowStatus.NONE; + public AllowStatus DanmakuBottomFilter { get; set; } = AllowStatus.NONE; + public AllowStatus DanmakuScrollFilter { get; set; } = AllowStatus.NONE; + public AllowStatus IsCustomDanmakuResolution { get; set; } = AllowStatus.NONE; + public int DanmakuScreenWidth { get; set; } = -1; + public int DanmakuScreenHeight { get; set; } = -1; + public string DanmakuFontName { get; set; } = null; + public int DanmakuFontSize { get; set; } = -1; + public int DanmakuLineCount { get; set; } = -1; + public DanmakuLayoutAlgorithm DanmakuLayoutAlgorithm { get; set; } = DanmakuLayoutAlgorithm.NONE; +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Models/NetworkSettings.cs b/src/Downkyi.Core/Settings/Models/NetworkSettings.cs new file mode 100644 index 0000000..239926a --- /dev/null +++ b/src/Downkyi.Core/Settings/Models/NetworkSettings.cs @@ -0,0 +1,40 @@ +using Aria2cNet.Server; +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings.Models; + +/// +/// 网络 +/// +public class NetworkSettings +{ + public AllowStatus IsLiftingOfRegion { get; set; } = AllowStatus.NONE; + + public AllowStatus UseSSL { get; set; } = AllowStatus.NONE; + public string UserAgent { get; set; } = string.Empty; + + public Enum.Downloader Downloader { get; set; } = Enum.Downloader.NOT_SET; + public int MaxCurrentDownloads { get; set; } = -1; + + #region built-in + public int Split { get; set; } = -1; + public AllowStatus IsHttpProxy { get; set; } = AllowStatus.NONE; + public string HttpProxy { get; set; } = null; + public int HttpProxyListenPort { get; set; } = -1; + #endregion + + #region Aria + public string AriaToken { get; set; } = null; + public string AriaHost { get; set; } = null; + public int AriaListenPort { get; set; } = -1; + public AriaConfigLogLevel AriaLogLevel { get; set; } = AriaConfigLogLevel.NOT_SET; + public int AriaSplit { get; set; } = -1; + public int AriaMaxOverallDownloadLimit { get; set; } = -1; + public int AriaMaxDownloadLimit { get; set; } = -1; + public AriaConfigFileAllocation AriaFileAllocation { get; set; } = AriaConfigFileAllocation.NOT_SET; + + public AllowStatus IsAriaHttpProxy { get; set; } = AllowStatus.NONE; + public string AriaHttpProxy { get; set; } = null; + public int AriaHttpProxyListenPort { get; set; } = -1; + #endregion +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Models/UserInfoSettings.cs b/src/Downkyi.Core/Settings/Models/UserInfoSettings.cs new file mode 100644 index 0000000..c00bfce --- /dev/null +++ b/src/Downkyi.Core/Settings/Models/UserInfoSettings.cs @@ -0,0 +1,9 @@ +namespace Downkyi.Core.Settings.Models; + +public class UserInfoSettings +{ + public long Mid { get; set; } + public string Name { get; set; } + public bool IsLogin { get; set; } // 是否登录 + public bool IsVip { get; set; } // 是否为大会员,未登录时为false +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Models/VideoContentSettings.cs b/src/Downkyi.Core/Settings/Models/VideoContentSettings.cs new file mode 100644 index 0000000..71c0484 --- /dev/null +++ b/src/Downkyi.Core/Settings/Models/VideoContentSettings.cs @@ -0,0 +1,10 @@ +namespace Downkyi.Core.Settings.Models; + +public class VideoContentSettings +{ + public bool DownloadAudio { get; set; } = true; + public bool DownloadVideo { get; set; } = true; + public bool DownloadDanmaku { get; set; } = true; + public bool DownloadSubtitle { get; set; } = true; + public bool DownloadCover { get; set; } = true; +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/Models/VideoSettings.cs b/src/Downkyi.Core/Settings/Models/VideoSettings.cs new file mode 100644 index 0000000..984981d --- /dev/null +++ b/src/Downkyi.Core/Settings/Models/VideoSettings.cs @@ -0,0 +1,22 @@ +using Downkyi.Core.FileName; +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings.Models; + +/// +/// 视频 +/// +public class VideoSettings +{ + public int VideoCodecs { get; set; } = -1; // AVC or HEVC + public int Quality { get; set; } = -1; // 画质 + public int AudioQuality { get; set; } = -1; // 音质 + public AllowStatus IsTranscodingFlvToMp4 { get; set; } = AllowStatus.NONE; // 是否将flv转为mp4 + public string SaveVideoRootPath { get; set; } = null; // 视频保存路径 + public List HistoryVideoRootPaths { get; set; } = null; // 历史视频保存路径 + public AllowStatus IsUseSaveVideoRootPath { get; set; } = AllowStatus.NONE; // 是否使用默认视频保存路径 + public VideoContentSettings VideoContent { get; set; } = null; // 下载内容 + public List FileNameParts { get; set; } = null; // 文件命名格式 + public string FileNamePartTimeFormat { get; set; } = null; // 文件命名中的时间格式 + public OrderFormat OrderFormat { get; set; } = OrderFormat.NOT_SET; // 文件命名中的序号格式 +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/SettingsManager.About.cs b/src/Downkyi.Core/Settings/SettingsManager.About.cs new file mode 100644 index 0000000..43da6ff --- /dev/null +++ b/src/Downkyi.Core/Settings/SettingsManager.About.cs @@ -0,0 +1,67 @@ +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings; + +public partial class SettingsManager +{ + // 是否接收测试版更新 + private readonly AllowStatus isReceiveBetaVersion = AllowStatus.NO; + + // 是否在启动时自动检查更新 + private readonly AllowStatus autoUpdateWhenLaunch = AllowStatus.YES; + + /// + /// 获取是否接收测试版更新 + /// + /// + public AllowStatus IsReceiveBetaVersion() + { + appSettings = GetSettings(); + if (appSettings.About.IsReceiveBetaVersion == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsReceiveBetaVersion(isReceiveBetaVersion); + return isReceiveBetaVersion; + } + return appSettings.About.IsReceiveBetaVersion; + } + + /// + /// 设置是否接收测试版更新 + /// + /// + /// + public bool IsReceiveBetaVersion(AllowStatus isReceiveBetaVersion) + { + appSettings.About.IsReceiveBetaVersion = isReceiveBetaVersion; + return SetSettings(); + } + + /// + /// 获取是否允许启动时检查更新 + /// + /// + public AllowStatus GetAutoUpdateWhenLaunch() + { + appSettings = GetSettings(); + if (appSettings.About.AutoUpdateWhenLaunch == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + SetAutoUpdateWhenLaunch(autoUpdateWhenLaunch); + return autoUpdateWhenLaunch; + } + return appSettings.About.AutoUpdateWhenLaunch; + } + + /// + /// 设置是否允许启动时检查更新 + /// + /// + /// + public bool SetAutoUpdateWhenLaunch(AllowStatus autoUpdateWhenLaunch) + { + appSettings.About.AutoUpdateWhenLaunch = autoUpdateWhenLaunch; + return SetSettings(); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/SettingsManager.Basic.cs b/src/Downkyi.Core/Settings/SettingsManager.Basic.cs new file mode 100644 index 0000000..40ca8fc --- /dev/null +++ b/src/Downkyi.Core/Settings/SettingsManager.Basic.cs @@ -0,0 +1,187 @@ +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings; + +public partial class SettingsManager +{ + // 默认下载完成后的操作 + private readonly AfterDownloadOperation afterDownload = AfterDownloadOperation.NONE; + + // 是否监听剪贴板 + private readonly AllowStatus isListenClipboard = AllowStatus.YES; + + // 视频详情页面是否自动解析 + private readonly AllowStatus isAutoParseVideo = AllowStatus.NO; + + // 默认的视频解析项 + private readonly ParseScope parseScope = ParseScope.NONE; + + // 解析后自动下载解析视频 + private readonly AllowStatus isAutoDownloadAll = AllowStatus.NO; + + // 下载完成列表排序 + private readonly DownloadFinishedSort finishedSort = DownloadFinishedSort.DOWNLOAD; + + /// + /// 获取下载完成后的操作 + /// + /// + public AfterDownloadOperation GetAfterDownloadOperation() + { + appSettings = GetSettings(); + if (appSettings.Basic.AfterDownload == AfterDownloadOperation.NOT_SET) + { + // 第一次获取,先设置默认值 + SetAfterDownloadOperation(afterDownload); + return afterDownload; + } + return appSettings.Basic.AfterDownload; + } + + /// + /// 设置下载完成后的操作 + /// + /// + /// + public bool SetAfterDownloadOperation(AfterDownloadOperation afterDownload) + { + appSettings.Basic.AfterDownload = afterDownload; + return SetSettings(); + } + + /// + /// 是否监听剪贴板 + /// + /// + public AllowStatus IsListenClipboard() + { + appSettings = GetSettings(); + if (appSettings.Basic.IsListenClipboard == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsListenClipboard(isListenClipboard); + return isListenClipboard; + } + return appSettings.Basic.IsListenClipboard; + } + + /// + /// 是否监听剪贴板 + /// + /// + /// + public bool IsListenClipboard(AllowStatus isListen) + { + appSettings.Basic.IsListenClipboard = isListen; + return SetSettings(); + } + + /// + /// 视频详情页面是否自动解析 + /// + /// + public AllowStatus IsAutoParseVideo() + { + appSettings = GetSettings(); + if (appSettings.Basic.IsAutoParseVideo == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsAutoParseVideo(isAutoParseVideo); + return isAutoParseVideo; + } + return appSettings.Basic.IsAutoParseVideo; + } + + /// + /// 视频详情页面是否自动解析 + /// + /// + /// + public bool IsAutoParseVideo(AllowStatus IsAuto) + { + appSettings.Basic.IsAutoParseVideo = IsAuto; + return SetSettings(); + } + + /// + /// 获取视频解析项 + /// + /// + public ParseScope GetParseScope() + { + appSettings = GetSettings(); + if (appSettings.Basic.ParseScope == ParseScope.NOT_SET) + { + // 第一次获取,先设置默认值 + SetParseScope(parseScope); + return parseScope; + } + return appSettings.Basic.ParseScope; + } + + /// + /// 设置视频解析项 + /// + /// + /// + public bool SetParseScope(ParseScope parseScope) + { + appSettings.Basic.ParseScope = parseScope; + return SetSettings(); + } + + /// + /// 解析后是否自动下载解析视频 + /// + /// + public AllowStatus IsAutoDownloadAll() + { + appSettings = GetSettings(); + if (appSettings.Basic.IsAutoDownloadAll == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsAutoDownloadAll(isAutoDownloadAll); + return isAutoDownloadAll; + } + return appSettings.Basic.IsAutoDownloadAll; + } + + /// + /// 解析后是否自动下载解析视频 + /// + /// + /// + public bool IsAutoDownloadAll(AllowStatus isAutoDownloadAll) + { + appSettings.Basic.IsAutoDownloadAll = isAutoDownloadAll; + return SetSettings(); + } + + /// + /// 获取下载完成列表排序 + /// + /// + public DownloadFinishedSort GetDownloadFinishedSort() + { + appSettings = GetSettings(); + if (appSettings.Basic.DownloadFinishedSort == DownloadFinishedSort.NOT_SET) + { + // 第一次获取,先设置默认值 + SetDownloadFinishedSort(finishedSort); + return finishedSort; + } + return appSettings.Basic.DownloadFinishedSort; + } + + /// + /// 设置下载完成列表排序 + /// + /// + /// + public bool SetDownloadFinishedSort(DownloadFinishedSort finishedSort) + { + appSettings.Basic.DownloadFinishedSort = finishedSort; + return SetSettings(); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/SettingsManager.Danmaku.cs b/src/Downkyi.Core/Settings/SettingsManager.Danmaku.cs new file mode 100644 index 0000000..c2db901 --- /dev/null +++ b/src/Downkyi.Core/Settings/SettingsManager.Danmaku.cs @@ -0,0 +1,308 @@ +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings; + +public partial class SettingsManager +{ + // 是否屏蔽顶部弹幕 + private readonly AllowStatus danmakuTopFilter = AllowStatus.NO; + + // 是否屏蔽底部弹幕 + private readonly AllowStatus danmakuBottomFilter = AllowStatus.NO; + + // 是否屏蔽滚动弹幕 + private readonly AllowStatus danmakuScrollFilter = AllowStatus.NO; + + // 是否自定义分辨率 + private readonly AllowStatus isCustomDanmakuResolution = AllowStatus.NO; + + // 分辨率-宽 + private readonly int danmakuScreenWidth = 1920; + + // 分辨率-高 + private readonly int danmakuScreenHeight = 1080; + + // 弹幕字体 + private readonly string danmakuFontName = "黑体"; + + // 弹幕字体大小 + private readonly int danmakuFontSize = 50; + + // 弹幕限制行数 + private readonly int danmakuLineCount = 0; + + // 弹幕布局算法 + private readonly DanmakuLayoutAlgorithm danmakuLayoutAlgorithm = DanmakuLayoutAlgorithm.SYNC; + + + /// + /// 获取是否屏蔽顶部弹幕 + /// + /// + public AllowStatus GetDanmakuTopFilter() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuTopFilter == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + SetDanmakuTopFilter(danmakuTopFilter); + return danmakuTopFilter; + } + return appSettings.Danmaku.DanmakuTopFilter; + } + + /// + /// 设置是否屏蔽顶部弹幕 + /// + /// + /// + public bool SetDanmakuTopFilter(AllowStatus danmakuFilter) + { + appSettings.Danmaku.DanmakuTopFilter = danmakuFilter; + return SetSettings(); + } + + /// + /// 获取是否屏蔽底部弹幕 + /// + /// + public AllowStatus GetDanmakuBottomFilter() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuBottomFilter == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + SetDanmakuBottomFilter(danmakuBottomFilter); + return danmakuBottomFilter; + } + return appSettings.Danmaku.DanmakuBottomFilter; + } + + /// + /// 设置是否屏蔽底部弹幕 + /// + /// + /// + public bool SetDanmakuBottomFilter(AllowStatus danmakuFilter) + { + appSettings.Danmaku.DanmakuBottomFilter = danmakuFilter; + return SetSettings(); + } + + /// + /// 获取是否屏蔽滚动弹幕 + /// + /// + public AllowStatus GetDanmakuScrollFilter() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuScrollFilter == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + SetDanmakuScrollFilter(danmakuScrollFilter); + return danmakuScrollFilter; + } + return appSettings.Danmaku.DanmakuScrollFilter; + } + + /// + /// 设置是否屏蔽滚动弹幕 + /// + /// + /// + public bool SetDanmakuScrollFilter(AllowStatus danmakuFilter) + { + appSettings.Danmaku.DanmakuScrollFilter = danmakuFilter; + return SetSettings(); + } + + /// + /// 获取是否自定义分辨率 + /// + /// + public AllowStatus IsCustomDanmakuResolution() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.IsCustomDanmakuResolution == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsCustomDanmakuResolution(isCustomDanmakuResolution); + return isCustomDanmakuResolution; + } + return appSettings.Danmaku.IsCustomDanmakuResolution; + } + + /// + /// 设置是否自定义分辨率 + /// + /// + /// + public bool IsCustomDanmakuResolution(AllowStatus isCustomResolution) + { + appSettings.Danmaku.IsCustomDanmakuResolution = isCustomResolution; + return SetSettings(); + } + + /// + /// 获取分辨率-宽 + /// + /// + public int GetDanmakuScreenWidth() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuScreenWidth == -1) + { + // 第一次获取,先设置默认值 + SetDanmakuScreenWidth(danmakuScreenWidth); + return danmakuScreenWidth; + } + return appSettings.Danmaku.DanmakuScreenWidth; + } + + /// + /// 设置分辨率-宽 + /// + /// + /// + public bool SetDanmakuScreenWidth(int screenWidth) + { + appSettings.Danmaku.DanmakuScreenWidth = screenWidth; + return SetSettings(); + } + + /// + /// 获取分辨率-高 + /// + /// + public int GetDanmakuScreenHeight() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuScreenHeight == -1) + { + // 第一次获取,先设置默认值 + SetDanmakuScreenHeight(danmakuScreenHeight); + return danmakuScreenHeight; + } + return appSettings.Danmaku.DanmakuScreenHeight; + } + + /// + /// 设置分辨率-高 + /// + /// + /// + public bool SetDanmakuScreenHeight(int screenHeight) + { + appSettings.Danmaku.DanmakuScreenHeight = screenHeight; + return SetSettings(); + } + + /// + /// 获取弹幕字体 + /// + /// + public string GetDanmakuFontName() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuFontName == null) + { + // 第一次获取,先设置默认值 + SetDanmakuFontName(danmakuFontName); + return danmakuFontName; + } + return appSettings.Danmaku.DanmakuFontName; + } + + /// + /// 设置弹幕字体 + /// + /// + /// + public bool SetDanmakuFontName(string danmakuFontName) + { + appSettings.Danmaku.DanmakuFontName = danmakuFontName; + return SetSettings(); + } + + /// + /// 获取弹幕字体大小 + /// + /// + public int GetDanmakuFontSize() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuFontSize == -1) + { + // 第一次获取,先设置默认值 + SetDanmakuFontSize(danmakuFontSize); + return danmakuFontSize; + } + return appSettings.Danmaku.DanmakuFontSize; + } + + /// + /// 设置弹幕字体大小 + /// + /// + /// + public bool SetDanmakuFontSize(int danmakuFontSize) + { + appSettings.Danmaku.DanmakuFontSize = danmakuFontSize; + return SetSettings(); + } + + /// + /// 获取弹幕限制行数 + /// + /// + public int GetDanmakuLineCount() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuLineCount == -1) + { + // 第一次获取,先设置默认值 + SetDanmakuLineCount(danmakuLineCount); + return danmakuLineCount; + } + return appSettings.Danmaku.DanmakuLineCount; + } + + /// + /// 设置弹幕限制行数 + /// + /// + /// + public bool SetDanmakuLineCount(int danmakuLineCount) + { + appSettings.Danmaku.DanmakuLineCount = danmakuLineCount; + return SetSettings(); + } + + /// + /// 获取弹幕布局算法 + /// + /// + public DanmakuLayoutAlgorithm GetDanmakuLayoutAlgorithm() + { + appSettings = GetSettings(); + if (appSettings.Danmaku.DanmakuLayoutAlgorithm == DanmakuLayoutAlgorithm.NONE) + { + // 第一次获取,先设置默认值 + SetDanmakuLayoutAlgorithm(danmakuLayoutAlgorithm); + return danmakuLayoutAlgorithm; + } + return appSettings.Danmaku.DanmakuLayoutAlgorithm; + } + + /// + /// 设置弹幕布局算法 + /// + /// + /// + public bool SetDanmakuLayoutAlgorithm(DanmakuLayoutAlgorithm danmakuLayoutAlgorithm) + { + appSettings.Danmaku.DanmakuLayoutAlgorithm = danmakuLayoutAlgorithm; + return SetSettings(); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/SettingsManager.Network.cs b/src/Downkyi.Core/Settings/SettingsManager.Network.cs new file mode 100644 index 0000000..186eb8f --- /dev/null +++ b/src/Downkyi.Core/Settings/SettingsManager.Network.cs @@ -0,0 +1,600 @@ +using Aria2cNet.Server; +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.Core.Settings; + +public partial class SettingsManager +{ + // 是否开启解除地区限制 + private readonly AllowStatus isLiftingOfRegion = AllowStatus.YES; + + // 启用https + private readonly AllowStatus useSSL = AllowStatus.YES; + + // UserAgent + private readonly string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"; + + // 下载器 + private readonly Enum.Downloader downloader = Enum.Downloader.BUILT_IN; + + // 最大同时下载数(任务数) + private readonly int maxCurrentDownloads = 3; + + // 单文件最大线程数 + private readonly int split = 8; + + // HttpProxy代理 + private readonly AllowStatus isHttpProxy = AllowStatus.NO; + private readonly string httpProxy = string.Empty; + private readonly int httpProxyListenPort = 0; + + // Aria服务器token + private readonly string ariaToken = "downkyi"; + + // Aria服务器host + private readonly string ariaHost = "http://localhost"; + + // Aria服务器端口号 + private readonly int ariaListenPort = 6800; + + // Aria日志等级 + private readonly AriaConfigLogLevel ariaLogLevel = AriaConfigLogLevel.INFO; + + // Aria单文件最大线程数 + private readonly int ariaSplit = 5; + + // Aria下载速度限制 + private readonly int ariaMaxOverallDownloadLimit = 0; + + // Aria下载单文件速度限制 + private readonly int ariaMaxDownloadLimit = 0; + + // Aria文件预分配 + private readonly AriaConfigFileAllocation ariaFileAllocation = AriaConfigFileAllocation.NONE; + + // Aria HttpProxy代理 + private readonly AllowStatus isAriaHttpProxy = AllowStatus.NO; + private readonly string ariaHttpProxy = string.Empty; + private readonly int ariaHttpProxyListenPort = 0; + + /// + /// 获取是否解除地区限制 + /// + /// + public AllowStatus IsLiftingOfRegion() + { + appSettings = GetSettings(); + if (appSettings.Network.IsLiftingOfRegion == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsLiftingOfRegion(isLiftingOfRegion); + return isLiftingOfRegion; + } + return appSettings.Network.IsLiftingOfRegion; + } + + /// + /// 设置是否解除地区限制 + /// + /// + /// + public bool IsLiftingOfRegion(AllowStatus isLiftingOfRegion) + { + appSettings.Network.IsLiftingOfRegion = isLiftingOfRegion; + return SetSettings(); + } + + /// + /// 获取是否启用https + /// + /// + public AllowStatus UseSSL() + { + appSettings = GetSettings(); + if (appSettings.Network.UseSSL == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + UseSSL(useSSL); + return useSSL; + } + return appSettings.Network.UseSSL; + } + + /// + /// 设置是否启用https + /// + /// + /// + public bool UseSSL(AllowStatus useSSL) + { + appSettings.Network.UseSSL = useSSL; + return SetSettings(); + } + + /// + /// 获取UserAgent + /// + /// + public string GetUserAgent() + { + appSettings = GetSettings(); + if (appSettings.Network.UserAgent == string.Empty) + { + // 第一次获取,先设置默认值 + SetUserAgent(userAgent); + return userAgent; + } + return appSettings.Network.UserAgent; + } + + /// + /// 设置UserAgent + /// + /// + /// + public bool SetUserAgent(string userAgent) + { + appSettings.Network.UserAgent = userAgent; + return SetSettings(); + } + + /// + /// 获取下载器 + /// + /// + public Enum.Downloader GetDownloader() + { + appSettings = GetSettings(); + if (appSettings.Network.Downloader == Enum.Downloader.NOT_SET) + { + // 第一次获取,先设置默认值 + SetDownloader(downloader); + return downloader; + } + return appSettings.Network.Downloader; + } + + /// + /// 设置下载器 + /// + /// + /// + public bool SetDownloader(Enum.Downloader downloader) + { + appSettings.Network.Downloader = downloader; + return SetSettings(); + } + + /// + /// 获取最大同时下载数(任务数) + /// + /// + public int GetMaxCurrentDownloads() + { + appSettings = GetSettings(); + if (appSettings.Network.MaxCurrentDownloads == -1) + { + // 第一次获取,先设置默认值 + SetMaxCurrentDownloads(maxCurrentDownloads); + return maxCurrentDownloads; + } + return appSettings.Network.MaxCurrentDownloads; + } + + /// + /// 设置最大同时下载数(任务数) + /// + /// + /// + public bool SetMaxCurrentDownloads(int maxCurrentDownloads) + { + appSettings.Network.MaxCurrentDownloads = maxCurrentDownloads; + return SetSettings(); + } + + /// + /// 获取单文件最大线程数 + /// + /// + public int GetSplit() + { + appSettings = GetSettings(); + if (appSettings.Network.Split == -1) + { + // 第一次获取,先设置默认值 + SetSplit(split); + return split; + } + return appSettings.Network.Split; + } + + /// + /// 设置单文件最大线程数 + /// + /// + /// + public bool SetSplit(int split) + { + appSettings.Network.Split = split; + return SetSettings(); + } + + /// + /// 获取是否开启Http代理 + /// + /// + public AllowStatus IsHttpProxy() + { + appSettings = GetSettings(); + if (appSettings.Network.IsHttpProxy == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsHttpProxy(isHttpProxy); + return isHttpProxy; + } + return appSettings.Network.IsHttpProxy; + } + + /// + /// 设置是否开启Http代理 + /// + /// + /// + public bool IsHttpProxy(AllowStatus isHttpProxy) + { + appSettings.Network.IsHttpProxy = isHttpProxy; + return SetSettings(); + } + + /// + /// 获取Http代理的地址 + /// + /// + public string GetHttpProxy() + { + appSettings = GetSettings(); + if (appSettings.Network.HttpProxy == null) + { + // 第一次获取,先设置默认值 + SetHttpProxy(httpProxy); + return httpProxy; + } + return appSettings.Network.HttpProxy; + } + + /// + /// 设置Aria的http代理的地址 + /// + /// + /// + public bool SetHttpProxy(string httpProxy) + { + appSettings.Network.HttpProxy = httpProxy; + return SetSettings(); + } + + /// + /// 获取Http代理的端口 + /// + /// + public int GetHttpProxyListenPort() + { + appSettings = GetSettings(); + if (appSettings.Network.HttpProxyListenPort == -1) + { + // 第一次获取,先设置默认值 + SetHttpProxyListenPort(httpProxyListenPort); + return httpProxyListenPort; + } + return appSettings.Network.HttpProxyListenPort; + } + + /// + /// 设置Http代理的端口 + /// + /// + /// + public bool SetHttpProxyListenPort(int httpProxyListenPort) + { + appSettings.Network.HttpProxyListenPort = httpProxyListenPort; + return SetSettings(); + } + + /// + /// 获取Aria服务器的token + /// + /// + public string GetAriaToken() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaToken == null) + { + // 第一次获取,先设置默认值 + SetHttpProxy(ariaToken); + return ariaToken; + } + return appSettings.Network.AriaToken; + } + + /// + /// 设置Aria服务器的token + /// + /// + /// + public bool SetAriaToken(string token) + { + appSettings.Network.AriaToken = token; + return SetSettings(); + } + + /// + /// 获取Aria服务器的host + /// + /// + public string GetAriaHost() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaHost == null) + { + // 第一次获取,先设置默认值 + SetHttpProxy(ariaHost); + return ariaHost; + } + return appSettings.Network.AriaHost; + } + + /// + /// 设置Aria服务器的host + /// + /// + /// + public bool SetAriaHost(string host) + { + appSettings.Network.AriaHost = host; + return SetSettings(); + } + + /// + /// 获取Aria服务器的端口号 + /// + /// + public int GetAriaListenPort() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaListenPort == -1) + { + // 第一次获取,先设置默认值 + SetAriaListenPort(ariaListenPort); + return ariaListenPort; + } + return appSettings.Network.AriaListenPort; + } + + /// + /// 设置Aria服务器的端口号 + /// + /// + /// + public bool SetAriaListenPort(int ariaListenPort) + { + appSettings.Network.AriaListenPort = ariaListenPort; + return SetSettings(); + } + + /// + /// 获取Aria日志等级 + /// + /// + public AriaConfigLogLevel GetAriaLogLevel() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaLogLevel == AriaConfigLogLevel.NOT_SET) + { + // 第一次获取,先设置默认值 + SetAriaLogLevel(ariaLogLevel); + return ariaLogLevel; + } + return appSettings.Network.AriaLogLevel; + } + + /// + /// 设置Aria日志等级 + /// + /// + /// + public bool SetAriaLogLevel(AriaConfigLogLevel ariaLogLevel) + { + appSettings.Network.AriaLogLevel = ariaLogLevel; + return SetSettings(); + } + + /// + /// 获取Aria单文件最大线程数 + /// + /// + public int GetAriaSplit() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaSplit == -1) + { + // 第一次获取,先设置默认值 + SetAriaSplit(ariaSplit); + return ariaSplit; + } + return appSettings.Network.AriaSplit; + } + + /// + /// 设置Aria单文件最大线程数 + /// + /// + /// + public bool SetAriaSplit(int ariaSplit) + { + appSettings.Network.AriaSplit = ariaSplit; + return SetSettings(); + } + + /// + /// 获取Aria下载速度限制 + /// + /// + public int GetAriaMaxOverallDownloadLimit() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaMaxOverallDownloadLimit == -1) + { + // 第一次获取,先设置默认值 + SetAriaMaxOverallDownloadLimit(ariaMaxOverallDownloadLimit); + return ariaMaxOverallDownloadLimit; + } + return appSettings.Network.AriaMaxOverallDownloadLimit; + } + + /// + /// 设置Aria下载速度限制 + /// + /// + /// + public bool SetAriaMaxOverallDownloadLimit(int ariaMaxOverallDownloadLimit) + { + appSettings.Network.AriaMaxOverallDownloadLimit = ariaMaxOverallDownloadLimit; + return SetSettings(); + } + + /// + /// 获取Aria下载单文件速度限制 + /// + /// + public int GetAriaMaxDownloadLimit() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaMaxDownloadLimit == -1) + { + // 第一次获取,先设置默认值 + SetAriaMaxDownloadLimit(ariaMaxDownloadLimit); + return ariaMaxDownloadLimit; + } + return appSettings.Network.AriaMaxDownloadLimit; + } + + /// + /// 设置Aria下载单文件速度限制 + /// + /// + /// + public bool SetAriaMaxDownloadLimit(int ariaMaxDownloadLimit) + { + appSettings.Network.AriaMaxDownloadLimit = ariaMaxDownloadLimit; + return SetSettings(); + } + + /// + /// 获取Aria文件预分配 + /// + /// + public AriaConfigFileAllocation GetAriaFileAllocation() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaFileAllocation == AriaConfigFileAllocation.NOT_SET) + { + // 第一次获取,先设置默认值 + SetAriaFileAllocation(ariaFileAllocation); + return ariaFileAllocation; + } + return appSettings.Network.AriaFileAllocation; + } + + /// + /// 设置Aria文件预分配 + /// + /// + /// + public bool SetAriaFileAllocation(AriaConfigFileAllocation ariaFileAllocation) + { + appSettings.Network.AriaFileAllocation = ariaFileAllocation; + return SetSettings(); + } + + /// + /// 获取是否开启Aria http代理 + /// + /// + public AllowStatus IsAriaHttpProxy() + { + appSettings = GetSettings(); + if (appSettings.Network.IsAriaHttpProxy == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsAriaHttpProxy(isAriaHttpProxy); + return isAriaHttpProxy; + } + return appSettings.Network.IsAriaHttpProxy; + } + + /// + /// 设置是否开启Aria http代理 + /// + /// + /// + public bool IsAriaHttpProxy(AllowStatus isAriaHttpProxy) + { + appSettings.Network.IsAriaHttpProxy = isAriaHttpProxy; + return SetSettings(); + } + + /// + /// 获取Aria的http代理的地址 + /// + /// + public string GetAriaHttpProxy() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaHttpProxy == null) + { + // 第一次获取,先设置默认值 + SetAriaHttpProxy(ariaHttpProxy); + return ariaHttpProxy; + } + return appSettings.Network.AriaHttpProxy; + } + + /// + /// 设置Aria的http代理的地址 + /// + /// + /// + public bool SetAriaHttpProxy(string ariaHttpProxy) + { + appSettings.Network.AriaHttpProxy = ariaHttpProxy; + return SetSettings(); + } + + /// + /// 获取Aria的http代理的端口 + /// + /// + public int GetAriaHttpProxyListenPort() + { + appSettings = GetSettings(); + if (appSettings.Network.AriaHttpProxyListenPort == -1) + { + // 第一次获取,先设置默认值 + SetAriaHttpProxyListenPort(ariaHttpProxyListenPort); + return ariaHttpProxyListenPort; + } + return appSettings.Network.AriaHttpProxyListenPort; + } + + /// + /// 设置Aria的http代理的端口 + /// + /// + /// + public bool SetAriaHttpProxyListenPort(int ariaHttpProxyListenPort) + { + appSettings.Network.AriaHttpProxyListenPort = ariaHttpProxyListenPort; + return SetSettings(); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/SettingsManager.UserInfo.cs b/src/Downkyi.Core/Settings/SettingsManager.UserInfo.cs new file mode 100644 index 0000000..267b8f6 --- /dev/null +++ b/src/Downkyi.Core/Settings/SettingsManager.UserInfo.cs @@ -0,0 +1,43 @@ +using Downkyi.Core.Settings.Models; + +namespace Downkyi.Core.Settings; + +public partial class SettingsManager +{ + // 登录用户的mid + private readonly UserInfoSettings userInfo = new() + { + Mid = -1, + Name = "", + IsLogin = false, + IsVip = false + }; + + /// + /// 获取登录用户信息 + /// + /// + public UserInfoSettings GetUserInfo() + { + appSettings = GetSettings(); + if (appSettings.UserInfo == null) + { + // 第一次获取,先设置默认值 + SetUserInfo(userInfo); + return userInfo; + } + return appSettings.UserInfo; + } + + /// + /// 设置中保存登录用户的信息,在index刷新用户状态时使用 + /// + /// + /// + public bool SetUserInfo(UserInfoSettings userInfo) + { + appSettings.UserInfo = userInfo; + return SetSettings(); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/SettingsManager.Video.cs b/src/Downkyi.Core/Settings/SettingsManager.Video.cs new file mode 100644 index 0000000..f543ca2 --- /dev/null +++ b/src/Downkyi.Core/Settings/SettingsManager.Video.cs @@ -0,0 +1,352 @@ +using Downkyi.Core.FileName; +using Downkyi.Core.Settings.Enum; +using Downkyi.Core.Settings.Models; + +namespace Downkyi.Core.Settings; + +public partial class SettingsManager +{ + // 设置优先下载的视频编码 + private readonly int videoCodecs = 7; + + // 设置优先下载画质 + private readonly int quality = 120; + + // 设置优先下载音质 + private readonly int audioQuality = 30280; + + // 是否下载flv视频后转码为mp4 + private readonly AllowStatus isTranscodingFlvToMp4 = AllowStatus.YES; + + // 默认下载目录 + private readonly string saveVideoRootPath = Path.Combine(Environment.CurrentDirectory, "Media"); + + // 历史下载目录 + private readonly List historyVideoRootPaths = new(); + + // 是否使用默认下载目录,如果是,则每次点击下载选中项时不再询问下载目录 + private readonly AllowStatus isUseSaveVideoRootPath = AllowStatus.NO; + + // 下载内容 + private readonly VideoContentSettings videoContent = new(); + + // 文件命名格式 + private readonly List fileNameParts = new() + { + FileNamePart.MAIN_TITLE, + FileNamePart.SLASH, + FileNamePart.SECTION, + FileNamePart.SLASH, + FileNamePart.ORDER, + FileNamePart.HYPHEN, + FileNamePart.PAGE_TITLE, + FileNamePart.HYPHEN, + FileNamePart.VIDEO_QUALITY, + FileNamePart.HYPHEN, + FileNamePart.VIDEO_CODEC, + }; + + // 文件命名中的时间格式 + private readonly string fileNamePartTimeFormat = "yyyy-MM-dd"; + + // 文件命名中的序号格式 + private readonly OrderFormat orderFormat = OrderFormat.NATURAL; + + /// + /// 获取优先下载的视频编码 + /// + /// + public int GetVideoCodecs() + { + appSettings = GetSettings(); + if (appSettings.Video.VideoCodecs == -1) + { + // 第一次获取,先设置默认值 + SetVideoCodecs(videoCodecs); + return videoCodecs; + } + return appSettings.Video.VideoCodecs; + } + + /// + /// 设置优先下载的视频编码 + /// + /// + /// + public bool SetVideoCodecs(int videoCodecs) + { + appSettings.Video.VideoCodecs = videoCodecs; + return SetSettings(); + } + + /// + /// 获取优先下载画质 + /// + /// + public int GetQuality() + { + appSettings = GetSettings(); + if (appSettings.Video.Quality == -1) + { + // 第一次获取,先设置默认值 + SetQuality(quality); + return quality; + } + return appSettings.Video.Quality; + } + + /// + /// 设置优先下载画质 + /// + /// + /// + public bool SetQuality(int quality) + { + appSettings.Video.Quality = quality; + return SetSettings(); + } + + /// + /// 获取优先下载音质 + /// + /// + public int GetAudioQuality() + { + appSettings = GetSettings(); + if (appSettings.Video.AudioQuality == -1) + { + // 第一次获取,先设置默认值 + SetAudioQuality(audioQuality); + return audioQuality; + } + return appSettings.Video.AudioQuality; + } + + /// + /// 设置优先下载音质 + /// + /// + /// + public bool SetAudioQuality(int quality) + { + appSettings.Video.AudioQuality = quality; + return SetSettings(); + } + + /// + /// 获取是否下载flv视频后转码为mp4 + /// + /// + public AllowStatus IsTranscodingFlvToMp4() + { + appSettings = GetSettings(); + if (appSettings.Video.IsTranscodingFlvToMp4 == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsTranscodingFlvToMp4(isTranscodingFlvToMp4); + return isTranscodingFlvToMp4; + } + return appSettings.Video.IsTranscodingFlvToMp4; + } + + /// + /// 设置是否下载flv视频后转码为mp4 + /// + /// + /// + public bool IsTranscodingFlvToMp4(AllowStatus isTranscodingFlvToMp4) + { + appSettings.Video.IsTranscodingFlvToMp4 = isTranscodingFlvToMp4; + return SetSettings(); + } + + /// + /// 获取下载目录 + /// + /// + public string GetSaveVideoRootPath() + { + appSettings = GetSettings(); + if (appSettings.Video.SaveVideoRootPath == null) + { + // 第一次获取,先设置默认值 + SetSaveVideoRootPath(saveVideoRootPath); + return saveVideoRootPath; + } + return appSettings.Video.SaveVideoRootPath; + } + + /// + /// 设置下载目录 + /// + /// + /// + public bool SetSaveVideoRootPath(string path) + { + appSettings.Video.SaveVideoRootPath = path; + return SetSettings(); + } + + /// + /// 获取历史下载目录 + /// + /// + public List GetHistoryVideoRootPaths() + { + appSettings = GetSettings(); + if (appSettings.Video.HistoryVideoRootPaths == null) + { + // 第一次获取,先设置默认值 + SetHistoryVideoRootPaths(historyVideoRootPaths); + return historyVideoRootPaths; + } + return appSettings.Video.HistoryVideoRootPaths; + } + + /// + /// 设置历史下载目录 + /// + /// + /// + public bool SetHistoryVideoRootPaths(List historyPaths) + { + appSettings.Video.HistoryVideoRootPaths = historyPaths; + return SetSettings(); + } + + /// + /// 获取是否使用默认下载目录 + /// + /// + public AllowStatus IsUseSaveVideoRootPath() + { + appSettings = GetSettings(); + if (appSettings.Video.IsUseSaveVideoRootPath == AllowStatus.NONE) + { + // 第一次获取,先设置默认值 + IsUseSaveVideoRootPath(isUseSaveVideoRootPath); + return isUseSaveVideoRootPath; + } + return appSettings.Video.IsUseSaveVideoRootPath; + } + + /// + /// 设置是否使用默认下载目录 + /// + /// + /// + public bool IsUseSaveVideoRootPath(AllowStatus isUseSaveVideoRootPath) + { + appSettings.Video.IsUseSaveVideoRootPath = isUseSaveVideoRootPath; + return SetSettings(); + } + + /// + /// 获取下载内容 + /// + /// + public VideoContentSettings GetVideoContent() + { + appSettings = GetSettings(); + if (appSettings.Video.VideoContent == null) + { + // 第一次获取,先设置默认值 + SetVideoContent(videoContent); + return videoContent; + } + return appSettings.Video.VideoContent; + } + + /// + /// 设置下载内容 + /// + /// + /// + public bool SetVideoContent(VideoContentSettings videoContent) + { + appSettings.Video.VideoContent = videoContent; + return SetSettings(); + } + + /// + /// 获取文件命名格式 + /// + /// + public List GetFileNameParts() + { + appSettings = GetSettings(); + if (appSettings.Video.FileNameParts == null || appSettings.Video.FileNameParts.Count == 0) + { + // 第一次获取,先设置默认值 + SetFileNameParts(fileNameParts); + return fileNameParts; + } + return appSettings.Video.FileNameParts; + } + + /// + /// 设置文件命名格式 + /// + /// + /// + public bool SetFileNameParts(List fileNameParts) + { + appSettings.Video.FileNameParts = fileNameParts; + return SetSettings(); + } + + /// + /// 获取文件命名中的时间格式 + /// + /// + public string GetFileNamePartTimeFormat() + { + appSettings = GetSettings(); + if (appSettings.Video.FileNamePartTimeFormat == null || appSettings.Video.FileNamePartTimeFormat == string.Empty) + { + // 第一次获取,先设置默认值 + SetFileNamePartTimeFormat(fileNamePartTimeFormat); + return fileNamePartTimeFormat; + } + return appSettings.Video.FileNamePartTimeFormat; + } + + /// + /// 设置文件命名中的时间格式 + /// + /// + /// + public bool SetFileNamePartTimeFormat(string timeFormat) + { + appSettings.Video.FileNamePartTimeFormat = timeFormat; + return SetSettings(); + } + + /// + /// 获取文件命名中的序号格式 + /// + /// + public OrderFormat GetOrderFormat() + { + appSettings = GetSettings(); + if (appSettings.Video.OrderFormat == OrderFormat.NOT_SET) + { + // 第一次获取,先设置默认值 + SetOrderFormat(orderFormat); + return orderFormat; + } + return appSettings.Video.OrderFormat; + } + + /// + /// 设置文件命名中的序号格式 + /// + /// + /// + public bool SetOrderFormat(OrderFormat orderFormat) + { + appSettings.Video.OrderFormat = orderFormat; + return SetSettings(); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Settings/SettingsManager.cs b/src/Downkyi.Core/Settings/SettingsManager.cs new file mode 100644 index 0000000..6716091 --- /dev/null +++ b/src/Downkyi.Core/Settings/SettingsManager.cs @@ -0,0 +1,102 @@ +using Downkyi.Core.Settings.Models; +using Newtonsoft.Json; + +#if DEBUG +#else +using Downkyi.Core.Utils.Encryptor; +#endif + +namespace Downkyi.Core.Settings; + +public partial class SettingsManager +{ + private static SettingsManager instance; + + // 内存中保存一份配置 + private AppSettings appSettings; + +#if DEBUG + // 设置的配置文件 + private readonly string settingsName = Storage.StorageManager.GetSettings() + "_debug.json"; +#else + // 设置的配置文件 + private readonly string settingsName = Storage.StorageManager.GetSettings(); + + // 密钥 + private readonly string password = "YO1J$4#p"; +#endif + + /// + /// 获取SettingsManager实例 + /// + /// + public static SettingsManager GetInstance() + { + instance ??= new SettingsManager(); + return instance; + } + + /// + /// 隐藏Settings()方法,必须使用单例模式 + /// + private SettingsManager() + { + appSettings = GetSettings(); + } + + /// + /// 获取AppSettingsModel + /// + /// + private AppSettings GetSettings() + { + try + { + var fileStream = new FileStream(settingsName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var streamReader = new StreamReader(fileStream, System.Text.Encoding.UTF8); + string jsonWordTemplate = streamReader.ReadToEnd(); + streamReader.Close(); + fileStream.Close(); + +#if DEBUG +#else + // 解密字符串 + jsonWordTemplate = Encryptor.DecryptString(jsonWordTemplate, password); +#endif + + return JsonConvert.DeserializeObject(jsonWordTemplate); + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + return new AppSettings(); + } + } + + /// + /// 设置AppSettingsModel + /// + /// + private bool SetSettings() + { + try + { + string json = JsonConvert.SerializeObject(appSettings); + +#if DEBUG +#else + // 加密字符串 + json = Encryptor.EncryptString(json, password); +#endif + + File.WriteAllText(settingsName, json); + return true; + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + return false; + } + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Storage/Constant.cs b/src/Downkyi.Core/Storage/Constant.cs new file mode 100644 index 0000000..9b2a07c --- /dev/null +++ b/src/Downkyi.Core/Storage/Constant.cs @@ -0,0 +1,67 @@ +namespace Downkyi.Core.Storage; + +/// +/// 存储到本地时使用的一些常量 +/// +internal static class Constant +{ + // 根目录 + //private static string Root { get; } = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/Downkyi"; + private static string Root { get; } = Environment.CurrentDirectory; + + // Aria + public static string Aria { get; } = $"{Root}/Aria"; + + // 日志 + public static string Logs { get; } = $"{Root}/Logs"; + + // 数据库 + public static string Database { get; } = $"{Root}/Storage"; + + // 历史(搜索、下载) (加密) + public static string Download { get; } = $"{Database}/Download.db"; + public static string History { get; } = $"{Database}/History.db"; + + // 配置 + public static string Config { get; } = $"{Root}/Config"; + + // 设置 + public static string Settings { get; } = $"{Config}/Settings"; + + // 登录cookies + public static string Login { get; } = $"{Config}/Login"; + + // Bilibili + private static string Bilibili { get; } = $"{Root}/Bilibili"; + + // 弹幕 + public static string Danmaku { get; } = $"{Bilibili}/Danmakus"; + + // 字幕 + public static string Subtitle { get; } = $"{Bilibili}/Subtitle"; + + // 评论 + // TODO + + // 头图 + public static string Toutu { get; } = $"{Bilibili}/Toutu"; + + // 封面 + public static string Cover { get; } = $"{Bilibili}/Cover"; + + // 封面文件索引 + public static string CoverIndex { get; } = $"{Cover}/Index.db"; + + // 视频快照 + public static string Snapshot { get; } = $"{Bilibili}/Snapshot"; + + // 视频快照文件索引 + public static string SnapshotIndex { get; } = $"{Cover}/Index.db"; + + // 用户头像 + public static string Header { get; } = $"{Bilibili}/Header"; + + // 用户头像文件索引 + public static string HeaderIndex { get; } = $"{Header}/Index.db"; + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Storage/StorageManager.cs b/src/Downkyi.Core/Storage/StorageManager.cs new file mode 100644 index 0000000..681aa4f --- /dev/null +++ b/src/Downkyi.Core/Storage/StorageManager.cs @@ -0,0 +1,153 @@ +namespace Downkyi.Core.Storage; + +public static class StorageManager +{ + /// + /// 获取历史记录的文件路径 + /// + /// + public static string GetAriaDir() + { + CreateDirectory(Constant.Aria); + return Constant.Aria; + } + + /// + /// 获取历史记录的文件路径 + /// + /// + public static string GetDownload() + { + CreateDirectory(Constant.Database); + return Constant.Download; + } + + /// + /// 获取历史记录的文件路径 + /// + /// + public static string GetHistory() + { + CreateDirectory(Constant.Database); + return Constant.History; + } + + /// + /// 获取设置的文件路径 + /// + /// + public static string GetSettings() + { + CreateDirectory(Constant.Config); + return Constant.Settings; + } + + /// + /// 获取登录cookies的文件路径 + /// + /// + public static string GetLogin() + { + CreateDirectory(Constant.Config); + return Constant.Login; + } + + /// + /// 获取弹幕的文件夹路径 + /// + /// + public static string GetDanmaku() + { + return CreateDirectory(Constant.Danmaku); + } + + /// + /// 获取字幕的文件夹路径 + /// + /// + public static string GetSubtitle() + { + return CreateDirectory(Constant.Subtitle); + } + + /// + /// 获取头图的文件夹路径 + /// + /// + public static string GetToutu() + { + return CreateDirectory(Constant.Toutu); + } + + /// + /// 获取封面的文件夹路径 + /// + /// + public static string GetCover() + { + return CreateDirectory(Constant.Cover); + } + + /// + /// 获取封面索引的文件路径 + /// + /// + public static string GetCoverIndex() + { + CreateDirectory(Constant.Cover); + return Constant.CoverIndex; + } + + /// + /// 获取视频快照的文件夹路径 + /// + /// + public static string GetSnapshot() + { + return CreateDirectory(Constant.Snapshot); + } + + /// + /// 获取视频快照索引的文件路径 + /// + /// + public static string GetSnapshotIndex() + { + CreateDirectory(Constant.Snapshot); + return Constant.SnapshotIndex; + } + + /// + /// 获取用户头像的文件夹路径 + /// + /// + public static string GetHeader() + { + return CreateDirectory(Constant.Header); + } + + /// + /// 获取用户头像索引的文件路径 + /// + /// + public static string GetHeaderIndex() + { + CreateDirectory(Constant.Header); + return Constant.HeaderIndex; + } + + + /// + /// 若文件夹不存在,则创建文件夹 + /// + /// + /// + private static string CreateDirectory(string directory) + { + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + return directory; + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/Encryptor/Encryptor.File.cs b/src/Downkyi.Core/Utils/Encryptor/Encryptor.File.cs new file mode 100644 index 0000000..15b9ec6 --- /dev/null +++ b/src/Downkyi.Core/Utils/Encryptor/Encryptor.File.cs @@ -0,0 +1,185 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Downkyi.Core.Utils.Encryptor; + +public sealed class EncryptorFile +{ + string iv = "xIrlMBGX"; + string key = "A2&P!buv"; + + /// + /// DES加密偏移量,必须是>=8位长的字符串 + /// + public string IV + { + get { return iv; } + set { iv = value; } + } + + /// + /// DES加密的私钥,必须是8位长的字符串 + /// + public string Key + { + get { return key; } + set { key = value; } + } + + /// + /// 对字符串进行DES加密 + /// + /// 待加密的字符串 + /// 加密后的BASE64编码的字符串 + public string Encrypt(string sourceString) + { + byte[] btKey = Encoding.Default.GetBytes(key); + byte[] btIV = Encoding.Default.GetBytes(iv); +#pragma warning disable SYSLIB0021 // 类型或成员已过时 + var des = new DESCryptoServiceProvider(); +#pragma warning restore SYSLIB0021 // 类型或成员已过时 + using (MemoryStream ms = new MemoryStream()) + { + byte[] inData = Encoding.Default.GetBytes(sourceString); + try + { + using (CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(btKey, btIV), CryptoStreamMode.Write)) + { + cs.Write(inData, 0, inData.Length); + cs.FlushFinalBlock(); + } + + return Convert.ToBase64String(ms.ToArray()); + } + catch + { + throw; + } + } + } + + /// + /// 对DES加密后的字符串进行解密 + /// + /// 待解密的字符串 + /// 解密后的字符串 + public string Decrypt(string encryptedString) + { + byte[] btKey = Encoding.Default.GetBytes(key); + byte[] btIV = Encoding.Default.GetBytes(iv); +#pragma warning disable SYSLIB0021 // 类型或成员已过时 + var des = new DESCryptoServiceProvider(); +#pragma warning restore SYSLIB0021 // 类型或成员已过时 + + using (MemoryStream ms = new MemoryStream()) + { + byte[] inData = Convert.FromBase64String(encryptedString); + try + { + using (CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(btKey, btIV), CryptoStreamMode.Write)) + { + cs.Write(inData, 0, inData.Length); + cs.FlushFinalBlock(); + } + + return Encoding.Default.GetString(ms.ToArray()); + } + catch + { + throw; + } + } + } + + /// + /// 对文件内容进行DES加密 + /// + /// 待加密的文件绝对路径 + /// 加密后的文件保存的绝对路径 + public void EncryptFile(string sourceFile, string destFile) + { + if (!File.Exists(sourceFile)) throw new FileNotFoundException("指定的文件路径不存在!", sourceFile); + + byte[] btKey = Encoding.Default.GetBytes(key); + byte[] btIV = Encoding.Default.GetBytes(iv); +#pragma warning disable SYSLIB0021 // 类型或成员已过时 + var des = new DESCryptoServiceProvider(); +#pragma warning restore SYSLIB0021 // 类型或成员已过时 + byte[] btFile = File.ReadAllBytes(sourceFile); + + using (FileStream fs = new FileStream(destFile, FileMode.Create, FileAccess.Write)) + { + try + { + using (CryptoStream cs = new CryptoStream(fs, des.CreateEncryptor(btKey, btIV), CryptoStreamMode.Write)) + { + cs.Write(btFile, 0, btFile.Length); + cs.FlushFinalBlock(); + } + } + catch + { + throw; + } + finally + { + fs.Close(); + } + } + } + + /// + /// 对文件内容进行DES加密,加密后覆盖掉原来的文件 + /// + /// 待加密的文件的绝对路径 + public void EncryptFile(string sourceFile) + { + EncryptFile(sourceFile, sourceFile); + } + + /// + /// 对文件内容进行DES解密 + /// + /// 待解密的文件绝对路径 + /// 解密后的文件保存的绝对路径 + public void DecryptFile(string sourceFile, string destFile) + { + if (!File.Exists(sourceFile)) throw new FileNotFoundException("指定的文件路径不存在!", sourceFile); + + byte[] btKey = Encoding.Default.GetBytes(key); + byte[] btIV = Encoding.Default.GetBytes(iv); +#pragma warning disable SYSLIB0021 // 类型或成员已过时 + var des = new DESCryptoServiceProvider(); +#pragma warning restore SYSLIB0021 // 类型或成员已过时 + byte[] btFile = File.ReadAllBytes(sourceFile); + + using (FileStream fs = new FileStream(destFile, FileMode.Create, FileAccess.Write)) + { + try + { + using (CryptoStream cs = new CryptoStream(fs, des.CreateDecryptor(btKey, btIV), CryptoStreamMode.Write)) + { + cs.Write(btFile, 0, btFile.Length); + cs.FlushFinalBlock(); + } + } + catch + { + throw; + } + finally + { + fs.Close(); + } + } + } + + /// + /// 对文件内容进行DES解密,加密后覆盖掉原来的文件 + /// + /// 待解密的文件的绝对路径 + public void DecryptFile(string sourceFile) + { + DecryptFile(sourceFile, sourceFile); + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/Encryptor/Encryptor.String.cs b/src/Downkyi.Core/Utils/Encryptor/Encryptor.String.cs new file mode 100644 index 0000000..e3ae690 --- /dev/null +++ b/src/Downkyi.Core/Utils/Encryptor/Encryptor.String.cs @@ -0,0 +1,68 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Downkyi.Core.Utils.Encryptor; + +public static partial class Encryptor +{ + /// + /// DES加密字符串 + /// + /// 待加密的字符串 + /// 加密密钥,要求为8位 + /// 加密成功返回加密后的字符串,失败返回源串 + public static string EncryptString(string encryptString, string encryptKey) + { + try + { + byte[] rgbKey = Encoding.UTF8.GetBytes(encryptKey[..8]);//转换为字节 + byte[] rgbIV = Encoding.UTF8.GetBytes(encryptKey[..8]); + byte[] inputByteArray = Encoding.UTF8.GetBytes(encryptString); +#pragma warning disable SYSLIB0021 // 类型或成员已过时 + var dCSP = new DESCryptoServiceProvider();//实例化数据加密标准 +#pragma warning restore SYSLIB0021 // 类型或成员已过时 + var mStream = new MemoryStream();//实例化内存流 + //将数据流链接到加密转换的流 + var cStream = new CryptoStream(mStream, dCSP.CreateEncryptor(rgbKey, rgbIV), CryptoStreamMode.Write); + cStream.Write(inputByteArray, 0, inputByteArray.Length); + cStream.FlushFinalBlock(); + // 转base64 + return Convert.ToBase64String(mStream.ToArray()); + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + return encryptString; + } + } + + /// + /// DES解密字符串 + /// + /// 待解密的字符串 + /// 解密密钥,要求为8位,和加密密钥相同 + /// 解密成功返回解密后的字符串,失败返源串 + public static string DecryptString(string decryptString, string decryptKey) + { + try + { + byte[] rgbKey = Encoding.UTF8.GetBytes(decryptKey); + byte[] rgbIV = Encoding.UTF8.GetBytes(decryptKey); + byte[] inputByteArray = Convert.FromBase64String(decryptString); +#pragma warning disable SYSLIB0021 // 类型或成员已过时 + var DCSP = new DESCryptoServiceProvider(); +#pragma warning restore SYSLIB0021 // 类型或成员已过时 + var mStream = new MemoryStream(); + var cStream = new CryptoStream(mStream, DCSP.CreateDecryptor(rgbKey, rgbIV), CryptoStreamMode.Write); + cStream.Write(inputByteArray, 0, inputByteArray.Length); + cStream.FlushFinalBlock(); + return Encoding.UTF8.GetString(mStream.ToArray()); + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + return decryptString; + } + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/Encryptor/Hash.cs b/src/Downkyi.Core/Utils/Encryptor/Hash.cs new file mode 100644 index 0000000..daa7bc4 --- /dev/null +++ b/src/Downkyi.Core/Utils/Encryptor/Hash.cs @@ -0,0 +1,67 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Downkyi.Core.Utils.Encryptor; + +public static class Hash +{ + /// + /// 计算字符串MD5值 + /// + /// + /// + public static string GetMd5Hash(string input) + { + if (input == null) + { + return null; + } + + MD5 md5Hash = MD5.Create(); + + // 将输入字符串转换为字节数组并计算哈希数据 + byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input)); + + // 创建一个 Stringbuilder 来收集字节并创建字符串 + var sBuilder = new StringBuilder(); + + // 循环遍历哈希数据的每一个字节并格式化为十六进制字符串 + for (int i = 0; i < data.Length; i++) + { + sBuilder.Append(data[i].ToString("x2")); + } + + // 返回十六进制字符串 + return sBuilder.ToString(); + } + + /// + /// 计算文件MD5值 + /// + /// + /// + public static string GetMD5HashFromFile(string fileName) + { + try + { + var file = new FileStream(fileName, FileMode.Open); +#pragma warning disable SYSLIB0021 // 类型或成员已过时 + MD5 md5 = new MD5CryptoServiceProvider(); +#pragma warning restore SYSLIB0021 // 类型或成员已过时 + byte[] retVal = md5.ComputeHash(file); + file.Close(); + + var sb = new StringBuilder(); + for (int i = 0; i < retVal.Length; i++) + { + sb.Append(retVal[i].ToString("x2")); + } + return sb.ToString(); + } + catch (Exception e) + { + throw new Exception("GetMD5HashFromFile()发生异常: {0}" + e.Message); + } + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/Format.cs b/src/Downkyi.Core/Utils/Format.cs new file mode 100644 index 0000000..760eb84 --- /dev/null +++ b/src/Downkyi.Core/Utils/Format.cs @@ -0,0 +1,220 @@ +using System.Text.RegularExpressions; + +namespace Downkyi.Core.Utils; + +public static class Format +{ + + /// + /// 格式化Duration时间 + /// + /// + /// + public static string FormatDuration(long duration) + { + string formatDuration; + if (duration / 60 > 0) + { + long dur = duration / 60; + if (dur / 60 > 0) + { + formatDuration = $"{dur / 60}h{dur % 60}m{duration % 60}s"; + } + else + { + formatDuration = $"{duration / 60}m{duration % 60}s"; + } + } + else + { + formatDuration = $"{duration}s"; + } + return formatDuration; + } + + /// + /// 格式化Duration时间,格式为00:00:00 + /// + /// + /// + public static string FormatDuration2(long duration) + { + string formatDuration; + if (duration / 60 > 0) + { + long dur = duration / 60; + if (dur / 60 > 0) + { + formatDuration = string.Format("{0:D2}", dur / 60) + ":" + string.Format("{0:D2}", dur % 60) + ":" + string.Format("{0:D2}", duration % 60); + } + else + { + formatDuration = "00:" + string.Format("{0:D2}", duration / 60) + ":" + string.Format("{0:D2}", duration % 60); + } + } + else + { + formatDuration = "00:00:" + string.Format("{0:D2}", duration); + } + return formatDuration; + } + + /// + /// 格式化Duration时间,格式为00:00 + /// + /// + /// + public static string FormatDuration3(long duration) + { + string formatDuration; + if (duration / 60 > 0) + { + long dur = duration / 60; + if (dur / 60 > 0) + { + formatDuration = string.Format("{0:D2}", dur / 60) + ":" + string.Format("{0:D2}", dur % 60) + ":" + string.Format("{0:D2}", duration % 60); + } + else + { + formatDuration = string.Format("{0:D2}", duration / 60) + ":" + string.Format("{0:D2}", duration % 60); + } + } + else + { + formatDuration = "00:" + string.Format("{0:D2}", duration); + } + return formatDuration; + } + + /// + /// 格式化数字,超过10000的数字将单位改为万,超过100000000的数字将单位改为亿,并保留1位小数 + /// + /// + /// + public static string FormatNumber(long number) + { + if (number > 99999999) + { + return (number / 100000000.0f).ToString("F1") + "亿"; + } + + if (number > 9999) + { + return (number / 10000.0f).ToString("F1") + "万"; + } + else + { + return number.ToString(); + } + } + + /// + /// 格式化网速 + /// + /// + /// + public static string FormatSpeed(float speed) + { + string formatSpeed; + if (speed <= 0) + { + formatSpeed = "0B/s"; + } + else if (speed < 1024) + { + formatSpeed = string.Format("{0:F2}", speed) + "B/s"; + } + else if (speed < 1024 * 1024) + { + formatSpeed = string.Format("{0:F2}", speed / 1024) + "KB/s"; + } + else + { + formatSpeed = string.Format("{0:F2}", speed / 1024 / 1024) + "MB/s"; + } + return formatSpeed; + } + + /// + /// 格式化字节大小,可用于文件大小的显示 + /// + /// + /// + public static string FormatFileSize(long fileSize) + { + string formatFileSize; + if (fileSize <= 0) + { + formatFileSize = "0B"; + } + else if (fileSize < 1024) + { + formatFileSize = fileSize.ToString() + "B"; + } + else if (fileSize < 1024 * 1024) + { + formatFileSize = (fileSize / 1024.0).ToString("#.##") + "KB"; + } + else if (fileSize < 1024 * 1024 * 1024) + { + formatFileSize = (fileSize / 1024.0 / 1024.0).ToString("#.##") + "MB"; + } + else + { + formatFileSize = (fileSize / 1024.0 / 1024.0 / 1024.0).ToString("#.##") + "GB"; + } + return formatFileSize; + } + + /// + /// 去除非法字符 + /// + /// + /// + public static string FormatFileName(string originName) + { + string destName = originName; + + // Windows中不能作为文件名的字符 + destName = destName.Replace("\\", " "); + destName = destName.Replace("/", " "); + destName = destName.Replace(":", " "); + destName = destName.Replace("*", " "); + destName = destName.Replace("?", " "); + destName = destName.Replace("\"", " "); + destName = destName.Replace("<", " "); + destName = destName.Replace(">", " "); + destName = destName.Replace("|", " "); + + // 转义字符 + destName = destName.Replace("\a", ""); + destName = destName.Replace("\b", ""); + destName = destName.Replace("\f", ""); + destName = destName.Replace("\n", ""); + destName = destName.Replace("\r", ""); + destName = destName.Replace("\t", ""); + destName = destName.Replace("\v", ""); + + // 控制字符 + destName = Regex.Replace(destName, @"\p{C}+", string.Empty); + + // 移除前导和尾部的空白字符、dot符 + int i, j; + for (i = 0; i < destName.Length; i++) + { + if (destName[i] != ' ' && destName[i] != '.') + { + break; + } + } + for (j = destName.Length - 1; j >= 0; j--) + { + if (destName[j] != ' ' && destName[j] != '.') + { + break; + } + } + return destName.Substring(i, j - i + 1); + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/HardDisk.cs b/src/Downkyi.Core/Utils/HardDisk.cs new file mode 100644 index 0000000..4b2bba1 --- /dev/null +++ b/src/Downkyi.Core/Utils/HardDisk.cs @@ -0,0 +1,64 @@ +namespace Downkyi.Core.Utils; + +public static class HardDisk +{ + /// + /// 获取指定驱动器的空间总大小 + /// + /// 只需输入代表驱动器的字母即可 + /// + public static long GetHardDiskSpace(string hardDiskName) + { + long totalSize = 0; + + try + { + hardDiskName = $"{hardDiskName}:\\"; + DriveInfo[] drives = DriveInfo.GetDrives(); + + foreach (DriveInfo drive in drives) + { + if (drive.Name == hardDiskName) + { + totalSize = drive.TotalSize; + } + } + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + } + + return totalSize; + } + + /// + /// 获取指定驱动器的剩余空间总大小 + /// + /// 只需输入代表驱动器的字母即可 + /// + public static long GetHardDiskFreeSpace(string hardDiskName) + { + long freeSpace = 0; + try + { + hardDiskName = $"{hardDiskName}:\\"; + DriveInfo[] drives = DriveInfo.GetDrives(); + + foreach (DriveInfo drive in drives) + { + if (drive.Name == hardDiskName) + { + freeSpace = drive.TotalFreeSpace; + } + } + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + } + + return freeSpace; + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/LinkStack.cs b/src/Downkyi.Core/Utils/LinkStack.cs new file mode 100644 index 0000000..a0b9494 --- /dev/null +++ b/src/Downkyi.Core/Utils/LinkStack.cs @@ -0,0 +1,87 @@ +namespace Downkyi.Core.Utils; + +// 链表实现栈 +public class LinkStack +{ + //栈顶指示器 + public Node Top { get; set; } + + //栈中结点的个数 + public int NCount { get; set; } + + //初始化 + public LinkStack() + { + Top = null; + NCount = 0; + } + + //获取栈的长度 + public int GetLength() + { + return NCount; + } + + //判断栈是否为空 + public bool IsEmpty() + { + if ((Top == null) && (0 == NCount)) + { + return true; + } + return false; + } + + //入栈 + public void Push(T item) + { + Node p = new(item); + if (Top == null) + { + Top = p; + } + else + { + p.Next = Top; + Top = p; + } + NCount++; + } + + //出栈 + public T Pop() + { + if (IsEmpty()) + { + return default; + } + Node p = Top; + Top = Top.Next; + --NCount; + return p.Data; + } + + // + public T Peek() + { + if (IsEmpty()) + { + return default; + } + + return Top.Data; + } +} + +//结点定义 +public class Node +{ + public T Data; + + public Node Next; + + public Node(T item) + { + Data = item; + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/ListHelper.cs b/src/Downkyi.Core/Utils/ListHelper.cs new file mode 100644 index 0000000..078cd3b --- /dev/null +++ b/src/Downkyi.Core/Utils/ListHelper.cs @@ -0,0 +1,56 @@ +using System.Collections.ObjectModel; + +namespace Downkyi.Core.Utils; + +public static class ListHelper +{ + + /// + /// 判断ObservableCollection中是否存在,不存在则添加 + /// + /// + /// + /// + public static void AddUnique(ObservableCollection list, T item) + { + if (!list.Contains(item)) + { + list.Add(item); + } + } + + /// + /// 判断List中是否存在,不存在则添加 + /// + /// + /// + /// + public static void AddUnique(List list, T item) + { + if (!list.Exists(t => t.Equals(item))) + { + list.Add(item); + } + } + + /// + /// 判断List中是否存在,不存在则添加 + /// + /// + /// + /// + /// + public static void InsertUnique(List list, T item, int index) + { + if (!list.Exists(t => t.Equals(item))) + { + list.Insert(index, item); + } + else + { + list.Remove(item); + list.Insert(index, item); + } + } + +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/ObjectHelper.cs b/src/Downkyi.Core/Utils/ObjectHelper.cs new file mode 100644 index 0000000..846d3a4 --- /dev/null +++ b/src/Downkyi.Core/Utils/ObjectHelper.cs @@ -0,0 +1,63 @@ +using System.Runtime.Serialization.Formatters.Binary; + +namespace Downkyi.Core.Utils; + +public static class ObjectHelper +{ + /// + /// 写入序列化对象到磁盘 + /// + /// + /// + /// + public static bool WriteObjectToDisk(string file, object obj) + { + try + { + using Stream stream = File.Create(file); + var formatter = new BinaryFormatter(); +#pragma warning disable SYSLIB0011 // 类型或成员已过时 + formatter.Serialize(stream, obj); +#pragma warning restore SYSLIB0011 // 类型或成员已过时 + + return true; + } + catch (IOException e) + { + Log.Log.Logger.Error(e); + return false; + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + return false; + } + } + + /// + /// 从磁盘读取序列化对象 + /// + /// + /// + public static object ReadObjectFromDisk(string file) + { + try + { + using Stream stream = File.Open(file, FileMode.Open); + var formatter = new BinaryFormatter(); +#pragma warning disable SYSLIB0011 // 类型或成员已过时 + return formatter.Deserialize(stream); +#pragma warning restore SYSLIB0011 // 类型或成员已过时 + } + catch (IOException e) + { + Log.Log.Logger.Error(e); + return null; + } + catch (Exception e) + { + Log.Log.Logger.Error(e); + return null; + } + } +} \ No newline at end of file diff --git a/src/Downkyi.Core/Utils/Stack.cs b/src/Downkyi.Core/Utils/Stack.cs new file mode 100644 index 0000000..cf133a3 --- /dev/null +++ b/src/Downkyi.Core/Utils/Stack.cs @@ -0,0 +1,58 @@ +namespace Downkyi.Core.Utils; + +public class Stack +{ + private int _maxSize; + private int _top = -1; + private T[] _data; + + public int Count => _maxSize; + public bool IsFull { get => _top == _maxSize - 1; } + public bool IsEmpty { get => _top == -1; } + + public Stack(int maxSize) + { + _maxSize = maxSize; + _data = new T[maxSize]; + } + + public void Push(T value) + { + if (IsFull) + { + return; + } + _data[++_top] = value; + } + + public T Pop() + { + if (IsEmpty) + { + return default; + } + + return _data[_top--]; + } + + public T Peek() + { + if (IsEmpty) + { + return default; + } + + return _data[_top]; + } + + //public Stack? Traverse() + //{ + // if (IsEmpty) { return default; } + + // for (int i = _top; i >= 0; i--) + // { + // // TODO + // } + //} + +} diff --git a/src/Downkyi.Core/Utils/Validator/Number.cs b/src/Downkyi.Core/Utils/Validator/Number.cs new file mode 100644 index 0000000..2d6ef4d --- /dev/null +++ b/src/Downkyi.Core/Utils/Validator/Number.cs @@ -0,0 +1,28 @@ +using System.Text.RegularExpressions; + +namespace Downkyi.Core.Utils.Validator; + +public static class Number +{ + + /// + /// 字符串转数字(长整型) + /// + /// + /// + public static long GetInt(string value) + { + return IsInt(value) ? long.Parse(value) : -1; + } + + /// + /// 是否为数字 + /// + /// + /// + public static bool IsInt(string value) + { + return Regex.IsMatch(value, @"^\d+$"); + } + +} \ No newline at end of file diff --git a/src/Downkyi.UI/Downkyi.UI.csproj b/src/Downkyi.UI/Downkyi.UI.csproj new file mode 100644 index 0000000..efa17c7 --- /dev/null +++ b/src/Downkyi.UI/Downkyi.UI.csproj @@ -0,0 +1,19 @@ + + + net6.0 + enable + enable + + + none + + + + + + + + + + + diff --git a/src/Downkyi.UI/Models/Cookie.cs b/src/Downkyi.UI/Models/Cookie.cs new file mode 100644 index 0000000..5b5a4ba --- /dev/null +++ b/src/Downkyi.UI/Models/Cookie.cs @@ -0,0 +1,7 @@ +namespace Downkyi.UI.Models; + +public class Cookie +{ + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Downkyi.UI/Models/FileNamePartDisplay.cs b/src/Downkyi.UI/Models/FileNamePartDisplay.cs new file mode 100644 index 0000000..f67cb86 --- /dev/null +++ b/src/Downkyi.UI/Models/FileNamePartDisplay.cs @@ -0,0 +1,16 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Downkyi.Core.FileName; + +namespace Downkyi.UI.Models; + +public partial class FileNamePartDisplay : ObservableObject +{ + [ObservableProperty] + public int _index; + + [ObservableProperty] + public FileNamePart _id; + + [ObservableProperty] + public string _title = string.Empty; +} \ No newline at end of file diff --git a/src/Downkyi.UI/Models/OrderFormatDisplay.cs b/src/Downkyi.UI/Models/OrderFormatDisplay.cs new file mode 100644 index 0000000..f9666a5 --- /dev/null +++ b/src/Downkyi.UI/Models/OrderFormatDisplay.cs @@ -0,0 +1,9 @@ +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.UI.Models; + +public class OrderFormatDisplay +{ + public OrderFormat Id { get; set; } + public string Name { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Downkyi.UI/Models/ParseScopeDisplay.cs b/src/Downkyi.UI/Models/ParseScopeDisplay.cs new file mode 100644 index 0000000..c18607f --- /dev/null +++ b/src/Downkyi.UI/Models/ParseScopeDisplay.cs @@ -0,0 +1,9 @@ +using Downkyi.Core.Settings.Enum; + +namespace Downkyi.UI.Models; + +public class ParseScopeDisplay +{ + public string Name { get; set; } = string.Empty; + public ParseScope ParseScope { get; set; } +} \ No newline at end of file diff --git a/src/Downkyi.UI/Models/TabHeader.cs b/src/Downkyi.UI/Models/TabHeader.cs new file mode 100644 index 0000000..07a7036 --- /dev/null +++ b/src/Downkyi.UI/Models/TabHeader.cs @@ -0,0 +1,9 @@ +namespace Downkyi.UI.Models; + +public class TabHeader +{ + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Icon { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Downkyi.UI/Models/ThirdParty.cs b/src/Downkyi.UI/Models/ThirdParty.cs new file mode 100644 index 0000000..5479924 --- /dev/null +++ b/src/Downkyi.UI/Models/ThirdParty.cs @@ -0,0 +1,40 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Diagnostics; + +namespace Downkyi.UI.Models; + +public partial class ThirdParty : ObservableObject +{ + #region 属性申明 + + [ObservableProperty] + private string _name = string.Empty; + + [ObservableProperty] + private string _author = string.Empty; + + [ObservableProperty] + private string _version = string.Empty; + + [ObservableProperty] + private string _license = string.Empty; + + [ObservableProperty] + private string _homepage = string.Empty; + + [ObservableProperty] + private string _licenseUrl = string.Empty; + + #endregion + + #region 命令申明 + + [RelayCommand] + private void VisitHomepage() { Process.Start(new ProcessStartInfo(Homepage) { UseShellExecute = true }); } + + [RelayCommand] + private void GetLicense() { Process.Start(new ProcessStartInfo(LicenseUrl) { UseShellExecute = true }); } + + #endregion +} \ No newline at end of file diff --git a/src/Downkyi.UI/Mvvm/BaseServices.cs b/src/Downkyi.UI/Mvvm/BaseServices.cs new file mode 100644 index 0000000..58035e3 --- /dev/null +++ b/src/Downkyi.UI/Mvvm/BaseServices.cs @@ -0,0 +1,34 @@ +using Downkyi.UI.Services; +using Downkyi.UI.Services.Event; + +namespace Downkyi.UI.Mvvm; + +public class BaseServices +{ + public IBroadcastEvent BroadcastEvent { get; } + + public INotificationEvent NotificationEvent { get; } + + public IDictionaryResource DictionaryResource { get; } + + public INavigationService NavigationService { get; } + + public IStoragePicker StoragePicker { get; } + + public IStorageService StorageService { get; } + + public BaseServices(IBroadcastEvent broadcastEvent, + INotificationEvent notificationEvent, + IDictionaryResource dictionaryResource, + INavigationService navigationService, + IStoragePicker storagePicker, + IStorageService storageService) + { + BroadcastEvent = broadcastEvent; + NotificationEvent = notificationEvent; + DictionaryResource = dictionaryResource; + NavigationService = navigationService; + StoragePicker = storagePicker; + StorageService = storageService; + } +} \ No newline at end of file diff --git a/src/Downkyi.UI/Mvvm/Navigation/INavigationAware.cs b/src/Downkyi.UI/Mvvm/Navigation/INavigationAware.cs new file mode 100644 index 0000000..35e50b6 --- /dev/null +++ b/src/Downkyi.UI/Mvvm/Navigation/INavigationAware.cs @@ -0,0 +1,8 @@ +namespace Downkyi.UI.Mvvm.Navigation; + +public interface INavigationAware +{ + void OnNavigatedTo(Dictionary? parameter); + + void OnNavigatedFrom(Dictionary? parameter); +} diff --git a/src/Downkyi.UI/Mvvm/ViewModelBase.cs b/src/Downkyi.UI/Mvvm/ViewModelBase.cs new file mode 100644 index 0000000..7b76597 --- /dev/null +++ b/src/Downkyi.UI/Mvvm/ViewModelBase.cs @@ -0,0 +1,31 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Downkyi.UI.Mvvm.Navigation; +using Downkyi.UI.Services; +using Downkyi.UI.Services.Event; + +namespace Downkyi.UI.Mvvm; + +public abstract class ViewModelBase : ObservableObject, INavigationAware +{ + private readonly BaseServices _baseServices; + + protected IBroadcastEvent BroadcastEvent => _baseServices.BroadcastEvent; + protected INotificationEvent NotificationEvent => _baseServices.NotificationEvent; + protected IDictionaryResource DictionaryResource => _baseServices.DictionaryResource; + protected INavigationService NavigationService => _baseServices.NavigationService; + protected IStoragePicker StoragePicker => _baseServices.StoragePicker; + protected IStorageService StorageService => _baseServices.StorageService; + + public ViewModelBase(BaseServices baseServices) + { + _baseServices = baseServices; + } + + public virtual void OnNavigatedTo(Dictionary? parameter) + { + } + + public virtual void OnNavigatedFrom(Dictionary? parameter) + { + } +} \ No newline at end of file diff --git a/src/Downkyi.UI/Services/Event/BroadcastEvent.cs b/src/Downkyi.UI/Services/Event/BroadcastEvent.cs new file mode 100644 index 0000000..8cbbaad --- /dev/null +++ b/src/Downkyi.UI/Services/Event/BroadcastEvent.cs @@ -0,0 +1,23 @@ +namespace Downkyi.UI.Services.Event; + +public class BroadcastEvent : IBroadcastEvent +{ + private delegate void BroadcastedEventHandler(object obj, string key); + private event BroadcastedEventHandler? Broadcasted; + + public void Receive(string key, Action action) + { + Broadcasted += (obj, k) => + { + if (key == k) + { + Task.Run(() => action?.Invoke(obj)); + } + }; + } + + public void Send(string key, object obj) + { + Broadcasted?.Invoke(obj, key); + } +} \ No newline at end of file diff --git a/src/Downkyi.UI/Services/Event/IBroadcastEvent.cs b/src/Downkyi.UI/Services/Event/IBroadcastEvent.cs new file mode 100644 index 0000000..73ea44c --- /dev/null +++ b/src/Downkyi.UI/Services/Event/IBroadcastEvent.cs @@ -0,0 +1,8 @@ +namespace Downkyi.UI.Services.Event; + +public interface IBroadcastEvent +{ + void Receive(string key, Action action); + + void Send(string key, object obj); +} \ No newline at end of file diff --git a/src/Downkyi.UI/Services/Event/INotificationEvent.cs b/src/Downkyi.UI/Services/Event/INotificationEvent.cs new file mode 100644 index 0000000..2fc4131 --- /dev/null +++ b/src/Downkyi.UI/Services/Event/INotificationEvent.cs @@ -0,0 +1,8 @@ +namespace Downkyi.UI.Services.Event; + +public interface INotificationEvent +{ + void Subscribe(Action action); + + void Publish(string msg); +} \ No newline at end of file diff --git a/src/Downkyi.UI/Services/Event/NotificationEvent.cs b/src/Downkyi.UI/Services/Event/NotificationEvent.cs new file mode 100644 index 0000000..7362bf0 --- /dev/null +++ b/src/Downkyi.UI/Services/Event/NotificationEvent.cs @@ -0,0 +1,18 @@ +namespace Downkyi.UI.Services.Event; + +public class NotificationEvent : INotificationEvent +{ + private Action? _action; + private string _message = string.Empty; + + public void Publish(string msg) + { + _message = msg; + Task.Run(() => _action?.Invoke(_message)); + } + + public void Subscribe(Action action) + { + _action = action; + } +} \ No newline at end of file diff --git a/src/Downkyi.UI/Services/IDictionaryResource.cs b/src/Downkyi.UI/Services/IDictionaryResource.cs new file mode 100644 index 0000000..da9b461 --- /dev/null +++ b/src/Downkyi.UI/Services/IDictionaryResource.cs @@ -0,0 +1,30 @@ +namespace Downkyi.UI.Services; + +public interface IDictionaryResource +{ + /// + /// 从资源获取颜色的16进制字符串 + /// + /// + /// + string GetColor(string resourceKey); + + /// + /// 从资源获取字符串 + /// + /// + /// + string GetString(string resourceKey); + + /// + /// 根据languageCode切换界面语言 + /// + /// + void LoadLanguage(string languageCode); + + /// + /// 切换主题 + /// + /// + void LoadTheme(string theme); +} \ No newline at end of file diff --git a/src/Downkyi.UI/Services/INavigationService.cs b/src/Downkyi.UI/Services/INavigationService.cs new file mode 100644 index 0000000..5f44a07 --- /dev/null +++ b/src/Downkyi.UI/Services/INavigationService.cs @@ -0,0 +1,12 @@ +namespace Downkyi.UI.Services; + +public interface INavigationService +{ + Task ForwardAsync(string viewKey); + + Task ForwardAsync(string viewKey, Dictionary parameter); + + Task BackwardAsync(); + + Task BackwardAsync(Dictionary parameter); +} \ No newline at end of file diff --git a/src/Downkyi.UI/Services/IStoragePicker.cs b/src/Downkyi.UI/Services/IStoragePicker.cs new file mode 100644 index 0000000..620ef8f --- /dev/null +++ b/src/Downkyi.UI/Services/IStoragePicker.cs @@ -0,0 +1,23 @@ +namespace Downkyi.UI.Services; + +public interface IStoragePicker +{ + /// + /// 弹出选择文件夹弹窗 + /// + /// + /// + Task FolderPicker(string directory = ""); + + /// + /// 选择视频dialog + /// + /// + Task SelectVideoFileAsync(); + + /// + /// 选择多个视频dialog + /// + /// + Task> SelectMultiVideoFileAsync(); +} \ No newline at end of file diff --git a/src/Downkyi.UI/Services/IStorageService.cs b/src/Downkyi.UI/Services/IStorageService.cs new file mode 100644 index 0000000..3ee4904 --- /dev/null +++ b/src/Downkyi.UI/Services/IStorageService.cs @@ -0,0 +1,5 @@ +namespace Downkyi.UI.Services; + +public interface IStorageService +{ +} diff --git a/src/Downkyi.UI/Services/StorageService.cs b/src/Downkyi.UI/Services/StorageService.cs new file mode 100644 index 0000000..ecedc3b --- /dev/null +++ b/src/Downkyi.UI/Services/StorageService.cs @@ -0,0 +1,5 @@ +namespace Downkyi.UI.Services; + +public class StorageService : IStorageService +{ +} diff --git a/src/Downkyi.UI/ViewModels/DownloadManager/DownloadFinishedViewModel.cs b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadFinishedViewModel.cs new file mode 100644 index 0000000..ee62d74 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadFinishedViewModel.cs @@ -0,0 +1,12 @@ +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.DownloadManager; + +public class DownloadFinishedViewModel : ViewModelBase +{ + public const string Key = "DownloadManager_DownloadFinished"; + + public DownloadFinishedViewModel(BaseServices baseServices) : base(baseServices) + { + } +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/DownloadManager/DownloadManagerViewModel.cs b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadManagerViewModel.cs new file mode 100644 index 0000000..6cb0e21 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadManagerViewModel.cs @@ -0,0 +1,47 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.UI.Models; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.DownloadManager; + +public partial class DownloadManagerViewModel : ViewModelBase +{ + public const string Key = "DownloadManager"; + + #region 页面属性申明 + + [ObservableProperty] + private int _selectedTabId = -1; + + [ObservableProperty] + private List _tabHeaders = new(); + + #endregion + + public DownloadManagerViewModel(BaseServices baseServices) : base(baseServices) + { + #region 属性初始化 + + TabHeaders.Add(new TabHeader { Id = 0, Title = DictionaryResource.GetString("Downloading") }); + TabHeaders.Add(new TabHeader { Id = 1, Title = DictionaryResource.GetString("DownloadFinished") }); + + #endregion + } + + #region 命令申明 + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task BackwardAsync() + { + Dictionary parameter = new() + { + { "key", Key }, + }; + + await NavigationService.BackwardAsync(parameter); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/DownloadManager/DownloadingViewModel.cs b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadingViewModel.cs new file mode 100644 index 0000000..662d2f2 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/DownloadManager/DownloadingViewModel.cs @@ -0,0 +1,12 @@ +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.DownloadManager; + +public class DownloadingViewModel : ViewModelBase +{ + public const string Key = "DownloadManager_Downloading"; + + public DownloadingViewModel(BaseServices baseServices) : base(baseServices) + { + } +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/IndexViewModel.cs b/src/Downkyi.UI/ViewModels/IndexViewModel.cs new file mode 100644 index 0000000..c18747d --- /dev/null +++ b/src/Downkyi.UI/ViewModels/IndexViewModel.cs @@ -0,0 +1,188 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Bili; +using Downkyi.Core.Settings; +using Downkyi.Core.Settings.Models; +using Downkyi.Core.Storage; +using Downkyi.UI.Mvvm; +using Downkyi.UI.ViewModels.DownloadManager; +using Downkyi.UI.ViewModels.Login; +using Downkyi.UI.ViewModels.Settings; +using Downkyi.UI.ViewModels.Toolbox; +using Downkyi.UI.ViewModels.User; + +namespace Downkyi.UI.ViewModels; + +public partial class IndexViewModel : ViewModelBase +{ + public const string Key = "Index"; + + private const string defaultHeader = "avares://Downkyi/Assets/bili/default_header.jpg"; + + #region 页面属性申明 + + [ObservableProperty] + private bool _loginPanelVisibility = false; + + [ObservableProperty] + private string _header = defaultHeader; + + [ObservableProperty] + private string? _userName = null; + + [ObservableProperty] + private string _inputText = string.Empty; + + #endregion + + public IndexViewModel(BaseServices baseServices) : base(baseServices) + { + #region 属性初始化 + #endregion + } + + /// + /// 导航到页面时执行 + /// + /// + public override async void OnNavigatedTo(Dictionary? parameter) + { + base.OnNavigatedTo(parameter); + + await UpdateUserInfo(); + } + + #region 命令申明 + + /// + /// 进入登录页面 + /// + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task LoginAsync() + { + if (UserName == null || UserName == string.Empty) + { + await NavigationService.ForwardAsync(LoginViewModel.Key); + } + else + { + // 进入用户空间 + var userInfo = SettingsManager.GetInstance().GetUserInfo(); + if (userInfo != null && userInfo.Mid != -1) + { + Dictionary parameter = new() + { + { "key", Key }, + { "mid", userInfo.Mid }, + }; + await NavigationService.ForwardAsync(MySpaceViewModel.Key, parameter); + } + } + } + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private Task InputAsync() + { + EnterBili(); + + return Task.CompletedTask; + } + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task SettingsAsync() + { + await NavigationService.ForwardAsync(SettingsViewModel.Key); + } + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task DownloadManagerAsync() + { + await NavigationService.ForwardAsync(DownloadManagerViewModel.Key); + } + + /// + /// 进入工具箱页面 + /// + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task ToolboxAsync() + { + await NavigationService.ForwardAsync(ToolboxViewModel.Key); + } + + #endregion + + #region 业务逻辑 + + private void EnterBili() + { + NotificationEvent.Publish("Enter Bili!"); + } + + private async Task UpdateUserInfo() + { + LoginPanelVisibility = false; + + // 检查本地是否存在login文件,没有则说明未登录 + if (!File.Exists(StorageManager.GetLogin())) + { + LoginPanelVisibility = true; + Header = defaultHeader; + UserName = null; + return; + } + + await Task.Run(() => + { + // 获取用户信息 + var userInfo = BiliLocator.Login.GetNavigationInfo(); + if (userInfo != null) + { + SettingsManager.GetInstance().SetUserInfo(new UserInfoSettings + { + Mid = userInfo.Mid, + Name = userInfo.Name, + IsLogin = userInfo.IsLogin, + IsVip = userInfo.VipStatus == 1 + }); + } + else + { + SettingsManager.GetInstance().SetUserInfo(new UserInfoSettings + { + Mid = -1, + Name = "", + IsLogin = false, + IsVip = false + }); + } + + // + LoginPanelVisibility = true; + + // 设置头像和用户名 + if (userInfo != null) + { + if (userInfo.Header != null) + { + Header = userInfo.Header; + } + else + { + Header = defaultHeader; + } + UserName = userInfo.Name; + } + else + { + Header = defaultHeader; + UserName = null; + } + }); + + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Login/CookiesViewModel.cs b/src/Downkyi.UI/ViewModels/Login/CookiesViewModel.cs new file mode 100644 index 0000000..0e41bdf --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Login/CookiesViewModel.cs @@ -0,0 +1,104 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Bili.Web; +using Downkyi.Core.Log; +using Downkyi.UI.Mvvm; +using System.Collections.ObjectModel; +using System.Net; + +namespace Downkyi.UI.ViewModels.Login; + +public partial class CookiesViewModel : ViewModelBase +{ + public const string Key = "Login_Cookies"; + + private CookieContainer? _cookieContainer; + + private bool _isParse; + + protected string LoginSuccessful = string.Empty; + + protected string LoginFailed = string.Empty; + + #region 页面属性申明 + + [ObservableProperty] + private string _cookiesStr = string.Empty; + + [ObservableProperty] + private ObservableCollection _cookies = new(); + + #endregion + + public CookiesViewModel(BaseServices baseServices) : base(baseServices) + { + LoginSuccessful = DictionaryResource.GetString("CookiesLoginSuccessful"); + LoginFailed = DictionaryResource.GetString("CookiesLoginFailed"); + } + + #region 命令申明 + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private Task TextChangedAsync() + { + if (_isParse) { return Task.CompletedTask; } + + _isParse = true; + + try + { + _cookieContainer = Core.Bili.Cookies.ParseCookieByString(CookiesStr); + var cookies = Core.Bili.Cookies.GetAllCookies(_cookieContainer); + + Cookies.Clear(); + foreach (var cookie in cookies) + { + Cookies.Add(new Models.Cookie { Key = cookie.Name, Value = cookie.Value }); + } + } + catch (Exception) { } + + _isParse = false; + return Task.CompletedTask; + } + + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private Task SaveCookiesAsync() + { + if (_cookieContainer == null) + { + return Task.CompletedTask; + } + + // 保存登录信息 + try + { + bool isSucceed = LoginHelper.SaveLoginInfoCookies(_cookieContainer); + if (!isSucceed) + { + Log.Logger.Error($"{Key}: {LoginFailed}"); + NotificationEvent.Publish(LoginFailed); + + return Task.CompletedTask; + } + } + catch (Exception e) + { + Log.Logger.Error(e); + NotificationEvent.Publish(LoginFailed); + + return Task.CompletedTask; + } + + // 发送通知 + NotificationEvent.Publish(LoginSuccessful); + Log.Logger.Info($"{Key}: {LoginSuccessful}"); + + BroadcastEvent.Send("cookiesLogin", "loginSuccessful"); + return Task.CompletedTask; + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Login/LoginViewModel.cs b/src/Downkyi.UI/ViewModels/Login/LoginViewModel.cs new file mode 100644 index 0000000..b94a647 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Login/LoginViewModel.cs @@ -0,0 +1,70 @@ +using CommunityToolkit.Mvvm.Input; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Login; + +public partial class LoginViewModel : ViewModelBase +{ + public const string Key = "Login"; + + private bool _isBackward = false; + + private bool _result = false; + + public LoginViewModel(BaseServices baseServices) : base(baseServices) + { + BroadcastEvent.Receive("qrcodeLogin", + new Action((obj) => + { + if (_isBackward) { return; } + + string? value = obj as string; + if (value == "loginSuccessful") + { + _result = true; + _ = BackwardAsync(); + } + if (value == "getLoginUrlFailed") + { + _result = false; + _ = BackwardAsync(); + } + }) + ); + + BroadcastEvent.Receive("cookiesLogin", + new Action((obj) => + { + if (_isBackward) { return; } + + string? value = obj as string; + if (value == "loginSuccessful") + { + _result = true; + _ = BackwardAsync(); + } + }) + ); + } + + #region 命令申明 + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task BackwardAsync() + { + Dictionary parameter = new() + { + { "key", Key }, + { "result", _result }, + }; + + await NavigationService.BackwardAsync(parameter); + + BroadcastEvent.Send("login", "backward"); + + _isBackward = true; + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Login/QRCodeViewModel.cs b/src/Downkyi.UI/ViewModels/Login/QRCodeViewModel.cs new file mode 100644 index 0000000..30fbe48 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Login/QRCodeViewModel.cs @@ -0,0 +1,197 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Bili; +using Downkyi.Core.Bili.Web; +using Downkyi.Core.Log; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Login; + +public partial class QRCodeViewModel : ViewModelBase +{ + public const string Key = "Login_QRCode"; + + private CancellationTokenSource? tokenSource; + + protected Action? _QRCodeAction; + + protected Action? _loginAction; + + protected string GetLoginUrlFailed = string.Empty; + + protected string LoginKeyError = string.Empty; + + protected string LoginTimeOut = string.Empty; + + protected string LoginSuccessful = string.Empty; + + protected string LoginFailed = string.Empty; + + #region 页面属性申明 + + [ObservableProperty] + private double _QRCodeOpacity; + + [ObservableProperty] + private bool _QRCodeStatus; + + #endregion + + public QRCodeViewModel(BaseServices baseServices) : base(baseServices) + { + GetLoginUrlFailed = DictionaryResource.GetString("GetLoginUrlFailed"); + LoginKeyError = DictionaryResource.GetString("LoginKeyError"); + LoginTimeOut = DictionaryResource.GetString("LoginTimeOut"); + LoginSuccessful = DictionaryResource.GetString("LoginSuccessful"); + LoginFailed = DictionaryResource.GetString("LoginFailed"); + } + + #region 命令申明 + + [RelayCommand] + private async Task OnLoaded() + { + // 初始化状态 + InitStatus(); + + await Task.Run(Login, (tokenSource = new CancellationTokenSource()).Token); + } + + [RelayCommand] + private void OnUnloaded() + { + // 初始化状态 + InitStatus(); + + // 结束任务 + tokenSource?.Cancel(); + } + + #endregion + + #region 业务逻辑 + + /// + /// 登录 + /// + private async Task Login() + { + try + { + var qrcode = BiliLocator.Login.GetQRCodeUrl(); + if (qrcode == null) + { + OnUnloaded(); + BroadcastEvent.Send("qrcodeLogin", "getLoginUrlFailed"); + NotificationEvent.Publish(GetLoginUrlFailed); + return; + } + + // 传递给主项目生成二维码 + _QRCodeAction?.Invoke(qrcode.Item1); + + Log.Logger.Debug($"{Key}: {qrcode.Item1}"); + + // poll + await GetLoginStatus(qrcode.Item2); + } + catch (Exception e) + { + Log.Logger.Error(e); + } + } + + /// + /// 循环查询登录状态 + /// + /// + private async Task GetLoginStatus(string oauthKey) + { + CancellationToken cancellationToken = tokenSource!.Token; + while (true) + { + await Task.Delay(1000); + var loginStatus = BiliLocator.Login.PollQRCode(oauthKey); + if (loginStatus == null) { continue; } + + switch (loginStatus.Code) + { + case 0: // 扫码登录成功 + + // 发送通知 + NotificationEvent.Publish(LoginSuccessful); + Log.Logger.Info($"{Key}: {LoginSuccessful}"); + + // 保存登录信息 + try + { + bool isSucceed = LoginHelper.SaveLoginInfoCookies(loginStatus.Url); + if (!isSucceed) + { + NotificationEvent.Publish(LoginFailed); + Log.Logger.Error($"{Key}: {LoginFailed}"); + } + } + catch (Exception e) + { + Log.Logger.Error(e); + NotificationEvent.Publish(LoginFailed); + } + + // 取消任务 + await Task.Delay(3000); + + OnUnloaded(); + BroadcastEvent.Send("qrcodeLogin", "loginSuccessful"); + + break; + case 86038: // 二维码已失效 + // 发送通知 + NotificationEvent.Publish(LoginTimeOut); + Log.Logger.Info($"{Key}: {LoginTimeOut}"); + // 取消任务 + tokenSource.Cancel(); + // 创建新任务 + await Task.Run(Login, (tokenSource = new CancellationTokenSource()).Token); + break; + case 86090: // 二维码已扫码未确认 + QRCodeOpacity = 0.1; + QRCodeStatus = true; + break; + case 86101: // 未扫码 + break; + } + + BroadcastEvent.Receive("login", + new Action((obj) => + { + string? v = obj as string; + if (v == "backward") + { + // 取消任务 + tokenSource.Cancel(); + } + }) + ); + + // 判断是否该结束线程,若为true,跳出while循环 + if (cancellationToken.IsCancellationRequested) + { + Log.Logger.Debug($"{Key}: 登录操作结束"); + break; + } + } + } + + /// + /// 初始化状态 + /// + protected virtual void InitStatus() + { + QRCodeOpacity = 1; + QRCodeStatus = false; + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Settings/AboutViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/AboutViewModel.cs new file mode 100644 index 0000000..309e455 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Settings/AboutViewModel.cs @@ -0,0 +1,115 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Settings; +using Downkyi.Core.Settings.Enum; +using Downkyi.UI.Models; +using Downkyi.UI.Mvvm; +using System.Diagnostics; + +namespace Downkyi.UI.ViewModels.Settings; + +public partial class AboutViewModel : BaseSettingsViewModel +{ + public const string Key = "Settings_About"; + + #region 页面属性申明 + + [ObservableProperty] + private string _appName = string.Empty; + + [ObservableProperty] + private string _appVersion = string.Empty; + + [ObservableProperty] + private bool _isReceiveBetaVersion; + + [ObservableProperty] + private bool _isAutoUpdateWhenLaunch; + + [ObservableProperty] + private List _thirdParties = new(); + + #endregion + + public AboutViewModel(BaseServices baseServices) : base(baseServices) + { + TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated"); + TipSettingFailed = DictionaryResource.GetString("TipSettingFailed"); + } + + #region 命令申明 + + /// + /// 加载页面时执行 + /// + [RelayCommand] + private void OnLoaded() + { + IsOnNavigatedTo = true; + + // 是否接收测试版更新 + var isReceiveBetaVersion = SettingsManager.GetInstance().IsReceiveBetaVersion(); + IsReceiveBetaVersion = isReceiveBetaVersion == AllowStatus.YES; + + // 是否在启动时自动检查更新 + var isAutoUpdateWhenLaunch = SettingsManager.GetInstance().GetAutoUpdateWhenLaunch(); + IsAutoUpdateWhenLaunch = isAutoUpdateWhenLaunch == AllowStatus.YES; + + IsOnNavigatedTo = false; + } + + /// + /// 访问主页事件 + /// + [RelayCommand] + private void VisitHomepage() + { + Process.Start(new ProcessStartInfo("https://github.com/leiurayer/downkyi") { UseShellExecute = true }); + } + + /// + /// 检查更新事件 + /// + [RelayCommand] + private void CheckUpdate() + { + // TODO + NotificationEvent.Publish("请前往主页下载最新版~"); + } + + /// + /// 意见反馈事件 + /// + [RelayCommand] + private void Feedback() + { + Process.Start(new ProcessStartInfo("https://github.com/leiurayer/downkyi/issues") { UseShellExecute = true }); + } + + /// + /// 是否接收测试版更新事件 + /// + [RelayCommand] + private void ReceiveBetaVersion() + { + AllowStatus isReceiveBetaVersion = IsReceiveBetaVersion ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().IsReceiveBetaVersion(isReceiveBetaVersion); + PublishTip(Key, isSucceed); + } + + /// + /// 是否在启动时自动检查更新事件 + /// + [RelayCommand] + private void AutoUpdateWhenLaunch() + { + AllowStatus isAutoUpdateWhenLaunch = IsAutoUpdateWhenLaunch ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().SetAutoUpdateWhenLaunch(isAutoUpdateWhenLaunch); + PublishTip(Key, isSucceed); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Settings/BaseSettingsViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/BaseSettingsViewModel.cs new file mode 100644 index 0000000..7caea0d --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Settings/BaseSettingsViewModel.cs @@ -0,0 +1,43 @@ +using Downkyi.Core.Log; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Settings; + +public class BaseSettingsViewModel : ViewModelBase +{ + protected bool IsOnNavigatedTo; + + protected string TipSettingUpdated = string.Empty; + + protected string TipSettingFailed = string.Empty; + + public BaseSettingsViewModel(BaseServices baseServices) : base(baseServices) + { + } + + #region UI逻辑 + + /// + /// 发送需要显示的tip + /// + /// + protected void PublishTip(string key, bool isSucceed) + { + if (IsOnNavigatedTo) { return; } + + // 发送通知 + if (isSucceed) + { + NotificationEvent.Publish(TipSettingUpdated); + Log.Logger.Info($"{key}: {TipSettingUpdated}"); + } + else + { + NotificationEvent.Publish(TipSettingFailed); + Log.Logger.Info($"{key}: {TipSettingFailed}"); + } + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Settings/BasicViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/BasicViewModel.cs new file mode 100644 index 0000000..05646ef --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Settings/BasicViewModel.cs @@ -0,0 +1,189 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Settings; +using Downkyi.Core.Settings.Enum; +using Downkyi.UI.Models; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Settings; + +public partial class BasicViewModel : BaseSettingsViewModel +{ + public const string Key = "Settings_Basic"; + + #region 页面属性申明 + + [ObservableProperty] + private bool _none; + + [ObservableProperty] + private bool _closeApp; + + [ObservableProperty] + private bool _closeSystem; + + [ObservableProperty] + private bool _isListenClipboard; + + [ObservableProperty] + private bool _isAutoParseVideo; + + [ObservableProperty] + private List _parseScopes = new(); + + [ObservableProperty] + private ParseScopeDisplay? _selectedParseScope; + + [ObservableProperty] + private bool _isAutoDownloadAll; + + #endregion + + public BasicViewModel(BaseServices baseServices) : base(baseServices) + { + TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated"); + TipSettingFailed = DictionaryResource.GetString("TipSettingFailed"); + + #region 属性初始化 + + // 解析范围 + ParseScopes.Add(new ParseScopeDisplay { Name = DictionaryResource.GetString("ParseNone"), ParseScope = ParseScope.NONE }); + ParseScopes.Add(new ParseScopeDisplay { Name = DictionaryResource.GetString("ParseSelectedItem"), ParseScope = ParseScope.SELECTED_ITEM }); + ParseScopes.Add(new ParseScopeDisplay { Name = DictionaryResource.GetString("ParseCurrentSection"), ParseScope = ParseScope.CURRENT_SECTION }); + ParseScopes.Add(new ParseScopeDisplay { Name = DictionaryResource.GetString("ParseAll"), ParseScope = ParseScope.ALL }); + + #endregion + } + + #region 命令申明 + + /// + /// 加载页面时执行 + /// + [RelayCommand] + private void OnLoaded() + { + IsOnNavigatedTo = true; + + // 下载完成后的操作 + AfterDownloadOperation afterDownload = SettingsManager.GetInstance().GetAfterDownloadOperation(); + SetAfterDownloadOperation(afterDownload); + + // 是否监听剪贴板 + AllowStatus isListenClipboard = SettingsManager.GetInstance().IsListenClipboard(); + IsListenClipboard = isListenClipboard == AllowStatus.YES; + + // 是否自动解析视频 + AllowStatus isAutoParseVideo = SettingsManager.GetInstance().IsAutoParseVideo(); + IsAutoParseVideo = isAutoParseVideo == AllowStatus.YES; + + // 解析范围 + ParseScope parseScope = SettingsManager.GetInstance().GetParseScope(); + SelectedParseScope = ParseScopes.FirstOrDefault(t => { return t.ParseScope == parseScope; }); + + // 解析后是否自动下载解析视频 + AllowStatus isAutoDownloadAll = SettingsManager.GetInstance().IsAutoDownloadAll(); + IsAutoDownloadAll = isAutoDownloadAll == AllowStatus.YES; + + IsOnNavigatedTo = false; + } + + /// + /// 下载完成后的操作事件 + /// + /// + [RelayCommand] + private void AfterDownload(object parameter) + { + if (parameter is not string) { return; } + + var afterDownload = parameter switch + { + "None" => AfterDownloadOperation.NONE, + "CloseApp" => AfterDownloadOperation.CLOSE_APP, + "CloseSystem" => AfterDownloadOperation.CLOSE_SYSTEM, + _ => AfterDownloadOperation.NONE, + }; + bool isSucceed = SettingsManager.GetInstance().SetAfterDownloadOperation(afterDownload); + PublishTip(Key, isSucceed); + } + + /// + /// 是否监听剪贴板事件 + /// + [RelayCommand] + private void ListenClipboard() + { + AllowStatus isListenClipboard = IsListenClipboard ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().IsListenClipboard(isListenClipboard); + PublishTip(Key, isSucceed); + } + + /// + /// 是否自动解析视频 + /// + [RelayCommand] + private void AutoParseVideo() + { + AllowStatus isAutoParseVideo = IsAutoParseVideo ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().IsAutoParseVideo(isAutoParseVideo); + PublishTip(Key, isSucceed); + } + + /// + /// 解析范围事件 + /// + /// + [RelayCommand] + private void SetParseScopes() + { + //if (parameter is not ParseScopeDisplay parseScope) { return; } + + bool isSucceed = SettingsManager.GetInstance().SetParseScope(SelectedParseScope!.ParseScope); + PublishTip(Key, isSucceed); + } + + /// + /// 解析后是否自动下载解析视频 + /// + [RelayCommand] + private void AutoDownloadAll() + { + AllowStatus isAutoDownloadAll = IsAutoDownloadAll ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().IsAutoDownloadAll(isAutoDownloadAll); + PublishTip(Key, isSucceed); + } + + #endregion + + + #region UI逻辑 + + /// + /// 设置下载完成后的操作 + /// + /// + private void SetAfterDownloadOperation(AfterDownloadOperation afterDownload) + { + switch (afterDownload) + { + case AfterDownloadOperation.NONE: + None = true; + break; + case AfterDownloadOperation.OPEN_FOLDER: + break; + case AfterDownloadOperation.CLOSE_APP: + CloseApp = true; + break; + case AfterDownloadOperation.CLOSE_SYSTEM: + CloseSystem = true; + break; + } + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Settings/DanmakuViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/DanmakuViewModel.cs new file mode 100644 index 0000000..7ddd10b --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Settings/DanmakuViewModel.cs @@ -0,0 +1,265 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Settings; +using Downkyi.Core.Settings.Enum; +using Downkyi.Core.Utils.Validator; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Settings; + +public partial class DanmakuViewModel : BaseSettingsViewModel +{ + public const string Key = "Settings_Danmaku"; + + #region 页面属性申明 + + [ObservableProperty] + private bool _topFilter; + + [ObservableProperty] + private bool _bottomFilter; + + [ObservableProperty] + private bool _scrollFilter; + + [ObservableProperty] + private int _screenWidth; + + [ObservableProperty] + private int _screenHeight; + + [ObservableProperty] + private List _fonts = new(); + + [ObservableProperty] + private string _selectedFont = string.Empty; + + [ObservableProperty] + private int _fontSize; + + [ObservableProperty] + private int _lineCount; + + [ObservableProperty] + private bool _sync; + + [ObservableProperty] + private bool _async; + + #endregion + + public DanmakuViewModel(BaseServices baseServices) : base(baseServices) + { + TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated"); + TipSettingFailed = DictionaryResource.GetString("TipSettingFailed"); + } + + #region 命令申明 + + /// + /// 加载页面时执行 + /// + [RelayCommand] + private void OnLoaded() + { + IsOnNavigatedTo = true; + + // 屏蔽顶部弹幕 + AllowStatus danmakuTopFilter = SettingsManager.GetInstance().GetDanmakuTopFilter(); + TopFilter = danmakuTopFilter == AllowStatus.YES; + + // 屏蔽底部弹幕 + AllowStatus danmakuBottomFilter = SettingsManager.GetInstance().GetDanmakuBottomFilter(); + BottomFilter = danmakuBottomFilter == AllowStatus.YES; + + // 屏蔽滚动弹幕 + AllowStatus danmakuScrollFilter = SettingsManager.GetInstance().GetDanmakuScrollFilter(); + ScrollFilter = danmakuScrollFilter == AllowStatus.YES; + + // 分辨率-宽 + ScreenWidth = SettingsManager.GetInstance().GetDanmakuScreenWidth(); + + // 分辨率-高 + ScreenHeight = SettingsManager.GetInstance().GetDanmakuScreenHeight(); + + // 弹幕字体 + string danmakuFont = SettingsManager.GetInstance().GetDanmakuFontName(); + if (danmakuFont != null && Fonts.Contains(danmakuFont)) + { + // 只有系统中存在当前设置的字体,才能显示 + SelectedFont = danmakuFont; + } + + // 弹幕字体大小 + FontSize = SettingsManager.GetInstance().GetDanmakuFontSize(); + + // 弹幕限制行数 + LineCount = SettingsManager.GetInstance().GetDanmakuLineCount(); + + // 弹幕布局算法 + DanmakuLayoutAlgorithm layoutAlgorithm = SettingsManager.GetInstance().GetDanmakuLayoutAlgorithm(); + SetLayoutAlgorithm(layoutAlgorithm); + + IsOnNavigatedTo = false; + } + + /// + /// 屏蔽顶部弹幕事件 + /// + [RelayCommand] + private void SetTopFilter() + { + AllowStatus isTopFilter = TopFilter ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().SetDanmakuTopFilter(isTopFilter); + PublishTip(Key, isSucceed); + } + + /// + /// 屏蔽底部弹幕事件 + /// + [RelayCommand] + private void SetBottomFilter() + { + AllowStatus isBottomFilter = BottomFilter ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().SetDanmakuBottomFilter(isBottomFilter); + PublishTip(Key, isSucceed); + } + + /// + /// 屏蔽滚动弹幕事件 + /// + [RelayCommand] + private void SetScrollFilter() + { + AllowStatus isScrollFilter = ScrollFilter ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().SetDanmakuScrollFilter(isScrollFilter); + PublishTip(Key, isSucceed); + } + + /// + /// 设置分辨率-宽事件 + /// + /// + [RelayCommand] + private void SetScreenWidth(string parameter) + { + int width = (int)Number.GetInt(parameter); + ScreenWidth = width; + + bool isSucceed = SettingsManager.GetInstance().SetDanmakuScreenWidth(ScreenWidth); + PublishTip(Key, isSucceed); + } + + /// + /// 设置分辨率-高事件 + /// + /// + [RelayCommand] + private void SetScreenHeight(string parameter) + { + int height = (int)Number.GetInt(parameter); + ScreenHeight = height; + + bool isSucceed = SettingsManager.GetInstance().SetDanmakuScreenHeight(ScreenHeight); + PublishTip(Key, isSucceed); + } + + /// + /// 弹幕字体选择事件 + /// + /// + [RelayCommand] + private void FontSelected(string parameter) + { + bool isSucceed = SettingsManager.GetInstance().SetDanmakuFontName(parameter); + PublishTip(Key, isSucceed); + } + + /// + /// 弹幕字体大小事件 + /// + /// + [RelayCommand] + private void SetFontSize(string parameter) + { + int fontSize = (int)Number.GetInt(parameter); + FontSize = fontSize; + + bool isSucceed = SettingsManager.GetInstance().SetDanmakuFontSize(FontSize); + PublishTip(Key, isSucceed); + } + + /// + /// 弹幕限制行数事件 + /// + /// + [RelayCommand] + private void SetLineCount(string parameter) + { + int lineCount = (int)Number.GetInt(parameter); + LineCount = lineCount; + + bool isSucceed = SettingsManager.GetInstance().SetDanmakuLineCount(LineCount); + PublishTip(Key, isSucceed); + } + + /// + /// 弹幕布局算法事件 + /// + /// + [RelayCommand] + private void SetLayoutAlgorithm(string parameter) + { + DanmakuLayoutAlgorithm layoutAlgorithm; + switch (parameter) + { + case "Sync": + layoutAlgorithm = DanmakuLayoutAlgorithm.SYNC; + break; + case "Async": + layoutAlgorithm = DanmakuLayoutAlgorithm.ASYNC; + break; + default: + layoutAlgorithm = DanmakuLayoutAlgorithm.SYNC; + break; + } + + bool isSucceed = SettingsManager.GetInstance().SetDanmakuLayoutAlgorithm(layoutAlgorithm); + PublishTip(Key, isSucceed); + + if (isSucceed) + { + SetLayoutAlgorithm(layoutAlgorithm); + } + } + + #endregion + + /// + /// 设置弹幕同步算法 + /// + /// + private void SetLayoutAlgorithm(DanmakuLayoutAlgorithm layoutAlgorithm) + { + switch (layoutAlgorithm) + { + case DanmakuLayoutAlgorithm.SYNC: + Sync = true; + Async = false; + break; + case DanmakuLayoutAlgorithm.ASYNC: + Sync = false; + Async = true; + break; + case DanmakuLayoutAlgorithm.NONE: + Sync = false; + Async = false; + break; + default: + break; + } + } +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Settings/NetworkViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/NetworkViewModel.cs new file mode 100644 index 0000000..acd18b1 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Settings/NetworkViewModel.cs @@ -0,0 +1,518 @@ +using Aria2cNet.Server; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Settings; +using Downkyi.Core.Settings.Enum; +using Downkyi.Core.Utils.Validator; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Settings; + +public partial class NetworkViewModel : BaseSettingsViewModel +{ + public const string Key = "Settings_Network"; + + #region 页面属性申明 + + [ObservableProperty] + private bool _useSSL; + + [ObservableProperty] + private string _userAgent = string.Empty; + + [ObservableProperty] + private bool _builtin; + + [ObservableProperty] + private bool _aria2c; + + [ObservableProperty] + private bool _customAria2c; + + [ObservableProperty] + private List _maxCurrentDownloads = new(); + + [ObservableProperty] + private int _selectedMaxCurrentDownload; + + [ObservableProperty] + private List _splits = new(); + + [ObservableProperty] + private int _selectedSplit; + + [ObservableProperty] + private bool _isHttpProxy; + + [ObservableProperty] + private string _httpProxy = string.Empty; + + [ObservableProperty] + private int _httpProxyPort; + + [ObservableProperty] + private string _ariaHost = string.Empty; + + [ObservableProperty] + private int _ariaListenPort; + + [ObservableProperty] + private string _ariaToken = string.Empty; + + [ObservableProperty] + private List _ariaLogLevels = new(); + + [ObservableProperty] + private string _selectedAriaLogLevel = string.Empty; + + [ObservableProperty] + private List _ariaMaxConcurrentDownloads = new(); + + [ObservableProperty] + private int _selectedAriaMaxConcurrentDownload; + + [ObservableProperty] + private List _ariaSplits = new(); + + [ObservableProperty] + private int _selectedAriaSplit; + + [ObservableProperty] + private int _ariaMaxOverallDownloadLimit; + + [ObservableProperty] + private int _ariaMaxDownloadLimit; + + [ObservableProperty] + private bool _isAriaHttpProxy; + + [ObservableProperty] + private string _ariaHttpProxy = string.Empty; + + [ObservableProperty] + private int _ariaHttpProxyPort; + + [ObservableProperty] + private List _ariaFileAllocations = new(); + + [ObservableProperty] + private string _selectedAriaFileAllocation = string.Empty; + + #endregion + + public NetworkViewModel(BaseServices baseServices) : base(baseServices) + { + TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated"); + TipSettingFailed = DictionaryResource.GetString("TipSettingFailed"); + + #region 属性初始化 + + // builtin同时下载数 + for (int i = 1; i <= 10; i++) { MaxCurrentDownloads.Add(i); } + + // builtin最大线程数 + for (int i = 1; i <= 10; i++) { Splits.Add(i); } + + // Aria的日志等级 + AriaLogLevels = new List + { + "DEBUG", + "INFO", + "NOTICE", + "WARN", + "ERROR" + }; + + // Aria同时下载数 + for (int i = 1; i <= 10; i++) { AriaMaxConcurrentDownloads.Add(i); } + + // Aria最大线程数 + for (int i = 1; i <= 10; i++) { AriaSplits.Add(i); } + + // Aria文件预分配 + AriaFileAllocations = new List + { + "NONE", + "PREALLOC", + "FALLOC" + }; + + #endregion + } + + #region 命令申明 + + /// + /// 加载页面时执行 + /// + [RelayCommand] + private void OnLoaded() + { + IsOnNavigatedTo = true; + + // 启用https + AllowStatus useSSL = SettingsManager.GetInstance().UseSSL(); + UseSSL = useSSL == AllowStatus.YES; + + // UserAgent + UserAgent = SettingsManager.GetInstance().GetUserAgent(); + + // 选择下载器 + var downloader = SettingsManager.GetInstance().GetDownloader(); + switch (downloader) + { + case Downloader.NOT_SET: + break; + case Downloader.BUILT_IN: + Builtin = true; + break; + case Downloader.ARIA: + Aria2c = true; + break; + case Downloader.CUSTOM_ARIA: + CustomAria2c = true; + break; + } + + // builtin同时下载数 + SelectedMaxCurrentDownload = SettingsManager.GetInstance().GetMaxCurrentDownloads(); + + // builtin最大线程数 + SelectedSplit = SettingsManager.GetInstance().GetSplit(); + + // 是否开启builtin http代理 + AllowStatus isHttpProxy = SettingsManager.GetInstance().IsHttpProxy(); + IsHttpProxy = isHttpProxy == AllowStatus.YES; + + // builtin的http代理的地址 + HttpProxy = SettingsManager.GetInstance().GetHttpProxy(); + + // builtin的http代理的端口 + HttpProxyPort = SettingsManager.GetInstance().GetHttpProxyListenPort(); + + // Aria服务器host + AriaHost = SettingsManager.GetInstance().GetAriaHost(); + + // Aria服务器端口 + AriaListenPort = SettingsManager.GetInstance().GetAriaListenPort(); + + // Aria服务器Token + AriaToken = SettingsManager.GetInstance().GetAriaToken(); + + // Aria的日志等级 + AriaConfigLogLevel ariaLogLevel = SettingsManager.GetInstance().GetAriaLogLevel(); + SelectedAriaLogLevel = ariaLogLevel.ToString("G"); + + // Aria同时下载数 + SelectedAriaMaxConcurrentDownload = SettingsManager.GetInstance().GetMaxCurrentDownloads(); + + // Aria最大线程数 + SelectedAriaSplit = SettingsManager.GetInstance().GetAriaSplit(); + + // Aria下载速度限制 + AriaMaxOverallDownloadLimit = SettingsManager.GetInstance().GetAriaMaxOverallDownloadLimit(); + + // Aria下载单文件速度限制 + AriaMaxDownloadLimit = SettingsManager.GetInstance().GetAriaMaxDownloadLimit(); + + // 是否开启Aria http代理 + AllowStatus isAriaHttpProxy = SettingsManager.GetInstance().IsAriaHttpProxy(); + IsAriaHttpProxy = isAriaHttpProxy == AllowStatus.YES; + + // Aria的http代理的地址 + AriaHttpProxy = SettingsManager.GetInstance().GetAriaHttpProxy(); + + // Aria的http代理的端口 + AriaHttpProxyPort = SettingsManager.GetInstance().GetAriaHttpProxyListenPort(); + + // Aria文件预分配 + AriaConfigFileAllocation ariaFileAllocation = SettingsManager.GetInstance().GetAriaFileAllocation(); + SelectedAriaFileAllocation = ariaFileAllocation.ToString("G"); + + IsOnNavigatedTo = false; + } + + /// + /// 是否启用https事件 + /// + [RelayCommand] + private void SetUseSSL() + { + AllowStatus useSSL = UseSSL ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().UseSSL(useSSL); + PublishTip(Key, isSucceed); + } + + /// + /// 设置UserAgent事件 + /// + [RelayCommand] + private void SetUserAgent() + { + bool isSucceed = SettingsManager.GetInstance().SetUserAgent(UserAgent); + PublishTip(Key, isSucceed); + } + + /// + /// 下载器选择事件 + /// + /// + [RelayCommand] + private void SelectDownloader(string parameter) + { + var downloader = parameter switch + { + "Builtin" => Downloader.BUILT_IN, + "Aria2c" => Downloader.ARIA, + "CustomAria2c" => Downloader.CUSTOM_ARIA, + _ => SettingsManager.GetInstance().GetDownloader(), + }; + bool isSucceed = SettingsManager.GetInstance().SetDownloader(downloader); + PublishTip(Key, isSucceed); + + // 弹窗提示是否重启程序 + // TODO + + //AlertService alertService = new AlertService(dialogService); + //ButtonResult result = alertService.ShowInfo(DictionaryResource.GetString("ConfirmReboot")); + //if (result == ButtonResult.OK) + //{ + // System.Windows.Application.Current.Shutdown(); + // System.Diagnostics.Process.Start(System.Reflection.Assembly.GetExecutingAssembly().Location); + //} + } + + /// + /// builtin同时下载数事件 + /// + /// + [RelayCommand] + private void SetMaxCurrentDownloads(object parameter) + { + SelectedMaxCurrentDownload = (int)parameter; + + bool isSucceed = SettingsManager.GetInstance().SetMaxCurrentDownloads(SelectedMaxCurrentDownload); + PublishTip(Key, isSucceed); + } + + /// + /// builtin最大线程数事件 + /// + /// + [RelayCommand] + private void SetSplits(object parameter) + { + SelectedSplit = (int)parameter; + + bool isSucceed = SettingsManager.GetInstance().SetSplit(SelectedSplit); + PublishTip(Key, isSucceed); + } + + /// + /// 是否开启builtin http代理事件 + /// + [RelayCommand] + private void SetIsHttpProxy() + { + AllowStatus isHttpProxy = IsHttpProxy ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().IsHttpProxy(isHttpProxy); + PublishTip(Key, isSucceed); + } + + /// + /// builtin的http代理的地址事件 + /// + /// + [RelayCommand] + private void SetHttpProxy(string parameter) + { + bool isSucceed = SettingsManager.GetInstance().SetHttpProxy(parameter); + PublishTip(Key, isSucceed); + } + + /// + /// builtin的http代理的端口事件 + /// + /// + [RelayCommand] + private void SetHttpProxyPort(string parameter) + { + int httpProxyPort = (int)Number.GetInt(parameter); + HttpProxyPort = httpProxyPort; + + bool isSucceed = SettingsManager.GetInstance().SetHttpProxyListenPort(HttpProxyPort); + PublishTip(Key, isSucceed); + } + + /// + /// Aria服务器host事件 + /// + /// + [RelayCommand] + private void SetAriaHost(string parameter) + { + AriaHost = parameter; + bool isSucceed = SettingsManager.GetInstance().SetAriaHost(AriaHost); + PublishTip(Key, isSucceed); + } + + /// + /// Aria服务器端口事件 + /// + /// + [RelayCommand] + private void SetAriaListenPort(string parameter) + { + int listenPort = (int)Number.GetInt(parameter); + AriaListenPort = listenPort; + + bool isSucceed = SettingsManager.GetInstance().SetAriaListenPort(AriaListenPort); + PublishTip(Key, isSucceed); + } + + /// + /// Aria服务器token事件 + /// + /// + [RelayCommand] + private void SetAriaToken(string parameter) + { + AriaToken = parameter; + bool isSucceed = SettingsManager.GetInstance().SetAriaToken(AriaToken); + PublishTip(Key, isSucceed); + } + + /// + /// Aria的日志等级事件 + /// + /// + [RelayCommand] + private void SetAriaLogLevels(string parameter) + { + var ariaLogLevel = parameter switch + { + "DEBUG" => AriaConfigLogLevel.DEBUG, + "INFO" => AriaConfigLogLevel.INFO, + "NOTICE" => AriaConfigLogLevel.NOTICE, + "WARN" => AriaConfigLogLevel.WARN, + "ERROR" => AriaConfigLogLevel.ERROR, + _ => AriaConfigLogLevel.INFO, + }; + bool isSucceed = SettingsManager.GetInstance().SetAriaLogLevel(ariaLogLevel); + PublishTip(Key, isSucceed); + } + + /// + /// Aria同时下载数事件 + /// + /// + [RelayCommand] + private void SetAriaMaxConcurrentDownloads(object parameter) + { + SelectedAriaMaxConcurrentDownload = (int)parameter; + + bool isSucceed = SettingsManager.GetInstance().SetMaxCurrentDownloads(SelectedAriaMaxConcurrentDownload); + PublishTip(Key, isSucceed); + } + + /// + /// Aria最大线程数事件 + /// + /// + [RelayCommand] + private void SetAriaSplits(object parameter) + { + SelectedAriaSplit = (int)parameter; + + bool isSucceed = SettingsManager.GetInstance().SetAriaSplit(SelectedAriaSplit); + PublishTip(Key, isSucceed); + } + + /// + /// Aria下载速度限制事件 + /// + /// + [RelayCommand] + private void SetAriaMaxOverallDownloadLimit(string parameter) + { + int downloadLimit = (int)Number.GetInt(parameter); + AriaMaxOverallDownloadLimit = downloadLimit; + + bool isSucceed = SettingsManager.GetInstance().SetAriaMaxOverallDownloadLimit(AriaMaxOverallDownloadLimit); + PublishTip(Key, isSucceed); + } + + /// + /// Aria下载单文件速度限制事件 + /// + /// + [RelayCommand] + private void SetAriaMaxDownloadLimit(string parameter) + { + int downloadLimit = (int)Number.GetInt(parameter); + AriaMaxDownloadLimit = downloadLimit; + + bool isSucceed = SettingsManager.GetInstance().SetAriaMaxDownloadLimit(AriaMaxDownloadLimit); + PublishTip(Key, isSucceed); + } + + /// + /// 是否开启Aria http代理事件 + /// + [RelayCommand] + private void SetIsAriaHttpProxy() + { + AllowStatus isAriaHttpProxy = IsAriaHttpProxy ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().IsAriaHttpProxy(isAriaHttpProxy); + PublishTip(Key, isSucceed); + } + + /// + /// Aria的http代理的地址事件 + /// + /// + [RelayCommand] + private void SetAriaHttpProxy(string parameter) + { + bool isSucceed = SettingsManager.GetInstance().SetAriaHttpProxy(parameter); + PublishTip(Key, isSucceed); + } + + /// + /// Aria的http代理的端口事件 + /// + /// + [RelayCommand] + private void SetAriaHttpProxyPort(string parameter) + { + int httpProxyPort = (int)Number.GetInt(parameter); + AriaHttpProxyPort = httpProxyPort; + + bool isSucceed = SettingsManager.GetInstance().SetAriaHttpProxyListenPort(AriaHttpProxyPort); + PublishTip(Key, isSucceed); + } + + /// + /// Aria文件预分配事件 + /// + /// + [RelayCommand] + private void SetAriaFileAllocations(string parameter) + { + var ariaFileAllocation = parameter switch + { + "NONE" => AriaConfigFileAllocation.NONE, + "PREALLOC" => AriaConfigFileAllocation.PREALLOC, + "FALLOC" => AriaConfigFileAllocation.FALLOC, + _ => AriaConfigFileAllocation.PREALLOC, + }; + bool isSucceed = SettingsManager.GetInstance().SetAriaFileAllocation(ariaFileAllocation); + PublishTip(Key, isSucceed); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Settings/SettingsViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/SettingsViewModel.cs new file mode 100644 index 0000000..3e5df42 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Settings/SettingsViewModel.cs @@ -0,0 +1,50 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.UI.Models; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Settings; + +public partial class SettingsViewModel : ViewModelBase +{ + public const string Key = "Settings"; + + #region 页面属性申明 + + [ObservableProperty] + private int _selectedTabId = -1; + + [ObservableProperty] + private List _tabHeaders = new(); + + #endregion + + public SettingsViewModel(BaseServices baseServices) : base(baseServices) + { + #region 属性初始化 + + TabHeaders.Add(new TabHeader { Id = 0, Title = DictionaryResource.GetString("Basic") }); + TabHeaders.Add(new TabHeader { Id = 1, Title = DictionaryResource.GetString("Network") }); + TabHeaders.Add(new TabHeader { Id = 2, Title = DictionaryResource.GetString("Video") }); + TabHeaders.Add(new TabHeader { Id = 3, Title = DictionaryResource.GetString("SettingDanmaku") }); + TabHeaders.Add(new TabHeader { Id = 4, Title = DictionaryResource.GetString("About") }); + + #endregion + } + + #region 命令申明 + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task BackwardAsync() + { + Dictionary parameter = new() + { + { "key", Key }, + }; + + await NavigationService.BackwardAsync(parameter); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Settings/VideoViewModel.cs b/src/Downkyi.UI/ViewModels/Settings/VideoViewModel.cs new file mode 100644 index 0000000..6a41779 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Settings/VideoViewModel.cs @@ -0,0 +1,529 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Bili.Models; +using Downkyi.Core.Bili.Utils; +using Downkyi.Core.FileName; +using Downkyi.Core.Settings; +using Downkyi.Core.Settings.Enum; +using Downkyi.Core.Settings.Models; +using Downkyi.UI.Models; +using Downkyi.UI.Mvvm; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Downkyi.UI.ViewModels.Settings; + +public partial class VideoViewModel : BaseSettingsViewModel +{ + public const string Key = "Settings_Video"; + + #region 页面属性申明 + + [ObservableProperty] + private List _videoCodecs = new(); + + [ObservableProperty] + private Quality? _selectedVideoCodec = new(); + + [ObservableProperty] + private List _videoQualityList = new(); + + [ObservableProperty] + private Quality? _selectedVideoQuality = new(); + + [ObservableProperty] + private List _audioQualityList = new(); + + [ObservableProperty] + private Quality? _selectedAudioQuality = new(); + + [ObservableProperty] + private bool _isTranscodingFlvToMp4; + + [ObservableProperty] + private bool _isUseDefaultDirectory; + + [ObservableProperty] + private string _saveVideoDirectory = string.Empty; + + [ObservableProperty] + private bool _downloadAll; + + [ObservableProperty] + private bool _downloadAudio; + + [ObservableProperty] + private bool _downloadVideo; + + [ObservableProperty] + private bool _downloadDanmaku; + + [ObservableProperty] + private bool _downloadSubtitle; + + [ObservableProperty] + private bool _downloadCover; + + [ObservableProperty] + private ObservableCollection _selectedFileName = new(); + + [ObservableProperty] + private List _optionalFields = new(); + + [ObservableProperty] + private int _selectedOptionalField; + + [ObservableProperty] + private List _timeFormatList = new(); + + [ObservableProperty] + private string _selectedTimeFormat = string.Empty; + + [ObservableProperty] + private List _orderFormatList = new(); + + [ObservableProperty] + private OrderFormatDisplay? _selectedOrderFormat = new(); + + #endregion + + public VideoViewModel(BaseServices baseServices) : base(baseServices) + { + TipSettingUpdated = DictionaryResource.GetString("TipSettingUpdated"); + TipSettingFailed = DictionaryResource.GetString("TipSettingFailed"); + + #region 属性初始化 + + // 优先下载的视频编码 + VideoCodecs = QualityList.GetCodecIds(); + + // 优先下载画质 + VideoQualityList = QualityList.GetResolutions(); + + // 优先下载音质 + AudioQualityList = QualityList.GetAudioQualities(); + AudioQualityList[3].Id = AudioQualityList[3].Id + 1000; + AudioQualityList[4].Id = AudioQualityList[4].Id + 1000; + + // 文件命名中的序号格式 + OrderFormatList.Add(new OrderFormatDisplay { Id = Core.Settings.Enum.OrderFormat.NATURAL, Name = DictionaryResource.GetString("OrderFormatNatural") }); + OrderFormatList.Add(new OrderFormatDisplay { Id = Core.Settings.Enum.OrderFormat.LEADING_ZEROS, Name = DictionaryResource.GetString("OrderFormatLeadingZeros") }); + + // 文件命名格式 + SelectedFileName.CollectionChanged += new NotifyCollectionChangedEventHandler((sender, e) => + { + // 当前显示的命名格式part + var fileName = new List(); + foreach (var item in SelectedFileName) + { + fileName.Add(item.Id); + } + + bool isSucceed = SettingsManager.GetInstance().SetFileNameParts(fileName); + PublishTip(Key, isSucceed); + }); + + foreach (FileNamePart item in Enum.GetValues(typeof(FileNamePart))) + { + string display = DisplayFileNamePart(item); + OptionalFields.Add(new() { Id = item, Title = display }); + } + + SelectedOptionalField = -1; + + // 文件命名中的时间格式 + TimeFormatList.Add("yyyy-MM-dd"); + TimeFormatList.Add("yyyy.MM.dd"); + + #endregion + } + + #region 命令申明 + + /// + /// 加载页面时执行 + /// + [RelayCommand] + private void OnLoaded() + { + IsOnNavigatedTo = true; + + // 优先下载的视频编码 + int videoCodecs = SettingsManager.GetInstance().GetVideoCodecs(); + SelectedVideoCodec = VideoCodecs.FirstOrDefault(t => { return t.Id == videoCodecs; }); + + // 优先下载画质 + int quality = SettingsManager.GetInstance().GetQuality(); + SelectedVideoQuality = VideoQualityList.FirstOrDefault(t => { return t.Id == quality; }); + + // 优先下载音质 + int audioQuality = SettingsManager.GetInstance().GetAudioQuality(); + SelectedAudioQuality = AudioQualityList.FirstOrDefault(t => { return t.Id == audioQuality; }); + + // 是否下载flv视频后转码为mp4 + AllowStatus isTranscodingFlvToMp4 = SettingsManager.GetInstance().IsTranscodingFlvToMp4(); + IsTranscodingFlvToMp4 = isTranscodingFlvToMp4 == AllowStatus.YES; + + // 是否使用默认下载目录 + AllowStatus isUseSaveVideoRootPath = SettingsManager.GetInstance().IsUseSaveVideoRootPath(); + IsUseDefaultDirectory = isUseSaveVideoRootPath == AllowStatus.YES; + + // 默认下载目录 + SaveVideoDirectory = SettingsManager.GetInstance().GetSaveVideoRootPath(); + + // 下载内容 + VideoContentSettings videoContent = SettingsManager.GetInstance().GetVideoContent(); + + DownloadAudio = videoContent.DownloadAudio; + DownloadVideo = videoContent.DownloadVideo; + DownloadDanmaku = videoContent.DownloadDanmaku; + DownloadSubtitle = videoContent.DownloadSubtitle; + DownloadCover = videoContent.DownloadCover; + + if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover) + { + DownloadAll = true; + } + else + { + DownloadAll = false; + } + + // 文件命名格式 + List fileNameParts = SettingsManager.GetInstance().GetFileNameParts(); + SelectedFileName.Clear(); + foreach (FileNamePart item in fileNameParts) + { + int index; + if (SelectedFileName.Count == 0) { index = 0; } + else { index = SelectedFileName.Max(it => it.Index) + 1; } + + string display = DisplayFileNamePart(item); + SelectedFileName.Add(new FileNamePartDisplay + { + Index = index, + Id = item, + Title = display, + }); + } + + // 文件命名中的时间格式 + SelectedTimeFormat = SettingsManager.GetInstance().GetFileNamePartTimeFormat(); + + // 文件命名中的序号格式 + OrderFormat orderFormat = SettingsManager.GetInstance().GetOrderFormat(); + SelectedOrderFormat = OrderFormatList.FirstOrDefault(t => { return t.Id == orderFormat; }); + + IsOnNavigatedTo = false; + } + + /// + /// 优先下载的视频编码事件 + /// + /// + [RelayCommand] + private void SetVideoCodecs() + { + bool isSucceed = SettingsManager.GetInstance().SetVideoCodecs(SelectedVideoCodec!.Id); + PublishTip(Key, isSucceed); + } + + /// + /// 优先下载画质事件 + /// + /// + [RelayCommand] + private void SetVideoQuality() + { + bool isSucceed = SettingsManager.GetInstance().SetQuality(SelectedVideoQuality!.Id); + PublishTip(Key, isSucceed); + } + + /// + /// 优先下载音质事件 + /// + /// + [RelayCommand] + private void SetAudioQuality() + { + bool isSucceed = SettingsManager.GetInstance().SetAudioQuality(SelectedAudioQuality!.Id); + PublishTip(Key, isSucceed); + } + + /// + /// 是否下载flv视频后转码为mp4事件 + /// + [RelayCommand] + private void SetIsTranscodingFlvToMp4() + { + AllowStatus isTranscodingFlvToMp4 = IsTranscodingFlvToMp4 ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().IsTranscodingFlvToMp4(isTranscodingFlvToMp4); + PublishTip(Key, isSucceed); + } + + /// + /// 是否使用默认下载目录事件 + /// + [RelayCommand] + private void SetIsUseDefaultDirectory() + { + AllowStatus isUseDefaultDirectory = IsUseDefaultDirectory ? AllowStatus.YES : AllowStatus.NO; + + bool isSucceed = SettingsManager.GetInstance().IsUseSaveVideoRootPath(isUseDefaultDirectory); + PublishTip(Key, isSucceed); + } + + /// + /// 修改默认下载目录事件 + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task ChangeSaveVideoDirectory() + { + string directory = await StoragePicker.FolderPicker(SaveVideoDirectory); + if (directory == "") { return; } + + bool isSucceed = SettingsManager.GetInstance().SetSaveVideoRootPath(directory); + PublishTip(Key, isSucceed); + + if (isSucceed) + { + SaveVideoDirectory = directory; + } + } + + /// + /// 所有内容选择事件 + /// + [RelayCommand] + private void SetDownloadAll() + { + if (DownloadAll) + { + DownloadAudio = true; + DownloadVideo = true; + DownloadDanmaku = true; + DownloadSubtitle = true; + DownloadCover = true; + } + else + { + DownloadAudio = false; + DownloadVideo = false; + DownloadDanmaku = false; + DownloadSubtitle = false; + DownloadCover = false; + } + + SetVideoContent(); + } + + /// + /// 音频选择事件 + /// + [RelayCommand] + private void SetDownloadAudio() + { + if (!DownloadAudio) + { + DownloadAll = false; + } + + if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover) + { + DownloadAll = true; + } + + SetVideoContent(); + } + + /// + /// 视频选择事件 + /// + [RelayCommand] + private void SetDownloadVideo() + { + if (!DownloadVideo) + { + DownloadAll = false; + } + + if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover) + { + DownloadAll = true; + } + + SetVideoContent(); + } + + /// + /// 弹幕选择事件 + /// + [RelayCommand] + private void SetDownloadDanmaku() + { + if (!DownloadDanmaku) + { + DownloadAll = false; + } + + if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover) + { + DownloadAll = true; + } + + SetVideoContent(); + } + + /// + /// 字幕选择事件 + /// + [RelayCommand] + private void SetDownloadSubtitle() + { + if (!DownloadSubtitle) + { + DownloadAll = false; + } + + if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover) + { + DownloadAll = true; + } + + SetVideoContent(); + } + + /// + /// 封面选择事件 + /// + [RelayCommand] + private void SetDownloadCover() + { + if (!DownloadCover) + { + DownloadAll = false; + } + + if (DownloadAudio && DownloadVideo && DownloadDanmaku && DownloadSubtitle && DownloadCover) + { + DownloadAll = true; + } + + SetVideoContent(); + } + + /// + /// 文件命名中的时间格式事件 + /// + /// + [RelayCommand] + private void TimeFormat() + { + bool isSucceed = SettingsManager.GetInstance().SetFileNamePartTimeFormat(SelectedTimeFormat); + PublishTip(Key, isSucceed); + } + + /// + /// 文件命名中的序号格式事件 + /// + /// + [RelayCommand] + private void OrderFormat() + { + bool isSucceed = SettingsManager.GetInstance().SetOrderFormat(SelectedOrderFormat!.Id); + PublishTip(Key, isSucceed); + } + + /// + /// 重置选中文件名字段 + /// + [RelayCommand] + private void Reset() + { + bool isSucceed = SettingsManager.GetInstance().SetFileNameParts(null); + PublishTip(Key, isSucceed); + + List fileNameParts = SettingsManager.GetInstance().GetFileNameParts(); + SelectedFileName.Clear(); + foreach (FileNamePart item in fileNameParts) + { + int index; + if (SelectedFileName.Count == 0) { index = 0; } + else { index = SelectedFileName.Max(it => it.Index) + 1; } + + string display = DisplayFileNamePart(item); + SelectedFileName.Add(new FileNamePartDisplay + { + Index = index, + Id = item, + Title = display + }); + } + + SelectedOptionalField = -1; + } + + #endregion + + /// + /// 保存下载视频内容到设置 + /// + private void SetVideoContent() + { + VideoContentSettings videoContent = new() + { + DownloadAudio = DownloadAudio, + DownloadVideo = DownloadVideo, + DownloadDanmaku = DownloadDanmaku, + DownloadSubtitle = DownloadSubtitle, + DownloadCover = DownloadCover + }; + + bool isSucceed = SettingsManager.GetInstance().SetVideoContent(videoContent); + PublishTip(Key, isSucceed); + } + + /// + /// 可选文件名字段点击 + /// + /// + protected void OptionalFieldsCommand(object? parameter) + { + if (parameter is not FileNamePartDisplay fileName) { return; } + + int index; + if (SelectedFileName.Count == 0) { index = 0; } + else { index = SelectedFileName.Max(it => it.Index) + 1; } + + SelectedFileName.Add(new FileNamePartDisplay + { + Index = index, + Id = fileName.Id, + Title = fileName.Title, + }); + + SelectedOptionalField = -1; + } + + /// + /// 选中文件名字段右键点击 + /// + /// + protected void SelectedFileNameRightButton(object? parameter) + { + if (parameter is not FileNamePartDisplay fileName) { return; } + + SelectedFileName.Remove(fileName); + + SelectedOptionalField = -1; + } + + /// + /// 文件名字段显示 + /// + /// + /// + protected virtual string DisplayFileNamePart(FileNamePart item) { return string.Empty; } + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Toolbox/BiliHelperViewModel.cs b/src/Downkyi.UI/ViewModels/Toolbox/BiliHelperViewModel.cs new file mode 100644 index 0000000..c78e455 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Toolbox/BiliHelperViewModel.cs @@ -0,0 +1,118 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.Bili.Utils; +using Downkyi.Core.Log; +using Downkyi.UI.Mvvm; +using System.Diagnostics; + +namespace Downkyi.UI.ViewModels.Toolbox; + +public partial class BiliHelperViewModel : ViewModelBase +{ + public const string Key = "Toolbox_BiliHelper"; + + #region 页面属性申明 + + [ObservableProperty] + private string _avid = string.Empty; + + [ObservableProperty] + private string _bvid = string.Empty; + + [ObservableProperty] + private string _danmakuUserID = string.Empty; + + [ObservableProperty] + private string _userMid = string.Empty; + + #endregion + + public BiliHelperViewModel(BaseServices baseServices) : base(baseServices) + { + #region 属性初始化 + #endregion + } + + #region 命令申明 + + /// + /// 输入avid事件 + /// + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task SetAvid() + { + if (!ParseEntrance.IsAvId(Avid)) { return; } + + long avid = ParseEntrance.GetAvId(Avid); + if (avid == -1) { return; } + + await Task.Run(() => + { + Bvid = BvId.Av2Bv((ulong)avid); + }); + } + + /// + /// 输入bvid事件 + /// + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task SetBvid() + { + if (!ParseEntrance.IsBvId(Bvid)) { return; } + + await Task.Run(() => + { + ulong avid = BvId.Bv2Av(Bvid); + Avid = $"av{avid}"; + }); + } + + /// + /// 访问视频网页事件 + /// + [RelayCommand] + private void GotoWeb() + { + string baseUrl = "https://www.bilibili.com/video/"; + Process.Start(new ProcessStartInfo(baseUrl + Bvid) { UseShellExecute = true }); + } + + /// + /// 查询弹幕发送者事件 + /// + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task FindDanmakuSender() + { + await Task.Run(() => + { + try + { + UserMid = DanmakuSender.FindDanmakuSender(DanmakuUserID); + } + catch (Exception e) + { + UserMid = string.Empty; + + Log.Logger.Error(e); + } + }); + } + + /// + /// 访问用户空间事件 + /// + [RelayCommand] + private void VisitUserSpace() + { + if (UserMid == string.Empty) { return; } + + string baseUrl = "https://space.bilibili.com/"; + Process.Start(new ProcessStartInfo(baseUrl + UserMid) { UseShellExecute = true }); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Toolbox/DelogoViewModel.cs b/src/Downkyi.UI/ViewModels/Toolbox/DelogoViewModel.cs new file mode 100644 index 0000000..b4696cb --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Toolbox/DelogoViewModel.cs @@ -0,0 +1,124 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.FFmpeg; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Toolbox; + +public partial class DelogoViewModel : ViewModelBase +{ + public const string Key = "Toolbox_Delogo"; + + // 是否正在执行去水印任务 + protected bool _isDelogo = false; + + #region 页面属性申明 + + [ObservableProperty] + private string _videoPath = string.Empty; + + [ObservableProperty] + private int _logoWidth; + + [ObservableProperty] + private int _logoHeight; + + [ObservableProperty] + private int _logoX; + + [ObservableProperty] + private int _logoY; + + [ObservableProperty] + private string _status = string.Empty; + + #endregion + + public DelogoViewModel(BaseServices baseServices) : base(baseServices) + { + #region 属性初始化 + + VideoPath = string.Empty; + + LogoWidth = 0; + LogoHeight = 0; + LogoX = 0; + LogoY = 0; + + #endregion + } + + #region 命令申明 + + /// + /// 选择视频事件 + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task SelectVideo() + { + if (_isDelogo) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished")); + return; + } + + VideoPath = await StoragePicker.SelectVideoFileAsync(); + } + + /// + /// 去水印事件 + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task Delogo() + { + if (_isDelogo) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished")); + return; + } + + if (VideoPath == "") + { + NotificationEvent.Publish(DictionaryResource.GetString("TipNoSeletedVideo")); + return; + } + + if (LogoWidth == -1) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipInputRightLogoWidth")); + return; + } + if (LogoHeight == -1) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipInputRightLogoHeight")); + return; + } + if (LogoX == -1) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipInputRightLogoX")); + return; + } + if (LogoY == -1) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipInputRightLogoY")); + return; + } + + // 新文件名 + string newFileName = VideoPath.Insert(VideoPath.Length - 4, "_delogo"); + Status = string.Empty; + + await Task.Run(() => + { + // 执行去水印程序 + _isDelogo = true; + FFmpegHelper.Delogo(VideoPath, newFileName, LogoX, LogoY, LogoWidth, LogoHeight, new Action((output) => + { + Status += output + "\n"; + })); + _isDelogo = false; + }); + } + + #endregion +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Toolbox/ExtractMediaViewModel.cs b/src/Downkyi.UI/ViewModels/Toolbox/ExtractMediaViewModel.cs new file mode 100644 index 0000000..490218a --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Toolbox/ExtractMediaViewModel.cs @@ -0,0 +1,139 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.FFmpeg; +using Downkyi.UI.Mvvm; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Downkyi.UI.ViewModels.Toolbox; + +public partial class ExtractMediaViewModel : ViewModelBase +{ + public const string Key = "Toolbox_ExtractMedia"; + + // 是否正在执行任务 + private bool _isExtracting = false; + + #region 页面属性申明 + + [ObservableProperty] + private string _videoPathsStr = string.Empty; + + [ObservableProperty] + private ObservableCollection _videoPaths = new(); + + [ObservableProperty] + private string _status = string.Empty; + + #endregion + + public ExtractMediaViewModel(BaseServices baseServices) : base(baseServices) + { + VideoPaths.CollectionChanged += new NotifyCollectionChangedEventHandler((sender, e) => + { + VideoPathsStr = string.Join(Environment.NewLine, VideoPaths); + }); + } + + #region 命令申明 + + /// + /// 选择视频事件 + /// + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task SelectVideo() + { + if (_isExtracting) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished")); + return; + } + + var videoPathList = await StoragePicker.SelectMultiVideoFileAsync(); + + VideoPaths.Clear(); + foreach (var path in videoPathList) + { + VideoPaths.Add(path); + } + } + + /// + /// 提取音频事件 + /// + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task ExtractAudio() + { + if (_isExtracting) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished")); + return; + } + + if (VideoPaths.Count <= 0) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipNoSeletedVideo")); + return; + } + + Status = string.Empty; + + await Task.Run(() => + { + _isExtracting = true; + foreach (var item in VideoPaths) + { + // 音频文件名 + string audioFileName = item.Remove(item.Length - 4, 4) + ".aac"; + // 执行提取音频程序 + FFmpegHelper.ExtractAudio(item, audioFileName, new Action((output) => + { + Status += output + "\n"; + })); + } + _isExtracting = false; + }); + } + + /// + /// 提取视频事件 + /// + /// + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task ExtractVideo() + { + if (_isExtracting) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipWaitTaskFinished")); + return; + } + + if (VideoPaths.Count <= 0) + { + NotificationEvent.Publish(DictionaryResource.GetString("TipNoSeletedVideo")); + return; + } + + Status = string.Empty; + + await Task.Run(() => + { + _isExtracting = true; + foreach (var item in VideoPaths) + { + // 视频文件名 + string videoFileName = item.Remove(item.Length - 4, 4) + "_onlyVideo.mp4"; + // 执行提取视频程序 + FFmpegHelper.ExtractVideo(item, videoFileName, new Action((output) => + { + Status += output + "\n"; + })); + } + _isExtracting = false; + }); + } + + #endregion +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/Toolbox/ToolboxViewModel.cs b/src/Downkyi.UI/ViewModels/Toolbox/ToolboxViewModel.cs new file mode 100644 index 0000000..5ec213d --- /dev/null +++ b/src/Downkyi.UI/ViewModels/Toolbox/ToolboxViewModel.cs @@ -0,0 +1,48 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Downkyi.UI.Models; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.Toolbox; + +public partial class ToolboxViewModel : ViewModelBase +{ + public const string Key = "Toolbox"; + + #region 页面属性申明 + + [ObservableProperty] + private int _selectedTabId = -1; + + [ObservableProperty] + private List _tabHeaders = new(); + + #endregion + + public ToolboxViewModel(BaseServices baseServices) : base(baseServices) + { + #region 属性初始化 + + TabHeaders.Add(new TabHeader { Id = 0, Title = DictionaryResource.GetString("BiliHelper") }); + TabHeaders.Add(new TabHeader { Id = 1, Title = DictionaryResource.GetString("Delogo") }); + TabHeaders.Add(new TabHeader { Id = 2, Title = DictionaryResource.GetString("ExtractMedia") }); + + #endregion + } + + #region 命令申明 + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task BackwardAsync() + { + Dictionary parameter = new() + { + { "key", Key }, + }; + + await NavigationService.BackwardAsync(parameter); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/User/MySpaceViewModel.cs b/src/Downkyi.UI/ViewModels/User/MySpaceViewModel.cs new file mode 100644 index 0000000..0f30bf0 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/User/MySpaceViewModel.cs @@ -0,0 +1,29 @@ +using CommunityToolkit.Mvvm.Input; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.User; + +public partial class MySpaceViewModel : ViewModelBase +{ + public const string Key = "MySpace"; + + public MySpaceViewModel(BaseServices baseServices) : base(baseServices) + { + } + + #region 命令申明 + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task BackwardAsync() + { + Dictionary parameter = new() + { + { "key", Key }, + }; + + await NavigationService.BackwardAsync(parameter); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.UI/ViewModels/User/UserSpaceViewModel.cs b/src/Downkyi.UI/ViewModels/User/UserSpaceViewModel.cs new file mode 100644 index 0000000..73131a0 --- /dev/null +++ b/src/Downkyi.UI/ViewModels/User/UserSpaceViewModel.cs @@ -0,0 +1,29 @@ +using CommunityToolkit.Mvvm.Input; +using Downkyi.UI.Mvvm; + +namespace Downkyi.UI.ViewModels.User; + +public partial class UserSpaceViewModel : ViewModelBase +{ + public const string Key = "UserSpace"; + + public UserSpaceViewModel(BaseServices baseServices) : base(baseServices) + { + } + + #region 命令申明 + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task BackwardAsync() + { + Dictionary parameter = new() + { + { "key", Key }, + }; + + await NavigationService.BackwardAsync(parameter); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Downkyi.sln b/src/Downkyi.sln new file mode 100644 index 0000000..8685ada --- /dev/null +++ b/src/Downkyi.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Downkyi", "Downkyi\Downkyi.csproj", "{4021D838-C609-49A7-9065-3AC23EBDBC54}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Downkyi.UI", "Downkyi.UI\Downkyi.UI.csproj", "{7FCEED5F-BF98-44B0-8F4D-594C182D235F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Downkyi.Core", "Downkyi.Core\Downkyi.Core.csproj", "{5CED42DB-2155-45D0-B195-57BB894CB97B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BiliSharp", "BiliSharp\BiliSharp.csproj", "{199B3491-51F0-460D-AB90-191B7DD377D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BiliSharp.UnitTest", "BiliSharp.UnitTest\BiliSharp.UnitTest.csproj", "{107D1B61-6936-45A0-B4AF-3776C736649A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4021D838-C609-49A7-9065-3AC23EBDBC54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4021D838-C609-49A7-9065-3AC23EBDBC54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4021D838-C609-49A7-9065-3AC23EBDBC54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4021D838-C609-49A7-9065-3AC23EBDBC54}.Release|Any CPU.Build.0 = Release|Any CPU + {7FCEED5F-BF98-44B0-8F4D-594C182D235F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FCEED5F-BF98-44B0-8F4D-594C182D235F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FCEED5F-BF98-44B0-8F4D-594C182D235F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FCEED5F-BF98-44B0-8F4D-594C182D235F}.Release|Any CPU.Build.0 = Release|Any CPU + {5CED42DB-2155-45D0-B195-57BB894CB97B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CED42DB-2155-45D0-B195-57BB894CB97B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CED42DB-2155-45D0-B195-57BB894CB97B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CED42DB-2155-45D0-B195-57BB894CB97B}.Release|Any CPU.Build.0 = Release|Any CPU + {199B3491-51F0-460D-AB90-191B7DD377D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {199B3491-51F0-460D-AB90-191B7DD377D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {199B3491-51F0-460D-AB90-191B7DD377D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {199B3491-51F0-460D-AB90-191B7DD377D6}.Release|Any CPU.Build.0 = Release|Any CPU + {107D1B61-6936-45A0-B4AF-3776C736649A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {107D1B61-6936-45A0-B4AF-3776C736649A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {107D1B61-6936-45A0-B4AF-3776C736649A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {107D1B61-6936-45A0-B4AF-3776C736649A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {32C09F62-DE01-4EBF-A0FA-6987F8C28C56} + EndGlobalSection +EndGlobal diff --git a/src/Downkyi/.gitignore b/src/Downkyi/.gitignore new file mode 100644 index 0000000..8afdcb6 --- /dev/null +++ b/src/Downkyi/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/src/Downkyi/App.axaml b/src/Downkyi/App.axaml new file mode 100644 index 0000000..fce40ba --- /dev/null +++ b/src/Downkyi/App.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/App.axaml.cs b/src/Downkyi/App.axaml.cs new file mode 100644 index 0000000..164cad6 --- /dev/null +++ b/src/Downkyi/App.axaml.cs @@ -0,0 +1,78 @@ +using AsyncImageLoader; +using AsyncImageLoader.Loaders; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using CommunityToolkit.Mvvm.DependencyInjection; +using Downkyi.UI.Services; +using Downkyi.Views; + +namespace Downkyi; + +public partial class App : Application +{ + public App() + { + } + + public override void Initialize() + { + // Register handle task exception + HandleTaskException.RegisterEvents(); + + // Register services + ServiceLocator.ConfigureServices(); + + // ͼ񻺴Ϊ + ImageLoader.AsyncImageLoader = new DiskCachedWebImageLoader(); + + // + Ioc.Default.GetRequiredService().LoadTheme("Default"); + + // л + Ioc.Default.GetRequiredService().LoadLanguage("Default"); + + AvaloniaXamlLoader.Load(this); + + var accentColor = Color.Parse(Ioc.Default.GetRequiredService().GetColor("ColorPrimary")); + Resources["SystemAccentColor"] = accentColor; + //Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); + //Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); + //Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); + //Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3); + //Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5); + //Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7); + + //var hsl = Color.ToHsl(accentColor); + //HslColor.FromHsl(hsl.H, hsl.S, 0.7); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + //ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + //desktop.MainWindow = new MainWindow + //{ + // DataContext = ServiceLocator.MainWindowViewModel, + //}; + desktop.MainWindow = new SplashWindow(() => + { + var mainWindow = new MainWindow() + { + DataContext = ServiceLocator.MainWindowViewModel, + }; + + mainWindow.Show(); + mainWindow.Focus(); + + desktop.MainWindow = mainWindow; + }); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/src/Downkyi/Assets/Fonts/SourceHanSansCN-Bold.otf b/src/Downkyi/Assets/Fonts/SourceHanSansCN-Bold.otf new file mode 100644 index 0000000..8e1e869 Binary files /dev/null and b/src/Downkyi/Assets/Fonts/SourceHanSansCN-Bold.otf differ diff --git a/src/Downkyi/Assets/Fonts/SourceHanSansCN-Normal.otf b/src/Downkyi/Assets/Fonts/SourceHanSansCN-Normal.otf new file mode 100644 index 0000000..abd36ce Binary files /dev/null and b/src/Downkyi/Assets/Fonts/SourceHanSansCN-Normal.otf differ diff --git a/src/Downkyi/Assets/bili/default_header.jpg b/src/Downkyi/Assets/bili/default_header.jpg new file mode 100644 index 0000000..5e5ea0b Binary files /dev/null and b/src/Downkyi/Assets/bili/default_header.jpg differ diff --git a/src/Downkyi/Assets/bili/login/login_top_bar.png b/src/Downkyi/Assets/bili/login/login_top_bar.png new file mode 100644 index 0000000..68dc5d3 Binary files /dev/null and b/src/Downkyi/Assets/bili/login/login_top_bar.png differ diff --git a/src/Downkyi/Assets/bili/login/qrcode_login_2233.png b/src/Downkyi/Assets/bili/login/qrcode_login_2233.png new file mode 100644 index 0000000..3d9ff56 Binary files /dev/null and b/src/Downkyi/Assets/bili/login/qrcode_login_2233.png differ diff --git a/src/Downkyi/Assets/bili/login/qrcode_login_tip.png b/src/Downkyi/Assets/bili/login/qrcode_login_tip.png new file mode 100644 index 0000000..177018b Binary files /dev/null and b/src/Downkyi/Assets/bili/login/qrcode_login_tip.png differ diff --git a/src/Downkyi/Assets/bili/login/scan_succeed.png b/src/Downkyi/Assets/bili/login/scan_succeed.png new file mode 100644 index 0000000..58151a1 Binary files /dev/null and b/src/Downkyi/Assets/bili/login/scan_succeed.png differ diff --git a/src/Downkyi/Assets/bili/下载.png b/src/Downkyi/Assets/bili/下载.png new file mode 100644 index 0000000..58151a1 Binary files /dev/null and b/src/Downkyi/Assets/bili/下载.png differ diff --git a/src/Downkyi/Assets/logo.ico b/src/Downkyi/Assets/logo.ico new file mode 100644 index 0000000..0145cb9 Binary files /dev/null and b/src/Downkyi/Assets/logo.ico differ diff --git a/src/Downkyi/Behaviors/FileNamePartDropHandler.cs b/src/Downkyi/Behaviors/FileNamePartDropHandler.cs new file mode 100644 index 0000000..8466cef --- /dev/null +++ b/src/Downkyi/Behaviors/FileNamePartDropHandler.cs @@ -0,0 +1,80 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.VisualTree; +using Avalonia.Xaml.Interactions.DragAndDrop; +using Downkyi.UI.Models; +using Downkyi.ViewModels.Settings; + +namespace Downkyi.Behaviors; + +public class FileNamePartDropHandler : DropHandlerBase +{ + private bool Validate(ItemsControl list, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute) where T : FileNamePartDisplay + { + if (sourceContext is not T sourceItem + || targetContext is not VideoViewModelProxy vm + || list.GetVisualAt(e.GetPosition(list)) is not Control targetControl + || targetControl.DataContext is not T targetItem) + { + return false; + } + + var items = vm.SelectedFileName; + var sourceIndex = items.IndexOf(sourceItem); + var targetIndex = items.IndexOf(targetItem); + + if (sourceIndex < 0 || targetIndex < 0) + { + return false; + } + + switch (e.DragEffects) + { + case DragDropEffects.Copy: + { + if (bExecute) + { + var clone = new FileNamePartDisplay() { Title = sourceItem.Title + "_copy" }; + InsertItem(items, clone, targetIndex + 1); + } + return true; + } + case DragDropEffects.Move: + { + if (bExecute) + { + MoveItem(items, sourceIndex, targetIndex); + } + return true; + } + case DragDropEffects.Link: + { + if (bExecute) + { + SwapItem(items, sourceIndex, targetIndex); + } + return true; + } + default: + return false; + } + } + + public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state) + { + if (e.Source is Control && sender is ItemsControl list) + { + return Validate(list, e, sourceContext, targetContext, false); + } + return false; + } + + public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state) + { + if (e.Source is Control && sender is ItemsControl list) + { + return Validate(list, e, sourceContext, targetContext, true); + } + return false; + } +} \ No newline at end of file diff --git a/src/Downkyi/Converter/IntEquealConverter.cs b/src/Downkyi/Converter/IntEquealConverter.cs new file mode 100644 index 0000000..99ec0b0 --- /dev/null +++ b/src/Downkyi/Converter/IntEquealConverter.cs @@ -0,0 +1,34 @@ +using Avalonia.Data; +using Avalonia.Data.Converters; +using System; +using System.Globalization; + +namespace Downkyi.Converter; + +public class IntEquealConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (parameter is string str) + { + if (value is int source && int.TryParse(str, out int target)) + { + return source == target; + } + } + else if (parameter is int integer) + { + if (value is int source && targetType.IsAssignableTo(typeof(bool))) + { + return source == integer; + } + } + + return new BindingNotification(new InvalidCastException(), BindingErrorType.Error); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Downkyi/Converter/StringToResourceConverter.cs b/src/Downkyi/Converter/StringToResourceConverter.cs new file mode 100644 index 0000000..e84598d --- /dev/null +++ b/src/Downkyi/Converter/StringToResourceConverter.cs @@ -0,0 +1,28 @@ +using Avalonia; +using Avalonia.Data.Converters; +using Downkyi.Services; +using System; +using System.Globalization; + +namespace Downkyi.Converter; + +/// +/// 通过字符串访问Resource资源 +/// +public class StringToResourceConverter : IValueConverter +{ + public static StringToResourceConverter Instance { get; } = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value == null) + return null; + + return DictionaryResource.GetResource(Application.Current?.Resources, value.ToString()!); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Downkyi/Downkyi.csproj b/src/Downkyi/Downkyi.csproj new file mode 100644 index 0000000..170e943 --- /dev/null +++ b/src/Downkyi/Downkyi.csproj @@ -0,0 +1,58 @@ + + + WinExe + net6.0 + enable + true + Downkyi + app.manifest + Assets\logo.ico + DownKyi + DownKyi + DownKyi + DownKyi + DownKyi + Copyright © Downkyi 2020-2023 + 2.0.0 + 2.0.0 + 2.0.0 + 2.0.0 + + + none + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/HandleTaskException.cs b/src/Downkyi/HandleTaskException.cs new file mode 100644 index 0000000..b43b963 --- /dev/null +++ b/src/Downkyi/HandleTaskException.cs @@ -0,0 +1,69 @@ +using Downkyi.Core.Log; +using NLog; +using System; +using System.Threading.Tasks; + +namespace Downkyi; + +public static class HandleTaskException +{ + public static void RegisterEvents() + { + // Task线程内未捕获异常处理事件 + TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; + + // 非UI线程未捕获异常处理事件(例如自己创建的一个子线程) + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + + } + + // Task线程内未捕获异常处理事件 + private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + try + { + if (e.Exception is Exception exception) + { + HandleException(exception); + } + } + catch (Exception ex) + { + HandleException(ex); + } + finally + { + e.SetObserved(); + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + LogManager.Shutdown(); + } + } + + // 非UI线程未捕获异常处理事件(例如自己创建的一个子线程) + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + try + { + if (e.ExceptionObject is Exception exception) + { + HandleException(exception); + } + } + catch (Exception ex) + { + HandleException(ex); + } + finally + { + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + LogManager.Shutdown(); + } + } + + // 日志记录 + private static void HandleException(Exception e) + { + Log.Logger.Fatal(e, "UnhandleException"); + } + +} \ No newline at end of file diff --git a/src/Downkyi/Images/Navigation.axaml b/src/Downkyi/Images/Navigation.axaml new file mode 100644 index 0000000..3fe2b85 --- /dev/null +++ b/src/Downkyi/Images/Navigation.axaml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Downkyi/Images/Normal.axaml b/src/Downkyi/Images/Normal.axaml new file mode 100644 index 0000000..2adcba2 --- /dev/null +++ b/src/Downkyi/Images/Normal.axaml @@ -0,0 +1,3 @@ + + + diff --git a/src/Downkyi/Languages/Default.axaml b/src/Downkyi/Languages/Default.axaml new file mode 100644 index 0000000..d04c36b --- /dev/null +++ b/src/Downkyi/Languages/Default.axaml @@ -0,0 +1,370 @@ + + + zh-cn + + + 哔哩下载姬 + 未知 + + + 关闭 + 最大化 + 最小化 + 还原 + 皮肤 + + + 登录 + 请输入B站网址链接或av、BV号等…… + 请输入B站视频播放地址…… + 设置 + 下载管理 + 工具箱 + + + 二维码 + Cookies + + 扫码成功 请在手机上确认 + 扫描二维码登录 + 请使用哔哩哔哩客户端 扫码登录 或扫码下载APP + 获取登录链接失败! + 密钥错误!尝试重新获取! + 已超时!尝试重新获取二维码! + 登录成功!(3秒后自动回到主页) + 未保存登录信息!(3秒后自动回到主页) + + 请输入Cookies + 保存 + 登录成功! + 未保存登录信息! + + + 我的个人空间 + 退出登录 + 请稍等,马上就好~ + 等级 + 关注数 + 悄悄关注数 + 粉丝数 + 黑名单数 + 节操值 + 封禁状态 + 收藏夹 + 我的订阅 + 稍后再看 + 历史记录 + + 正常 + 封停 + + + 个人空间 + 已关注 + 未关注 + 关注数 + 粉丝数 + 获赞数 + 播放数 + 阅读数 + 全部 + 投稿视频 + 频道 + 合集和列表 + 请稍等,马上就好~ + + + 收藏夹 + 个内容 + + + 播放: + 收藏: + UP主: + 投稿: + 收藏于: + + + 我的订阅 + 追番 + 追剧 + 尚未观看 + + + 历史记录 + 已看完 + 刚开始 + 看到 + + + 稍后再看 + + + 下载选中项 + 下载全部 + 请稍等,马上就好~ + + + 好友 + 关注 + 粉丝 + + 请稍等,马上就好~ + 请稍等,马上就好~ + + 全部关注 + 悄悄关注 + + + 复制封面图片 + 复制封面URL + 播放 + 弹幕 + 点赞 + 投币 + 收藏 + 分享 + 评论 + + 番剧 + 电影 + 纪录片 + 国创 + 电视剧 + 综艺 + 课程 + + 序号 + 名称 + 时长 + 音质 + 画质 + 视频编码 + + 全选 + 解析 + 解析视频 + 下载选中项 + 下载全部 + + 已经添加到下载列表~ + 已经下载完成~ + 没有选中项符合下载要求! + 成功添加了 + 项~ + + + 正在下载 + 已下载 + + 音频 + 视频 + 弹幕 + 字幕 + 封面 + 正在解析…… + 下载中…… + 混流中…… + 暂停中…… + 等待中…… + 下载失败 + + 正在下载 + 个视频! + 全部暂停 + 全部开始 + 全部删除 + + 已下载 + 个视频! + 按下载先后排序 + 按序号排序 + 清空所有记录 + + 开始 + 暂停 + 重试 + 移除 + 打开文件夹 + 播放 + + + 按回车键应用设置 + + 基本 + 下载完成后的动作: + + 关闭程序 + 关闭计算机 + 监听剪贴板 + 视频自动解析 + 视频解析范围: + 解析后自动下载已解析视频 + + 网络 + 启用https(若下载器提示SSL错误,则关闭此项) + UserAgent: + 选择下载器(重启生效): + 内建下载器 + Aria2下载器 + 自定义Aria2下载器 + Aria服务器地址: + Aria服务器端口: + Aria服务器Token: + Aria日志等级: + Aria同时下载数: + Aria最大线程数: + Aria下载速度限制(KB/s) + 全局下载速度限制[0: 无限制] + 单任务下载速度限制[0: 无限制] + Aria文件预分配: + 使用Http代理 + 代理地址: + 端口: + 同时下载数: + 最大线程数: + + 视频 + 优先下载的视频编码: + 优先下载的视频画质: + 优先下载的视频音质: + 下载FLV视频后转码为mp4 + 使用默认下载目录 + 默认下载目录: + 默认将文件下载到该文件夹中 + 更改目录 + 文件命名格式 + 文件名: + 可选字段: + 序号 + 视频章节 + 视频标题 + 分P标题 + 视频分区 + 音质 + 画质 + 视频编码 + 发布时间 + 空格 + UP主ID + UP主昵称 + 恢复默认 + 时间格式: + 序号格式: + 自然数 + 前导零填充 + + 弹幕 + 按类型屏蔽 + 顶部 + 底部 + 滚动 + 分辨率: + 字体名称: + 字体大小: + 限制行数: + 布局算法: + 速度同步 + 每一条滚动弹幕的移动速度都是相同的 + 速度异步 + 弹幕滚动速度取决于它的文本长度 + + 关于 + 访问主页 + 当前版本: + 检查更新 + 意见反馈 + 接收测试版更新 + 启动时检查更新 + 设置已更新~ + 设置失败! + 第三方组件 + 名称 + 作者 + 版本 + 许可证 + 免责申明 + 1.本软件只提供视频解析,不提供任何资源上传、存储到服务器的功能。 + 2.本软件仅解析来自B站的内容,不会对解析到的音视频进行二次编码,部分视频会进行有限的格式转换、拼接等操作。 + 3.本软件解析得到的所有内容均来自B站UP主上传、分享,其版权均归原作者所有。内容提供者、上传者(UP主)应对其提供、上传的内容承担全部责任。 + 4.本软件提供的所有资源,仅可用作学习交流使用,未经原作者授权,禁止用于其他用途。请在下载24小时内删除。为尊重作者版权,请前往资源的原始发布网站观看,支持原创,谢谢。 + 5.任何涉及商业盈利目的均不得使用,否则产生的一切后果将由您自己承担。 + 6.因使用本软件产生的版权问题,软件作者概不负责。 + + + B站助手 + AV和BV互转 + AV号形如"av123",BV号形如"BVxxx"。 + AVID: + BVID: + 访问网页 + 弹幕中的userID: + 查询弹幕发送者-> + + 去水印 + 视频地址: + 选择视频 + 水印尺寸与位置(左上角为原点,单位:像素) + 宽: + 高: + X: + Y: + 请等待当前任务完成! + 没有选择视频! + 请输入正确的宽度(正整数)! + 请输入正确的高度(正整数)! + 请输入正确的X(正整数)! + 请输入正确的Y(正整数)! + 输出信息: + + 音视频分离 + 提取音频 + 提取视频 + + + 信息 + 警告 + 错误 + + 确定 + 取消 + + 此项需重启生效,您确定要重新启动吗? + 您确定要删除吗? + 选择文件夹 + 选择视频 + + 路径错误! + + + 下载设置 + 位置: + 浏览 + 盘剩余空间: + 下载内容: + 下载内容 + 所有 + 音频 + 视频 + 弹幕 + 字幕 + 封面 + 设此文件夹为默认下载文件夹 + 选中后下次将不会弹出此窗口 + 下载 + 文件夹路径不能为空 + 该磁盘不存在 + + + 选择解析项 + + 解析选中项 + 解析当前页视频 + 解析所有视频 + 设为默认,下次不再弹出 + + diff --git a/src/Downkyi/Models/AppInfo.cs b/src/Downkyi/Models/AppInfo.cs new file mode 100644 index 0000000..372ac7d --- /dev/null +++ b/src/Downkyi/Models/AppInfo.cs @@ -0,0 +1,54 @@ +using CommunityToolkit.Mvvm.DependencyInjection; +using Downkyi.Services; +using Downkyi.UI.Services; +using System; +using System.Text.RegularExpressions; + +namespace Downkyi.Models; + +public class AppInfo +{ + public string Name { get; } = Ioc.Default.GetRequiredService().GetString("AppName"); + public long VersionCode { get; } + public string VersionName { get; } + + const int a = 2; + const int b = 0; + const int c = 0; + + public AppInfo() + { + VersionCode = a * 10000 + b * 100 + c; + +#if DEBUG + VersionName = $"{a}.{b}.{c}-debug"; +#else + VersionName = $"{a}.{b}.{c}"; +#endif + } + + public static int VersionNameToCode(string versionName) + { + int code = 0; + + var isMatch = Regex.IsMatch(versionName, @"^v?([1-9]\d|\d).([1-9]\d|\d).([1-9]\d|\d)$"); + if (!isMatch) + { + return 0; + } + + string pattern = @"([1-9]\d|\d)"; + var m = Regex.Matches(versionName, pattern); + if (m.Count == 3) + { + int i = 2; + foreach (var item in m) + { + code += int.Parse(item == null ? "" : item.ToString()!) * (int)Math.Pow(100, i); + i--; + } + } + + return code; + } +} \ No newline at end of file diff --git a/src/Downkyi/Program.cs b/src/Downkyi/Program.cs new file mode 100644 index 0000000..ebd9b7c --- /dev/null +++ b/src/Downkyi/Program.cs @@ -0,0 +1,27 @@ +using Avalonia; +using NLog; +using System; + +namespace Downkyi; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + //GC.KeepAlive(typeof(SvgImageExtension).Assembly); + //GC.KeepAlive(typeof(Avalonia.Svg.Skia.Svg).Assembly); + + return AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} \ No newline at end of file diff --git a/src/Downkyi/Roots.xml b/src/Downkyi/Roots.xml new file mode 100644 index 0000000..9248c6f --- /dev/null +++ b/src/Downkyi/Roots.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Downkyi/ServiceLocator.cs b/src/Downkyi/ServiceLocator.cs new file mode 100644 index 0000000..7f0d059 --- /dev/null +++ b/src/Downkyi/ServiceLocator.cs @@ -0,0 +1,123 @@ +using CommunityToolkit.Mvvm.DependencyInjection; +using Downkyi.Services; +using Downkyi.UI.Mvvm; +using Downkyi.UI.Services; +using Downkyi.UI.Services.Event; +using Downkyi.UI.ViewModels; +using Downkyi.UI.ViewModels.DownloadManager; +using Downkyi.UI.ViewModels.Login; +using Downkyi.UI.ViewModels.Settings; +using Downkyi.UI.ViewModels.Toolbox; +using Downkyi.UI.ViewModels.User; +using Downkyi.ViewModels; +using Downkyi.ViewModels.Login; +using Downkyi.ViewModels.Settings; +using Downkyi.ViewModels.Toolbox; +using Microsoft.Extensions.DependencyInjection; + +namespace Downkyi; + +public static class ServiceLocator +{ + private static bool _initialized; + + // 主窗体 + public static MainWindowViewModel MainWindowViewModel => + Ioc.Default.GetRequiredService(); + + // 主页 + public static IndexViewModel IndexViewModel => + Ioc.Default.GetRequiredService(); + + // 登录 + public static LoginViewModel LoginViewModel => + Ioc.Default.GetRequiredService(); + public static QRCodeViewModelProxy QRCodeViewModel => + Ioc.Default.GetRequiredService(); + public static CookiesViewModel CookiesViewModel => + Ioc.Default.GetRequiredService(); + + // 我的个人空间 + public static MySpaceViewModel MySpaceViewModel => + Ioc.Default.GetRequiredService(); + + // 个人空间 + public static UserSpaceViewModel UserSpaceViewModel => + Ioc.Default.GetRequiredService(); + + // 设置 + public static SettingsViewModel SettingsViewModel => + Ioc.Default.GetRequiredService(); + public static BasicViewModel BasicViewModel => + Ioc.Default.GetRequiredService(); + public static NetworkViewModel NetworkViewModel => + Ioc.Default.GetRequiredService(); + public static VideoViewModelProxy VideoViewModel => + Ioc.Default.GetRequiredService(); + public static DanmakuViewModelProxy DanmakuViewModel => + Ioc.Default.GetRequiredService(); + public static AboutViewModelProxy AboutViewModel => + Ioc.Default.GetRequiredService(); + + // 下载管理 + public static DownloadManagerViewModel DownloadManagerViewModel => + Ioc.Default.GetRequiredService(); + public static DownloadingViewModel DownloadingViewModel => + Ioc.Default.GetRequiredService(); + public static DownloadFinishedViewModel DownloadFinishedViewModel => + Ioc.Default.GetRequiredService(); + + // 工具箱 + public static ToolboxViewModel ToolboxViewModel => + Ioc.Default.GetRequiredService(); + public static BiliHelperViewModel BiliHelperViewModel => + Ioc.Default.GetRequiredService(); + public static DelogoViewModelProxy DelogoViewModel => + Ioc.Default.GetRequiredService(); + public static ExtractMediaViewModelProxy ExtractMediaViewModel => + Ioc.Default.GetRequiredService(); + + /// + /// Configures the services for the application. + /// + public static void ConfigureServices() + { + // Register services + if (!_initialized) + { + _initialized = true; + Ioc.Default.ConfigureServices( + new ServiceCollection() + //Services + .AddScoped() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + //ViewModels + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .BuildServiceProvider()); + } + } +} \ No newline at end of file diff --git a/src/Downkyi/Services/DictionaryResource.cs b/src/Downkyi/Services/DictionaryResource.cs new file mode 100644 index 0000000..40dea22 --- /dev/null +++ b/src/Downkyi/Services/DictionaryResource.cs @@ -0,0 +1,147 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Media; +using Avalonia.Styling; +using Downkyi.UI.Services; +using System; +using System.Collections.Generic; + +namespace Downkyi.Services; + +public class DictionaryResource : IDictionaryResource +{ + public string GetColor(string resourceKey) + { + object? value = GetResource(Application.Current!.Resources, resourceKey); + return value == null ? "#00000000" : ((Color)value).ToString(); + } + + public string GetString(string resourceKey) + { + object? value = GetResource(Application.Current!.Resources, resourceKey); + return value == null ? "" : (string)value; + } + + public void LoadLanguage(string languageCode) + { + LoadXamlDictionary(languageCode, "Languages"); + } + + public void LoadTheme(string theme) + { + LoadXamlDictionary($"{theme}/Theme", "Themes"); + LoadXamlStyle($"{theme}/Styles", "Themes"); + } + + /// + /// 递归遍历所有资源 + /// + /// + /// + /// + public static object? GetResource(IResourceDictionary? dictionary, string value) + { + if (dictionary == null) + return null; + + object? result = null; + if (dictionary.ContainsKey(value)) + { + result = dictionary[value]; + return result; + } + + foreach (var provider in dictionary.MergedDictionaries) + { + if (provider is ResourceDictionary resources) + { + object? temp = GetResource(resources, value); + if (temp != null) + { + result = temp; + } + } + if (provider is ResourceInclude include) + { + object? temp = GetResource(include.Loaded, value); + if (temp != null) + { + result = temp; + } + } + } + + return result; + } + + /// + /// 更换xaml资源字典 + /// + /// xaml文件名,不包含扩展名 + /// 文件所在包(路径) + private static void LoadXamlDictionary(string fileName, string package) + { + if (string.IsNullOrEmpty(fileName) == false) + { + var dictionariesToRemove = new List(); + + foreach (var dictionary in Application.Current!.Resources.MergedDictionaries) + { + if (dictionary.ToString()!.Contains("ResourceInclude")) + { + dictionariesToRemove.Add(dictionary); + } + } + + foreach (var item in dictionariesToRemove) + { + Application.Current.Resources.MergedDictionaries.Remove(item); + } + + //string? assemblyName = Assembly.GetEntryAssembly()!.GetName().Name; + //var xaml = new Uri($"avares://{assemblyName}/{package}/{fileName}.axaml"); + // 直接指定程序集名称,为了预览是可以正确找到资源 + var xaml = new Uri($"avares://Downkyi/{package}/{fileName}.axaml"); + + var languageDictionary = (ResourceDictionary)AvaloniaXamlLoader.Load(xaml); + Application.Current.Resources.MergedDictionaries.Add(languageDictionary); + } + } + + /// + /// 更换xaml样式 + /// + /// xaml文件名,不包含扩展名 + /// 文件所在包(路径) + private static void LoadXamlStyle(string fileName, string package) + { + if (string.IsNullOrEmpty(fileName) == false) + { + var stylesToRemove = new List(); + + foreach (var style in Application.Current!.Styles) + { + if (style.ToString()!.Contains("StyleInclude")) + { + stylesToRemove.Add(style); + } + } + + foreach (var item in stylesToRemove) + { + Application.Current.Styles.Remove(item); + } + + //string? assemblyName = Assembly.GetEntryAssembly()!.GetName().Name; + //var xaml = new Uri($"avares://{assemblyName}/{package}/{fileName}.axaml"); + // 直接指定程序集名称,为了预览是可以正确找到资源 + var xaml = new Uri($"avares://Downkyi/{package}/{fileName}.axaml"); + + var styles = (Styles)AvaloniaXamlLoader.Load(xaml); + Application.Current.Styles.Add(styles); + } + } + +} \ No newline at end of file diff --git a/src/Downkyi/Services/NavigationService.cs b/src/Downkyi/Services/NavigationService.cs new file mode 100644 index 0000000..8e475af --- /dev/null +++ b/src/Downkyi/Services/NavigationService.cs @@ -0,0 +1,71 @@ +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; +using Downkyi.UI.Services; +using Downkyi.ViewModels; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Downkyi.Services; + +public class NavigationService : INavigationService +{ + public NavigationService() + { + } + + public async Task ForwardAsync(string viewKey) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + if (App.Current!.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow!.DataContext is MainWindowViewModel viewModel) + { + viewModel.Forward(viewKey); + } + } + }); + } + + public async Task ForwardAsync(string viewKey, Dictionary parameter) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + if (App.Current!.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow!.DataContext is MainWindowViewModel viewModel) + { + viewModel.Forward(viewKey, parameter); + } + } + }); + } + + public async Task BackwardAsync() + { + await Dispatcher.UIThread.InvokeAsync(() => + { + if (App.Current!.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow!.DataContext is MainWindowViewModel viewModel) + { + viewModel.Backward(); + } + } + }); + } + + public async Task BackwardAsync(Dictionary parameter) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + if (App.Current!.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow!.DataContext is MainWindowViewModel viewModel) + { + viewModel.Backward(parameter); + } + } + }); + } +} \ No newline at end of file diff --git a/src/Downkyi/Services/StoragePicker.cs b/src/Downkyi/Services/StoragePicker.cs new file mode 100644 index 0000000..09ccdd8 --- /dev/null +++ b/src/Downkyi/Services/StoragePicker.cs @@ -0,0 +1,102 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.DependencyInjection; +using Downkyi.UI.Services; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Web; + +namespace Downkyi.Services; + +public class StoragePicker : IStoragePicker +{ + public async Task FolderPicker(string directory = "") + { + if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || + desktop.MainWindow?.StorageProvider is not { } provider) + throw new NullReferenceException("Missing StorageProvider instance."); + + var storageProvider = desktop.MainWindow!.StorageProvider; + FolderPickerOpenOptions options = new() + { + AllowMultiple = false, + Title = Ioc.Default.GetRequiredService().GetString("SelectDirectory"), + SuggestedStartLocation = await storageProvider.TryGetFolderFromPathAsync(directory) + }; + + var folder = await storageProvider.OpenFolderPickerAsync(options); + if (folder.Count > 0) + { + return folder[0].Path.AbsolutePath; + } + + return string.Empty; + } + + public async Task SelectVideoFileAsync() + { + if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || + desktop.MainWindow?.StorageProvider is not { } provider) + throw new NullReferenceException("Missing StorageProvider instance."); + + // Get top level from the current control. Alternatively, you can use Window reference instead. + var topLevel = TopLevel.GetTopLevel(desktop.MainWindow); + + // Start async operation to open the dialog. + var file = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = Ioc.Default.GetRequiredService().GetString("SelectVideoFile"), + AllowMultiple = false, + FileTypeFilter = new List + { + new FilePickerFileType("mp4") + { + Patterns = new List() { "*.mp4" } + } + } + }); + + if (file.Count > 0) + { + return HttpUtility.UrlDecode(file[0].Path.AbsolutePath); + } + + return string.Empty; + } + + public async Task> SelectMultiVideoFileAsync() + { + if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || + desktop.MainWindow?.StorageProvider is not { } provider) + throw new NullReferenceException("Missing StorageProvider instance."); + + // Get top level from the current control. Alternatively, you can use Window reference instead. + var topLevel = TopLevel.GetTopLevel(desktop.MainWindow); + + // Start async operation to open the dialog. + var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = Ioc.Default.GetRequiredService().GetString("SelectVideoFile"), + AllowMultiple = true, + FileTypeFilter = new List + { + new FilePickerFileType("mp4") + { + Patterns = new List() { "*.mp4" } + } + } + }); + + var result = new List(); + foreach (var file in files) + { + result.Add(HttpUtility.UrlDecode(file.Path.AbsolutePath)); + } + + return result; + } + +} \ No newline at end of file diff --git a/src/Downkyi/Themes/Default/Color.axaml b/src/Downkyi/Themes/Default/Color.axaml new file mode 100644 index 0000000..0d40d6d --- /dev/null +++ b/src/Downkyi/Themes/Default/Color.axaml @@ -0,0 +1,60 @@ + + + Black + Black + white + + white + black + #FF6ECEF8 + #FF30B8F6 + #FFE81123 + #FFF1707A + + #FFFF4D3C + + #FF00A1D6 + #C800A1D6 + #7F00A1D6 + #3300A1D6 + + #FFFB7299 + + #FF00A1D6 + #FFFFFFFF + #FFFFFFFF + #FF00A1D6 + #FFBDBDBD + #FFE4E4E4 + #FFBDBDBD + #C8BDBDBD + #7FBDBDBD + #33BDBDBD + + #7F000000 + #33999999 + + #FFF4F4F4 + + #FFF4F4F4 + + #FF999999 + #7F999999 + #99000000 + + #7FD0D0D0 + #7FFFFFFF + #7FD0D0D0 + + #FFE4E4E4 + #FFF3CB85 + + white + black + #FF999999 + #FF757575 + + #FFFFAE00 + #FF02B5DA + + diff --git a/src/Downkyi/Themes/Default/ColorBrush.axaml b/src/Downkyi/Themes/Default/ColorBrush.axaml new file mode 100644 index 0000000..a69d0bd --- /dev/null +++ b/src/Downkyi/Themes/Default/ColorBrush.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles.axaml b/src/Downkyi/Themes/Default/Styles.axaml new file mode 100644 index 0000000..6e7acdf --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles.axaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/Button.axaml b/src/Downkyi/Themes/Default/Styles/Button.axaml new file mode 100644 index 0000000..7abb96e --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/Button.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/CheckBox.axaml b/src/Downkyi/Themes/Default/Styles/CheckBox.axaml new file mode 100644 index 0000000..8fb340c --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/CheckBox.axaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/ComboBox.axaml b/src/Downkyi/Themes/Default/Styles/ComboBox.axaml new file mode 100644 index 0000000..d9e6cf2 --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/ComboBox.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/DataGrid.axaml b/src/Downkyi/Themes/Default/Styles/DataGrid.axaml new file mode 100644 index 0000000..6b0730f --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/DataGrid.axaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/ItemsControl.axaml b/src/Downkyi/Themes/Default/Styles/ItemsControl.axaml new file mode 100644 index 0000000..ade2375 --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/ItemsControl.axaml @@ -0,0 +1,24 @@ + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/ItemsRepeater.axaml b/src/Downkyi/Themes/Default/Styles/ItemsRepeater.axaml new file mode 100644 index 0000000..604aae0 --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/ItemsRepeater.axaml @@ -0,0 +1,29 @@ + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/ListBox.axaml b/src/Downkyi/Themes/Default/Styles/ListBox.axaml new file mode 100644 index 0000000..35ddd71 --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/ListBox.axaml @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/RadioButton.axaml b/src/Downkyi/Themes/Default/Styles/RadioButton.axaml new file mode 100644 index 0000000..3eec13d --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/RadioButton.axaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/Downkyi/Themes/Default/Styles/TextBox.axaml b/src/Downkyi/Themes/Default/Styles/TextBox.axaml new file mode 100644 index 0000000..cae2c41 --- /dev/null +++ b/src/Downkyi/Themes/Default/Styles/TextBox.axaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/Themes/Default/Theme.axaml b/src/Downkyi/Themes/Default/Theme.axaml new file mode 100644 index 0000000..9dd0b58 --- /dev/null +++ b/src/Downkyi/Themes/Default/Theme.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/Downkyi/ViewLocator.cs b/src/Downkyi/ViewLocator.cs new file mode 100644 index 0000000..317b05c --- /dev/null +++ b/src/Downkyi/ViewLocator.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Downkyi.UI.Mvvm; +using System; + +namespace Downkyi; + +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? data) + { + string name = data!.GetType().FullName!.Replace("ViewModel", "View"); + + if (name.Contains(".UI")) + { + name = name.Replace(".UI", ""); + } + if (name.Contains("Proxy")) + { + name = name.Replace("Proxy", ""); + } + + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/src/Downkyi/ViewModels/Login/QRCodeViewModelProxy.cs b/src/Downkyi/ViewModels/Login/QRCodeViewModelProxy.cs new file mode 100644 index 0000000..5db5cb7 --- /dev/null +++ b/src/Downkyi/ViewModels/Login/QRCodeViewModelProxy.cs @@ -0,0 +1,52 @@ +using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.ComponentModel; +using Downkyi.UI.Mvvm; +using Downkyi.UI.ViewModels.Login; +using QRCoder; +using System.IO; + +namespace Downkyi.ViewModels.Login; + +public partial class QRCodeViewModelProxy : QRCodeViewModel +{ + #region 页面属性申明 + + [ObservableProperty] + private Bitmap? _QRCode; + + #endregion + + public QRCodeViewModelProxy(BaseServices baseServices) : base(baseServices) + { + // 根据请求到的url生成二维码 + _QRCodeAction = new((url) => + { + byte[]? qrcodeByte = GenerateQrCode(url, 10); + using var ms = new MemoryStream(qrcodeByte); + Bitmap img = new(ms); + QRCode = img; + }); + } + + protected override void InitStatus() + { + base.InitStatus(); + + QRCode = null; + } + + /// + /// 生成二维码 + /// + /// 信息 + /// 版本 1 ~ 40 + private static byte[] GenerateQrCode(string msg, int version) + { + QRCodeGenerator qrGenerator = new(); + QRCodeData qrCodeData = qrGenerator.CreateQrCode(msg, QRCodeGenerator.ECCLevel.H/* 这里设置容错率的一个级别 */, true, false, QRCodeGenerator.EciMode.Utf8, version); + BitmapByteQRCode qrCode = new(qrCodeData); + var codeByte = qrCode.GetGraphic(20); + return codeByte; + } + +} \ No newline at end of file diff --git a/src/Downkyi/ViewModels/MainWindowViewModel.cs b/src/Downkyi/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..ed48015 --- /dev/null +++ b/src/Downkyi/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,115 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.DependencyInjection; +using Downkyi.Core.Utils; +using Downkyi.UI.Mvvm; +using Downkyi.UI.ViewModels; +using Downkyi.UI.ViewModels.DownloadManager; +using Downkyi.UI.ViewModels.Login; +using Downkyi.UI.ViewModels.Settings; +using Downkyi.UI.ViewModels.Toolbox; +using Downkyi.UI.ViewModels.User; +using System.Collections.Generic; +using System.Threading; + +namespace Downkyi.ViewModels; + +public partial class MainWindowViewModel : ViewModelBase +{ + public const string Key = "Main"; + + private readonly LinkStack _pages = new(); + + #region 页面属性申明 + + [ObservableProperty] + private ViewModelBase? _content = null; + + [ObservableProperty] + private bool _messageVisibility = false; + + [ObservableProperty] + private string _message = string.Empty; + + #endregion + + public MainWindowViewModel(BaseServices baseServices) : base(baseServices) + { + Dictionary parameter = new() + { + { "key", Key }, + { "result", "start" }, + }; + Forward(IndexViewModel.Key, parameter); + + // 订阅消息发送事件 + string oldMessage; + NotificationEvent.Subscribe((message) => + { + oldMessage = Message; + Message = message; + int sleep = 2000; + if (oldMessage == Message) { sleep = 1500; } + + MessageVisibility = true; + Thread.Sleep(sleep); + MessageVisibility = false; + }); + + } + + public void Forward(string viewKey, Dictionary? parameter = null) + { + var viewModel = SetContent(viewKey); + if (viewModel == null) { return; } + + viewModel.OnNavigatedTo(parameter); + Content = viewModel; + + _pages.Push(viewKey); + } + + public void Backward(Dictionary? parameter = null) + { + _pages.Pop(); + var viewModel = SetContent(_pages.Peek()); + if (viewModel == null) { return; } + + viewModel.OnNavigatedTo(parameter); + Content = viewModel; + } + + private static ViewModelBase? SetContent(string? viewKey) + { + if (viewKey == null) { return null; } + + ViewModelBase? viewModel = null; + switch (viewKey) + { + case IndexViewModel.Key: + viewModel = Ioc.Default.GetRequiredService(); + break; + case LoginViewModel.Key: + viewModel = Ioc.Default.GetRequiredService(); + break; + case MySpaceViewModel.Key: + viewModel = Ioc.Default.GetRequiredService(); + break; + case UserSpaceViewModel.Key: + viewModel = Ioc.Default.GetRequiredService(); + break; + case SettingsViewModel.Key: + viewModel = Ioc.Default.GetRequiredService(); + break; + case DownloadManagerViewModel.Key: + viewModel = Ioc.Default.GetRequiredService(); + break; + case ToolboxViewModel.Key: + viewModel = Ioc.Default.GetRequiredService(); + break; + default: + break; + } + return viewModel; + } + +} \ No newline at end of file diff --git a/src/Downkyi/ViewModels/Settings/AboutViewModelProxy.cs b/src/Downkyi/ViewModels/Settings/AboutViewModelProxy.cs new file mode 100644 index 0000000..8ad0925 --- /dev/null +++ b/src/Downkyi/ViewModels/Settings/AboutViewModelProxy.cs @@ -0,0 +1,112 @@ +using Downkyi.Models; +using Downkyi.UI.Mvvm; +using Downkyi.UI.ViewModels.Settings; +using System.Threading.Tasks; + +namespace Downkyi.ViewModels.Settings; + +public class AboutViewModelProxy : AboutViewModel +{ + public AboutViewModelProxy(BaseServices baseServices) : base(baseServices) + { + #region 属性初始化 + + AppInfo app = new(); + AppName = app.Name; + AppVersion = app.VersionName; + + AddThirdParty(); + + #endregion + } + + private async void AddThirdParty() + { + await Task.Run(() => + { + ThirdParties.Add(new() + { + Name = "Aria2cNet", + Author = "Leiurayer", + Version = "1.0.1", + License = "MIT", + Homepage = "https://github.com/leiurayer/Aria2cNet", + LicenseUrl = "https://licenses.nuget.org/MIT" + }); + ThirdParties.Add(new() + { + Name = "AsyncImageLoader.Avalonia", + Author = "SKProCH", + Version = "3.0.0-avalonia11-preview4", + License = "LICENSE", + Homepage = "https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia", + LicenseUrl = "https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia/blob/master/LICENSE" + }); + ThirdParties.Add(new() + { + Name = "Avalonia", + Author = "Avalonia Team", + Version = "11.0.0", + License = "MIT", + Homepage = "https://avaloniaui.net/", + LicenseUrl = "https://licenses.nuget.org/MIT" + }); + ThirdParties.Add(new() + { + Name = "CommunityToolkit.Mvvm", + Author = "Microsoft", + Version = "8.2.1", + License = "MIT", + Homepage = "https://github.com/CommunityToolkit/dotnet", + LicenseUrl = "https://licenses.nuget.org/MIT" + }); + ThirdParties.Add(new() + { + Name = "MessageBox.Avalonia", + Author = "Lary", + Version = "3.0.0", + License = "MIT", + Homepage = "https://github.com/CreateLab/MessageBox.Avalonia", + LicenseUrl = "https://licenses.nuget.org/MIT" + }); + ThirdParties.Add(new() + { + Name = "Newtonsoft.Json", + Author = "James Newton-King", + Version = "13.0.3", + License = "MIT", + Homepage = "https://www.newtonsoft.com/json", + LicenseUrl = "https://licenses.nuget.org/MIT" + }); + ThirdParties.Add(new() + { + Name = "NLog", + Author = "Jarek Kowalski et al.", + Version = "5.2.2", + License = "BSD-3-Clause", + Homepage = "https://nlog-project.org/", + LicenseUrl = "https://licenses.nuget.org/BSD-3-Clause" + }); + ThirdParties.Add(new() + { + Name = "QRCoder", + Author = "Raffael Herrmann", + Version = "1.4.3", + License = "MIT", + Homepage = "https://github.com/codebude/QRCoder/", + LicenseUrl = "https://licenses.nuget.org/MIT" + }); + + ThirdParties.Add(new() + { + Name = "sqlite-net-pcl", + Author = "SQLite-net", + Version = "1.8.116", + License = "LICENSE", + Homepage = "https://github.com/praeclarum/sqlite-net", + LicenseUrl = "https://www.nuget.org/packages/sqlite-net-pcl/1.8.116/license" + }); + }); + } + +} \ No newline at end of file diff --git a/src/Downkyi/ViewModels/Settings/DanmakuViewModelProxy.cs b/src/Downkyi/ViewModels/Settings/DanmakuViewModelProxy.cs new file mode 100644 index 0000000..b86cbd0 --- /dev/null +++ b/src/Downkyi/ViewModels/Settings/DanmakuViewModelProxy.cs @@ -0,0 +1,21 @@ +using Avalonia.Media; +using Downkyi.UI.Mvvm; +using Downkyi.UI.ViewModels.Settings; + +namespace Downkyi.ViewModels.Settings; + +public class DanmakuViewModelProxy : DanmakuViewModel +{ + public DanmakuViewModelProxy(BaseServices baseServices) : base(baseServices) + { + #region 属性初始化 + + var fonts = FontManager.Current.SystemFonts; + foreach (var font in fonts) + { + Fonts.Add(font.Name); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Downkyi/ViewModels/Settings/VideoViewModelProxy.cs b/src/Downkyi/ViewModels/Settings/VideoViewModelProxy.cs new file mode 100644 index 0000000..33d33ec --- /dev/null +++ b/src/Downkyi/ViewModels/Settings/VideoViewModelProxy.cs @@ -0,0 +1,128 @@ +using Avalonia.Controls; +using Avalonia.Input; +using CommunityToolkit.Mvvm.Input; +using Downkyi.Core.FileName; +using Downkyi.UI.Mvvm; +using Downkyi.UI.ViewModels.Settings; + +namespace Downkyi.ViewModels.Settings; + +public partial class VideoViewModelProxy : VideoViewModel +{ + public VideoViewModelProxy(BaseServices baseServices) : base(baseServices) + { + } + + #region 命令申明 + + /// + /// 可选文件名字段点击事件 + /// + /// + [RelayCommand] + private void SelectOptionalFields(object args) + { + if (args is PointerReleasedEventArgs releasedEventArgs) + { + if (releasedEventArgs.InitialPressMouseButton == MouseButton.Left) + { + if (releasedEventArgs.Source is Border border) + { + OptionalFieldsCommand(border.Child?.DataContext); + } + if (releasedEventArgs.Source is TextBlock text) + { + OptionalFieldsCommand(text.DataContext); + } + } + } + } + + /// + /// 选中文件名字段右键点击事件 + /// + /// + [RelayCommand] + private void SetSelectedFileName(object args) + { + if (args is PointerReleasedEventArgs releasedEventArgs) + { + if (releasedEventArgs.InitialPressMouseButton == MouseButton.Right) + { + if (releasedEventArgs.Source is Border border) + { + SelectedFileNameRightButton(border.Child?.DataContext); + } + if (releasedEventArgs.Source is TextBlock text) + { + SelectedFileNameRightButton(text.DataContext); + } + } + } + } + + #endregion + + protected override string DisplayFileNamePart(FileNamePart item) + { + string display = string.Empty; + switch (item) + { + case FileNamePart.ORDER: + display = DictionaryResource.GetString("DisplayOrder"); + break; + case FileNamePart.SECTION: + display = DictionaryResource.GetString("DisplaySection"); + break; + case FileNamePart.MAIN_TITLE: + display = DictionaryResource.GetString("DisplayMainTitle"); + break; + case FileNamePart.PAGE_TITLE: + display = DictionaryResource.GetString("DisplayPageTitle"); + break; + case FileNamePart.VIDEO_ZONE: + display = DictionaryResource.GetString("DisplayVideoZone"); + break; + case FileNamePart.AUDIO_QUALITY: + display = DictionaryResource.GetString("DisplayAudioQuality"); + break; + case FileNamePart.VIDEO_QUALITY: + display = DictionaryResource.GetString("DisplayVideoQuality"); + break; + case FileNamePart.VIDEO_CODEC: + display = DictionaryResource.GetString("DisplayVideoCodec"); + break; + case FileNamePart.VIDEO_PUBLISH_TIME: + display = DictionaryResource.GetString("DisplayVideoPublishTime"); + break; + case FileNamePart.AVID: + display = "avid"; + break; + case FileNamePart.BVID: + display = "bvid"; + break; + case FileNamePart.CID: + display = "cid"; + break; + case FileNamePart.UP_MID: + display = DictionaryResource.GetString("DisplayUpMid"); + break; + case FileNamePart.UP_NAME: + display = DictionaryResource.GetString("DisplayUpName"); + break; + } + + if (((int)item) >= 100) + { + display = HyphenSeparated.Hyphen[(int)item]; + } + + if (display == " ") + { + display = DictionaryResource.GetString("DisplaySpace"); + } + + return display; + } + +} \ No newline at end of file diff --git a/src/Downkyi/ViewModels/Toolbox/DelogoViewModelProxy.cs b/src/Downkyi/ViewModels/Toolbox/DelogoViewModelProxy.cs new file mode 100644 index 0000000..efe75cc --- /dev/null +++ b/src/Downkyi/ViewModels/Toolbox/DelogoViewModelProxy.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.Input; +using Downkyi.UI.Mvvm; +using Downkyi.UI.ViewModels.Toolbox; + +namespace Downkyi.ViewModels.Toolbox; + +public partial class DelogoViewModelProxy : DelogoViewModel +{ + public DelogoViewModelProxy(BaseServices baseServices) : base(baseServices) + { + } + + #region 命令申明 + + /// + /// Status改变事件 + /// + /// + [RelayCommand] + private void SetStatus(object parameter) + { + if (parameter is not TextBox output) { return; } + + // TextBox滚动到底部 + output.SelectionStart = Status.Length; + output.SelectionEnd = Status.Length; + } + + #endregion +} \ No newline at end of file diff --git a/src/Downkyi/ViewModels/Toolbox/ExtractMediaViewModelProxy.cs b/src/Downkyi/ViewModels/Toolbox/ExtractMediaViewModelProxy.cs new file mode 100644 index 0000000..329a27c --- /dev/null +++ b/src/Downkyi/ViewModels/Toolbox/ExtractMediaViewModelProxy.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.Input; +using Downkyi.UI.Mvvm; +using Downkyi.UI.ViewModels.Toolbox; + +namespace Downkyi.ViewModels.Toolbox; + +public partial class ExtractMediaViewModelProxy : ExtractMediaViewModel +{ + public ExtractMediaViewModelProxy(BaseServices baseServices) : base(baseServices) + { + } + + #region 命令申明 + + /// + /// Status改变事件 + /// + /// + [RelayCommand] + private void SetStatus(object parameter) + { + if (parameter is not TextBox output) { return; } + + // TextBox滚动到底部 + output.SelectionStart = Status.Length; + output.SelectionEnd = Status.Length; + } + + #endregion +} \ No newline at end of file diff --git a/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml b/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml new file mode 100644 index 0000000..837a983 --- /dev/null +++ b/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml @@ -0,0 +1,14 @@ + + Welcome to DownloadFinishedView! + diff --git a/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml.cs b/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml.cs new file mode 100644 index 0000000..7130f9d --- /dev/null +++ b/src/Downkyi/Views/DownloadManager/DownloadFinishedView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.DownloadManager +{ + public partial class DownloadFinishedView : UserControl + { + public DownloadFinishedView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml b/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml new file mode 100644 index 0000000..996c28b --- /dev/null +++ b/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml.cs b/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml.cs new file mode 100644 index 0000000..817db8e --- /dev/null +++ b/src/Downkyi/Views/DownloadManager/DownloadManagerView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.DownloadManager +{ + public partial class DownloadManagerView : UserControl + { + public DownloadManagerView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/Views/DownloadManager/DownloadingView.axaml b/src/Downkyi/Views/DownloadManager/DownloadingView.axaml new file mode 100644 index 0000000..7152cb4 --- /dev/null +++ b/src/Downkyi/Views/DownloadManager/DownloadingView.axaml @@ -0,0 +1,14 @@ + + Welcome to DownloadingView! + diff --git a/src/Downkyi/Views/DownloadManager/DownloadingView.axaml.cs b/src/Downkyi/Views/DownloadManager/DownloadingView.axaml.cs new file mode 100644 index 0000000..383ae0a --- /dev/null +++ b/src/Downkyi/Views/DownloadManager/DownloadingView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.DownloadManager +{ + public partial class DownloadingView : UserControl + { + public DownloadingView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/Views/IndexView.axaml b/src/Downkyi/Views/IndexView.axaml new file mode 100644 index 0000000..72f0200 --- /dev/null +++ b/src/Downkyi/Views/IndexView.axaml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Downkyi/Views/IndexView.axaml.cs b/src/Downkyi/Views/IndexView.axaml.cs new file mode 100644 index 0000000..a98581e --- /dev/null +++ b/src/Downkyi/Views/IndexView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views +{ + public partial class IndexView : UserControl + { + public IndexView() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/Downkyi/Views/Login/CookiesView.axaml b/src/Downkyi/Views/Login/CookiesView.axaml new file mode 100644 index 0000000..c957bbd --- /dev/null +++ b/src/Downkyi/Views/Login/CookiesView.axaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/Views/Login/LoginView.axaml.cs b/src/Downkyi/Views/Login/LoginView.axaml.cs new file mode 100644 index 0000000..430614c --- /dev/null +++ b/src/Downkyi/Views/Login/LoginView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.Login +{ + public partial class LoginView : UserControl + { + public LoginView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/Views/Login/QRCodeView.axaml b/src/Downkyi/Views/Login/QRCodeView.axaml new file mode 100644 index 0000000..0f0cde6 --- /dev/null +++ b/src/Downkyi/Views/Login/QRCodeView.axaml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/Views/Login/QRCodeView.axaml.cs b/src/Downkyi/Views/Login/QRCodeView.axaml.cs new file mode 100644 index 0000000..a6bc04f --- /dev/null +++ b/src/Downkyi/Views/Login/QRCodeView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.Login +{ + public partial class QRCodeView : UserControl + { + public QRCodeView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/Views/MainWindow.axaml b/src/Downkyi/Views/MainWindow.axaml new file mode 100644 index 0000000..e7f0b1a --- /dev/null +++ b/src/Downkyi/Views/MainWindow.axaml @@ -0,0 +1,42 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Downkyi/Views/MainWindow.axaml.cs b/src/Downkyi/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..327d22e --- /dev/null +++ b/src/Downkyi/Views/MainWindow.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views +{ + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/Downkyi/Views/Settings/AboutView.axaml b/src/Downkyi/Views/Settings/AboutView.axaml new file mode 100644 index 0000000..f936422 --- /dev/null +++ b/src/Downkyi/Views/Settings/AboutView.axaml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/Views/Settings/SettingsView.axaml.cs b/src/Downkyi/Views/Settings/SettingsView.axaml.cs new file mode 100644 index 0000000..d5c8a0e --- /dev/null +++ b/src/Downkyi/Views/Settings/SettingsView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.Settings +{ + public partial class SettingsView : UserControl + { + public SettingsView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/Views/Settings/VideoView.axaml b/src/Downkyi/Views/Settings/VideoView.axaml new file mode 100644 index 0000000..59da197 --- /dev/null +++ b/src/Downkyi/Views/Settings/VideoView.axaml @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Downkyi/Views/Toolbox/ToolboxView.axaml.cs b/src/Downkyi/Views/Toolbox/ToolboxView.axaml.cs new file mode 100644 index 0000000..9e81914 --- /dev/null +++ b/src/Downkyi/Views/Toolbox/ToolboxView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.Toolbox +{ + public partial class ToolboxView : UserControl + { + public ToolboxView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/Views/User/MySpaceView.axaml b/src/Downkyi/Views/User/MySpaceView.axaml new file mode 100644 index 0000000..b4cc9b4 --- /dev/null +++ b/src/Downkyi/Views/User/MySpaceView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/src/Downkyi/Views/User/MySpaceView.axaml.cs b/src/Downkyi/Views/User/MySpaceView.axaml.cs new file mode 100644 index 0000000..4f8067a --- /dev/null +++ b/src/Downkyi/Views/User/MySpaceView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.User +{ + public partial class MySpaceView : UserControl + { + public MySpaceView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/Views/User/UserSpaceView.axaml b/src/Downkyi/Views/User/UserSpaceView.axaml new file mode 100644 index 0000000..4468f49 --- /dev/null +++ b/src/Downkyi/Views/User/UserSpaceView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/src/Downkyi/Views/User/UserSpaceView.axaml.cs b/src/Downkyi/Views/User/UserSpaceView.axaml.cs new file mode 100644 index 0000000..8e1f430 --- /dev/null +++ b/src/Downkyi/Views/User/UserSpaceView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Downkyi.Views.User +{ + public partial class UserSpaceView : UserControl + { + public UserSpaceView() + { + InitializeComponent(); + } + } +} diff --git a/src/Downkyi/app.manifest b/src/Downkyi/app.manifest new file mode 100644 index 0000000..e0ce8d0 --- /dev/null +++ b/src/Downkyi/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..9b191a5 --- /dev/null +++ b/src/README.md @@ -0,0 +1 @@ +# Downkyi \ No newline at end of file diff --git a/third_party/aria2-1.36.0-aarch64-linux-android-build1.zip b/third_party/aria2-1.36.0-aarch64-linux-android-build1.zip new file mode 100644 index 0000000..ff916da Binary files /dev/null and b/third_party/aria2-1.36.0-aarch64-linux-android-build1.zip differ diff --git a/third_party/aria2-1.36.0-win-32bit-build1.zip b/third_party/aria2-1.36.0-win-32bit-build1.zip new file mode 100644 index 0000000..99fe5c1 Binary files /dev/null and b/third_party/aria2-1.36.0-win-32bit-build1.zip differ diff --git a/third_party/aria2-1.36.0-win-64bit-build1.zip b/third_party/aria2-1.36.0-win-64bit-build1.zip new file mode 100644 index 0000000..260e3a4 Binary files /dev/null and b/third_party/aria2-1.36.0-win-64bit-build1.zip differ diff --git a/third_party/aria2-1.36.0.tar.xz b/third_party/aria2-1.36.0.tar.xz new file mode 100644 index 0000000..2f1e333 Binary files /dev/null and b/third_party/aria2-1.36.0.tar.xz differ