diff --git a/.gitignore b/.gitignore index 62f0e2ef..169794e4 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,15 @@ logs/ saves/ build/ dist/ -*.spec \ No newline at end of file +*.spec + +# Custom files +.cookies.txt +cookies.txt +.config.ini +config.ini +chaoxing.log +.chaoxing.log +./config.ini +./chaoxing.log +./cookies.txt diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9c214a38..00000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3.9-alpine as builder -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories -RUN apk update && apk add gcc musl-dev linux-headers -COPY . /app -WORKDIR /app -RUN pip install -r requirements.txt -RUN pip install pyinstaller==5.9.0 -RUN pyinstaller -F main.py -n chaoxing - -FROM alpine:3.17 -COPY --from=builder /app/dist /app -WORKDIR /app -CMD ["./chaoxing"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702d..00000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - 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 index abece506..b084c20e 100644 --- a/README.md +++ b/README.md @@ -19,23 +19,21 @@ :star: 觉得有帮助的朋友可以给个Star ## :point_up: 更新通知 -20221123更新通知: 已由[B1gM8c](https://github.com/B1gM8c)修复重复获取章节BUG +20231114更新通知: 3.0大版本更新,修复403报错,优化代码结构 ## :books: 使用方法 -### 源码运行(推荐) -1. 提前准备: Python版本>=3.9 因为使用到了:=表达式。urllib3=1.25.11 因为后面的版本对代理的支持有变化。 -2. `git clone --depth=1 https://github.com/Samueli924/chaoxing` 项目至本地 -3. `cd chaoxing` -4. `pip install -r requirements.txt` -5. `python main.py` 运行程序 -6. 可选参数 -debug 开启DEBUG模式 --no-log 不输出日志 --no-logo 隐藏开头项目LOGO --no-sec 关闭隐私保护 +### 源码运行 +1. `git clone --depth=1 https://github.com/Samueli924/chaoxing` 项目至本地 +2. `cd chaoxing` +3. `pip install -r requirements.txt` +4. (可选配置文件运行)复制config_template.ini文件为config.ini文件,修改文件内的账号密码内容, 执行 `python main.py -c config.ini` +5. (可选命令行运行)`python main.py -u 手机号 -p 密码 -l 课程ID1,课程ID2,课程ID3...(可选)` -### 使用Docker运行 -1. `git clone --depth=1 https://github.com/Samueli924/chaoxing` 获取项目源码 -2. `docker-compose run --rm app`, 在交互式终端中运行容器 - -你可以在终端中使用`ctrl+p+q`来让容器退出交互式终端并在后台运行 +### 打包文件运行 +1. 从最新[Releases](https://github.com/Samueli924/chaoxing/releases)中下载exe文件 +2. (可选配置文件运行)下载config_template.ini文件保存为config.ini文件,修改文件内的账号密码内容, 执行 `./chaoxing.exe -c config.ini` +3. (可选命令行运行)`./chaoxing.exe -u "手机号" -p "密码" -l 课程ID1,课程ID2,课程ID3...(可选)` ## :heart: CONTRIBUTORS ### :one:感谢[huajijam](https://github.com/huajijam)对chaoxing项目的贡献! [PR #73](https://github.com/Samueli924/chaoxing/pull/73) @@ -46,10 +44,9 @@ ### :six:感谢[a81608882](https://github.com/a81608882)修复403报错BUG[Pull #142](https://github.com/Samueli924/chaoxing/pull/142) ### :seven:感谢[yhm97](https://github.com/yhm97)修复音频格式导致的403BUG[Pull #187](https://github.com/Samueli924/chaoxing/pull/187) ### :eight:感谢[evibhm](https://github.com/evibhm)修复Docker运行的DNS问题、优化Docker映像大小[Pull #232](https://github.com/Samueli924/chaoxing/pull/232) -### 对于代码有任何问题或建议欢迎Pull&Request - +### 对于代码有任何问题或建议欢迎Pull&Request ## :warning: 免责声明 - 本代码遵循 [GPL-3.0 License](https://github.com/Samueli924/chaoxing/blob/main/LICENSE)协议,允许**开源/免费使用和引用/修改/衍生代码的开源/免费使用**,不允许**修改和衍生的代码作为闭源的商业软件发布和销售**,禁止**使用本代码盈利**,以此代码为基础的程序**必须**同样遵守[GPL-3.0 License](https://github.com/Samueli924/chaoxing/blob/main/LICENSE)协议 - 本代码仅用于**学习讨论**,禁止**用于盈利** -- 他人或组织使用本代码进行的任何**违法行为**与本人无关 +- 他人或组织使用本代码进行的任何**违法行为**与本人无关 \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore deleted file mode 100644 index 0e4f7f49..00000000 --- a/api/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -__pycache__/ -*.py[cod] -*$py.class \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py index e69de29b..a8039929 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +def formatted_output(_status, _text, _data): + return {"status": _status, "msg": _text, "data": _data} \ No newline at end of file diff --git a/api/base.py b/api/base.py new file mode 100644 index 00000000..59aff43f --- /dev/null +++ b/api/base.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +import re +import requests +import time +import random +from hashlib import md5 + +from api import formatted_output +from api.cipher import AESCipher +from api.logger import logger +from api.cookies import save_cookies, use_cookies +from api.process import show_progress +from api.config import GlobalConst as gc +from api.decode import (decode_course_list, + decode_course_point, + decode_course_card) + + +def get_timestamp(): + return str(int(time.time() * 1000)) + + +def get_random_seconds(): + return random.randint(30, 90) + + +def init_session(isVideo: bool = False): + _session = requests.session() + if isVideo: + _session.headers = gc.VIDEO_HEADERS + else: + _session.headers = gc.HEADERS + _session.cookies.update(use_cookies()) + return _session + + +class Account: + username = None + password = None + last_login = None + isSuccess = None + def __init__(self, _username, _password): + self.username = _username + self.password = _password + + +class Chaoxing: + def __init__(self, account: Account = None): + self.account = account + self.cipher = AESCipher() + + def login(self): + _session = requests.session() + _url = "https://passport2.chaoxing.com/fanyalogin" + _data = {"fid": "-1", + "uname": self.cipher.encrypt(self.account.username), + "password": self.cipher.encrypt(self.account.password), + "refer": "https%3A%2F%2Fi.chaoxing.com", + "t": True, + "forbidotherlogin": 0, + "validate": "", + "doubleFactorLogin": 0, + "independentId": 0 + } + logger.trace("正在尝试登录...") + resp = _session.post(_url, headers=gc.HEADERS, data=_data) + if resp and resp.json()["status"] == True: + save_cookies(_session) + logger.info("登录成功...") + return {"status": True, "msg": "登录成功"} + else: + return {"status": False, "msg": str(resp.json()["msg2"])} + + def get_fid(self): + _session = init_session() + return _session.cookies.get("fid") + + def get_uid(self): + _session = init_session() + return _session.cookies.get("_uid") + + def get_course_list(self): + _session = init_session() + _url = "https://mooc2-ans.chaoxing.com/mooc2-ans/visit/courselistdata" + _data = { + "courseType": 1, + "courseFolderId": 0, + "query": "", + "superstarClass": 0 + } + logger.trace("正在读取所有的课程列表...") + _resp = _session.post(_url, data=_data) + logger.info("课程列表读取完毕...") + return decode_course_list(_resp.text) + + def get_course_point(self, _courseid, _clazzid, _cpi): + _session = init_session() + _url = f"https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentcourse?courseid={_courseid}&clazzid={_clazzid}&cpi={_cpi}&ut=s" + logger.trace("开始读取课程所有章节...") + _resp = _session.get(_url) + logger.info("课程章节读取成功...") + return decode_course_point(_resp.text) + + def get_job_list(self, _clazzid, _courseid, _cpi, _knowledgeid): + _session = init_session() + _url = f"https://mooc1.chaoxing.com/mooc-ans/knowledge/cards?clazzid={_clazzid}&courseid={_courseid}&knowledgeid={_knowledgeid}&num=0&ut=s&cpi={_cpi}&v=20160407-3&mooc2=1" + logger.trace("开始读取章节所有任务点...") + _resp = _session.get(_url) + _job_list, _job_info = decode_course_card(_resp.text) + logger.info("章节任务点读取成功...") + return _job_list, _job_info + + def get_enc(self, clazzId, jobid, objectId, playingTime, duration, userid): + # https://github.com/ZhyMC/chaoxing-xuexitong-autoflush/blob/445c8d8a8cc63472dd90cdf2a6ab28542c56d93b/logger.js + return md5( + f"[{clazzId}][{userid}][{jobid}][{objectId}][{playingTime * 1000}][d_yHJ!$pdA~5][{duration * 1000}][0_{duration}]" + .encode()).hexdigest() + + def video_progress_log(self, _session, _course, _job, _job_info, _dtoken, _duration, _playingTime): + _url = (f"https://mooc1.chaoxing.com/mooc-ans/multimedia/log/a/" + f"{_course['cpi']}/" + f"{_dtoken}?" + f"clazzId={_course['clazzId']}&" + f"playingTime=0&" + f"duration={_duration}&" + f"clipTime=0_{_duration}&" + f"objectId={_job['objectid']}&" + f"otherInfo={_job['otherinfo']}&" + f"courseId={_course['courseId']}&" + f"jobid={_job['jobid']}&" + f"userid={self.get_uid()}&" + f"isdrag=3&" + f"view=pc&" + f"enc={self.get_enc(_course['clazzId'], _job['jobid'], _job['objectid'], _playingTime, _duration, self.get_uid())}&" + f"rt=0.9&" + f"dtype=Video&" + f"_t={get_timestamp()}") + resp = _session.get(_url) + return resp.json()["isPassed"] + + def study_video(self, _course, _job, _job_info, _speed: float = 1): + _session = init_session(isVideo=True) + _session.headers.update() + _info_url = f"https://mooc1.chaoxing.com/ananas/status/{_job['objectid']}?k={self.get_fid()}&flag=normal" + _video_info = _session.get(_info_url).json() + _dtoken = _video_info["dtoken"] # 7d8349c683a65af92b68ed42b9eebfc1 + _duration = _video_info["duration"] + _crc = _video_info["crc"] # 993fd31ba058895723016fa4fe9a6486 + _key = _video_info["key"] # 71980072a32dd57d151f7c3f6b3b122c + _isPassed = False + _isFinished = False + _playingTime = 0 + logger.info(f"开始任务:{_job['name']}, 总时长: {_duration}秒") + while not _isPassed: + if _isFinished: + _playingTime = _duration + _isPassed = self.video_progress_log(_session, _course, _job, _job_info, _dtoken, _duration, 0) + if _isPassed: + break + _wait_time = get_random_seconds() + if _playingTime + _wait_time >= int(_duration): + _wait_time = int(_duration) - _playingTime + _isFinished = True + # 播放进度条 + show_progress(_job['name'], _playingTime, _wait_time, _duration, _speed) + _playingTime += _wait_time + logger.info(f"\n任务完成:{_job['name']}") + + def study_document(self, _course, _job): + _session = init_session() + _url = f"https://mooc1.chaoxing.com/ananas/job/document?jobid={_job['jobid']}&knowledgeid={re.findall('nodeId_(.*?)-', _job['otherinfo'])[0]}&courseid={_course['courseId']}&clazzid={_course['clazzId']}&jtoken={_job['jtoken']}&_dc={get_timestamp()}" + _resp = _session.get(_url) diff --git a/api/chaoxing.py b/api/chaoxing.py deleted file mode 100644 index facdf982..00000000 --- a/api/chaoxing.py +++ /dev/null @@ -1,321 +0,0 @@ -import json -import random -import re -import secrets -import time -from base64 import b64encode -from hashlib import md5 - -from Crypto.Cipher import AES -import binascii - -import requests -from requests.utils import dict_from_cookiejar - -from utils.functions import Logger -from utils.functions import pretty_print, sort_missions, get_enc_time, show_progress, save_users - - -class Chaoxing: - - def __init__(self, usernm, passwd, debug, show): - self.usernm = usernm - self.passwd = passwd - self.logger = Logger("ChaoxingAPI", debug, show) - self.session = None - self.uid = None - self.cookies = None - self.courses = None - self.selected_course = None - self.missions = None - self.speed = None - - def init_explorer(self): - self.session = requests.session() - self.session.headers = { - 'User-Agent': - f'Dalvik/2.1.0 (Linux; U; Android {random.randint(9, 12)}; MI{random.randint(10, 12)} Build/SKQ1.210216.001) (device:MI{random.randint(10, 12)}) Language/zh_CN com.chaoxing.mobile/ChaoXingStudy_3_5.1.4_android_phone_614_74 (@Kalimdor)_{secrets.token_hex(16)}', - 'X-Requested-With': 'com.chaoxing.mobile' - } - - def re_init_login(self): - del self.session - self.session = requests.session() - self.session.headers = { - 'User-Agent': - f'Dalvik/2.1.0 (Linux; U; Android {random.randint(9, 12)}; MI{random.randint(10, 12)} Build/SKQ1.210216.001) (device:MI{random.randint(10, 12)}) Language/zh_CN com.chaoxing.mobile/ChaoXingStudy_3_5.1.4_android_phone_614_74 (@Kalimdor)_{secrets.token_hex(16)}', - 'X-Requested-With': 'com.chaoxing.mobile' - } - self.login() - - def pkcs7padding(self, text): - """ - 明文使用PKCS7填充 - """ - bs = 16 - length = len(text) - bytes_length = len(text.encode('utf-8')) - padding_size = length if (bytes_length == length) else bytes_length - padding = bs - padding_size % bs - padding_text = chr(padding) * padding - self.coding = chr(padding) - return text + padding_text - - def encryptByAES(self, message): - keyword = "u2oh6Vu^HWe4_AES" - key = keyword.encode('utf-8') - iv = keyword.encode('utf-8') - - cipher = AES.new(key, AES.MODE_CBC, iv) - # 处理明文 - content_padding = self.pkcs7padding(message) - # 加密 - encrypt_bytes = cipher.encrypt(content_padding.encode('utf-8')) - # 重新编码 - result = str(b64encode(encrypt_bytes), encoding='utf-8') - return result - - def login(self): - """ - 登录 - :return: - """ - url = "https://passport2.chaoxing.com/fanyalogin" - data = { - "fid": "-1", - "uname": self.encryptByAES(self.usernm), - "password": self.encryptByAES(self.passwd), - "t": "true", - "refer": "http%3A%2F%2Fi.chaoxing.com", - "forbidotherlogin": "0", - "validate": "", - "doubleFactorLogin": "0", - "independentId": "0" - } - - self.logger.debug("发送登录数据") - resp = self.session.post(url, data=data) - self.logger.debug("收到返回数据") - if resp.json()["status"]: - self.uid = resp.cookies['_uid'] - self.cookies = dict_from_cookiejar(resp.cookies) - save_users(usernm=self.usernm, passwd=self.passwd) - return True - else: - self.logger.error("登录失败:" + str(resp.json())) - return False - - def status(self): - """ - 检测Cookies是否有效 - :return: - """ - if not re.findall( - "用户登录", - self.session.get("https://i.chaoxing.com/base").text): - self.logger.debug("用户Cookies有效") - return True - else: - return False - - def get_current_ms(self): - return round(time.time() * 1000) - - def get_all_courses(self): - url = 'https://mooc1-api.chaoxing.com/mycourse/backclazzdata?view=json&mcode=' - courses = self.session.get(url).json() - if courses["result"] == 1: # 假如返回值为1 - __temp = courses["channelList"] - for course in __temp: # 删除所有的自建课程 - if "course" not in course['content']: - __temp.remove(course) - self.courses = __temp - return True - else: - self.logger.error("无法获取相关课程数据") - return False - - def select_course(self): - pretty_print(self.courses) - index = int(input("请输入您要学习的课程序号:")) - self.selected_course = self.courses[index - 1] - self.logger.debug("---selected_course info begin---") - self.logger.debug(self.selected_course) - self.logger.debug("---selected_course info end---") - return True - - def get_selected_course_data(self): - url = 'https://mooc1-api.chaoxing.com/gas/clazz' - params = { - 'id': self.selected_course["key"], - 'fields': - 'id,bbsid,classscore,isstart,allowdownload,chatid,name,state,isthirdaq,isfiled,information,discuss,visiblescore,begindate,coursesetting.fields(id,courseid,hiddencoursecover,hiddenwrongset,coursefacecheck),course.fields(id,name,infocontent,objectid,app,bulletformat,mappingcourseid,imageurl,teacherfactor,knowledge.fields(id,name,indexOrder,parentnodeid,status,layer,label,begintime,endtime,attachment.fields(id,type,objectid,extension).type(video)))', - 'view': 'json' - } - self.missions = sort_missions( - self.session.get(url, params=params).json()["data"][0]["course"] - ["data"][0]["knowledge"]["data"]) - return True - - def get_mission(self, mission_id, course_id): - url = 'https://mooc1-api.chaoxing.com/gas/knowledge' - enc = get_enc_time() - params = { - 'id': mission_id, - 'courseid': course_id, - 'fields': - 'id,parentnodeid,indexorder,label,layer,name,begintime,createtime,lastmodifytime,status,jobUnfinishedCount,clickcount,openlock,card.fields(id,knowledgeid,title,knowledgeTitile,description,cardorder).contentcard(all)', - 'view': 'json', - 'token': "4faa8662c59590c6f43ae9fe5b002b42", - '_time': enc[0], - 'inf_enc': enc[1] - } - return self.session.get(url, params=params).json() - - def get_knowledge(self, clazzid, courseid, knowledgeid, num): - url = 'https://mooc1-api.chaoxing.com/knowledge/cards' - params = { - 'clazzid': clazzid, - 'courseid': courseid, - 'knowledgeid': knowledgeid, - 'num': num, - 'isPhone': 1, - 'control': True, - } - return self.session.get(url, params=params).text - - def get_attachments(self, text): - if res := re.search( - r'window\.AttachmentSetting =({.*"hiddenConfig":false,.*"attachments":.*})', - text): - attachments = json.loads(res[1]) - self.logger.debug("---attachments info begin---") - self.logger.debug(attachments) - self.logger.debug("---attachments info end---") - return attachments - - def get_d_token(self, objectid, fid): - url = 'https://mooc1-api.chaoxing.com/ananas/status/{}'.format( - objectid) - params = { - 'k': fid, - 'flag': 'normal', - '_dc': int(round(time.time() * 1000)) - } - self.logger.debug("获取视频信息") - d_token_raw = self.session.get(url, params=params) - self.logger.debug("视频信息已获取") - self.logger.debug("---d_token info begin---") - self.logger.debug(d_token_raw) - self.logger.debug("---d_token info end---") - try: - d_token = d_token_raw.json() - except: - self.logger.debug("出现JSONDecoder异常,正在跳过当前任务") - d_token = None - return d_token - - def get_enc(self, clazzId, jobid, objectId, playingTime, duration, userid): - # https://github.com/ZhyMC/chaoxing-xuexitong-autoflush/blob/445c8d8a8cc63472dd90cdf2a6ab28542c56d93b/logger.js - return md5( - f"[{clazzId}][{userid}][{jobid}][{objectId}][{playingTime * 1000}][d_yHJ!$pdA~5][{duration * 1000}][0_{duration}]" - .encode()).hexdigest() - - def add_log(self, personid, courseid, classid, encode): - log_url = f"https://fystat-ans.chaoxing.com/log/setlog?personid={personid}&courseId={courseid}&classId={classid}&encode={encode}" - self.logger.debug("录入学习记录") - resp = self.session.get(url=log_url) - self.logger.debug("收到录入结果") - if "success" in resp.text: - print("学习记录录入成功") - return True - else: - print("学习记录录入失败") - self.logger.debug("---resp.text info begin---") - self.logger.debug(resp.text) - self.logger.debug("---resp.text info end---") - - def main_pass_video(self, personid, dtoken, otherInfo, playingTime, - clazzId, duration, jobid, objectId, userid, dtype, - _tsp): - url = 'https://mooc1-api.chaoxing.com/multimedia/log/a/{}/{}'.format( - personid, dtoken) - # print(url) - params = { - 'otherInfo': - otherInfo, - 'playingTime': - str(playingTime), - 'duration': - str(duration), - # 'akid': None, - 'jobid': - jobid, - 'clipTime': - '0_{}'.format(duration), - 'clazzId': - str(clazzId), - 'objectId': - objectId, - 'userid': - userid, - 'isdrag': - '0', - 'enc': - self.get_enc(clazzId, jobid, objectId, playingTime, duration, - userid), - 'rt': - '0.9', # 'rt': '1.0', ?? - # 'dtype': 'Video', 音频文件为Audio - 'dtype': - dtype, - 'view': - 'pc', - '_t': - str(int(round(time.time() * 1000))) - } - mylist = [] - for key in params.items(): - my = "=".join(key) - mylist.append(my) - params = "&".join(mylist) - # print:(url+params) - tmp_response = self.session.get(url, params=params) - try: - result = tmp_response.json() - except Exception: - self.logger.debug("任务失败") - result = { - 'error': { - 'status_code': tmp_response.status_code, - 'text': tmp_response.text - } - } - return result - # return self.session.get(url, params=params).json() - - def pass_video(self, video_duration, cpi, dtoken, otherInfo, clazzid, - jobid, objectid, userid, name, speed, dtype, _tsp): - sec = 58 - playingTime = 0 - print("当前播放速率:" + str(speed) + "倍速") - while True: - if sec >= 58: - sec = 0 - res = self.main_pass_video(cpi, dtoken, otherInfo, playingTime, - clazzid, video_duration, jobid, - objectid, userid, dtype, _tsp) - print(res) - if res.get('isPassed'): - show_progress(name, video_duration, video_duration) - break - elif res.get('error'): - self.logger.debug("---result info begin---") - self.logger.debug(res) - self.logger.debug("---result info end---") - raise Exception('出现错误') - continue - show_progress(name, playingTime, video_duration) - playingTime += 1 * self.speed - sec += 1 * self.speed - time.sleep(1) diff --git a/api/cipher.py b/api/cipher.py new file mode 100644 index 00000000..d3cb348c --- /dev/null +++ b/api/cipher.py @@ -0,0 +1,56 @@ +# -*- coding:utf-8 -*- +# -*- coding: utf-8 -*- +import base64 + +import pyaes + +from api.config import GlobalConst as gc + + +def pkcs7_unpadding(string): + return string[0:-ord(string[-1])] + + +def pkcs7_padding(s, block_size=16): + bs = block_size + return s + (bs - len(s) % bs) * chr(bs - len(s) % bs).encode() + + +def split_to_data_blocks(byte_str, block_size=16): + length = len(byte_str) + j, y = divmod(length, block_size) + blocks = [] + shenyu = j * block_size + for i in range(j): + start = i * block_size + end = (i + 1) * block_size + blocks.append(byte_str[start:end]) + stext = byte_str[shenyu:] + if stext: + blocks.append(stext) + return blocks + + +class AESCipher(): + def __init__(self): + self.key = str(gc.AESKey).encode("utf8") + self.iv = str(gc.AESKey).encode("utf8") + + def encrypt(self, plaintext: str): + ciphertext = b'' + cbc = pyaes.AESModeOfOperationCBC(self.key, self.iv) + plaintext = plaintext.encode('utf-8') + blocks = split_to_data_blocks(pkcs7_padding(plaintext)) + for b in blocks: + ciphertext = ciphertext + cbc.encrypt(b) + base64_text = base64.b64encode(ciphertext).decode("utf8") + return base64_text + + # def decrypt(self, ciphertext: str): + # cbc = pyaes.AESModeOfOperationCBC(self.key, self.iv) + # ciphertext.encode('utf8') + # ciphertext = base64.b64decode(ciphertext) + # ptext = b"" + # for b in split_to_data_blocks(ciphertext): + # ptext = ptext + cbc.decrypt(b) + # return pkcs7_unpadding(ptext.decode()) \ No newline at end of file diff --git a/api/config.py b/api/config.py new file mode 100644 index 00000000..251233c2 --- /dev/null +++ b/api/config.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +class GlobalConst: + AESKey = "u2oh6Vu^HWe4_AES" + HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", + "Sec-Ch-Ua": '"Chromium";v="118", "Google Chrome";v="118", "Not=A?Brand";v="99"' + } + COOKIES_PATH = "cookies.txt" + VIDEO_HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", + "Referer": "https://mooc1.chaoxing.com/ananas/modules/video/index.html?v=2023-1110-1610", + "Host": "mooc1.chaoxing.com" + } + THRESHOLD = 3 \ No newline at end of file diff --git a/api/cookies.py b/api/cookies.py new file mode 100644 index 00000000..617c1609 --- /dev/null +++ b/api/cookies.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +import os.path +import pickle + +from api.config import GlobalConst as gc + + +def save_cookies(_session): + with open(gc.COOKIES_PATH, 'wb') as f: + pickle.dump(_session.cookies, f) + + +def use_cookies(): + if os.path.exists(gc.COOKIES_PATH): + with open(gc.COOKIES_PATH, 'rb') as f: + _cookies = pickle.load(f) + return _cookies \ No newline at end of file diff --git a/api/decode.py b/api/decode.py new file mode 100644 index 00000000..fcc9888a --- /dev/null +++ b/api/decode.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +import json +from bs4 import BeautifulSoup +import re +from api.logger import logger + + +def decode_course_list(_text): + logger.trace("开始解码课程列表...") + _soup = BeautifulSoup(_text, "lxml") + _raw_courses = _soup.select("li.course") + _course_list = list() + for course in _raw_courses: + _course_detail = {} + _course_detail["id"] = course.attrs["id"] + _course_detail["info"] = course.attrs["info"] + _course_detail["roleid"] = course.attrs["roleid"] + _course_detail["clazzId"] = course.select_one("input.clazzId").attrs["value"] + _course_detail["courseId"] = course.select_one("input.courseId").attrs["value"] + _course_detail["cpi"] = re.findall("cpi=(.*?)&", course.select_one("a").attrs["href"])[0] + _course_detail["title"] = course.select_one("span.course-name").attrs["title"] + _course_detail["desc"] = course.select_one("p.margint10").attrs["title"] + _course_detail["teacher"] = course.select_one("p.color3").attrs["title"] + _course_list.append(_course_detail) + return _course_list + + +def decode_course_point(_text): + logger.trace("开始解码章节列表...") + _soup = BeautifulSoup(_text, "lxml") + _course_point = {} + _point_list = [] + _course_point["finish_num"] = _soup.select_one("div.chapter_head h2.xs_head_name span").text + _course_point["total_num"] = re.findall("/(\d{1,10})", _soup.select_one("div.chapter_head h2.xs_head_name").text)[0] + + _raw_points = _soup.select("div.chapter_item") + for _point in _raw_points: + if (not "id" in _point.attrs) or (not "title" in _point.attrs): + continue + _point_detail = {} + _point_detail["id"] = re.findall("^cur(\d{1,20})$", _point.attrs["id"])[0] + _point_detail["title"] = str(_point.select_one("span.catalog_sbar").text) + " " + str(_point.attrs["title"]) + _point_detail["jobCount"] = 0 + if _point.select_one("input.knowledgeJobCount"): + _point_detail["jobCount"] = _point.select_one("input.knowledgeJobCount").attrs["value"] + _point_list.append(_point_detail) + _course_point["points"] = _point_list + return _course_point + + +def decode_course_card(_text: str): + logger.trace("开始解码任务点列表...") + _temp = re.findall("mArg=\{(.*?)};", _text.replace(" ", "")) + if _temp: + _temp = _temp[0] + else: + return None + _cards = json.loads("{" + _temp + "}") + if _cards: + _job_info = {} + _job_info["ktoken"] = _cards["defaults"]["ktoken"] # 0754c189543882e328d711960117de81 + _job_info["mtEnc"] = _cards["defaults"]["mtEnc"] # 5a281a5c9819ae54f0b23bf97865133e + _job_info["reportTimeInterval"] = _cards["defaults"]["reportTimeInterval"] # 60 + _job_info["defenc"] = _cards["defaults"]["defenc"] # 48a25584121db3161e429c9b0861b3da + _job_info["cardid"] = _cards["defaults"]["cardid"] # 690554045 + _job_info["cpi"] = _cards["defaults"]["cpi"] # 82923364 + _job_info["qnenc"] = _cards["defaults"]["qnenc"] # a60d09ee89bc30d264e09d24b77e3ada + _job_info["ktoken"] = _cards["defaults"]["ktoken"] + _job_info["ktoken"] = _cards["defaults"]["ktoken"] + _job_info["ktoken"] = _cards["defaults"]["ktoken"] + _job_info["ktoken"] = _cards["defaults"]["ktoken"] + _cards = _cards["attachments"] + _job_list = [] + for _card in _cards: + # 已经通过的任务 + if "isPassed" in _card and _card["isPassed"] == True: + continue + # 不属于任务点的任务 + if "job" not in _card or _card["job"] == False: + continue + # 视频任务 + if _card["type"] == "video": + _job = {} + _job["type"] = "video" + _job["jobid"] = _card["jobid"] + _job["name"] = _card["property"]["name"] + _job["otherinfo"] = _card["otherInfo"] + _job["mid"] = _card["mid"] + _job["objectid"] = _card["objectId"] + _job["aid"] = _card["aid"] + _job["doublespeed"] = _card["property"]["doublespeed"] + _job_list.append(_job) + continue + if _card["type"] == "document": + _job = {} + _job["type"] = "document" + _job["jobid"] = _card["jobid"] + _job["otherinfo"] = _card["otherInfo"] + _job["jtoken"] = _card["jtoken"] + _job["mid"] = _card["mid"] + _job["enc"] = _card["enc"] + _job["aid"] = _card["aid"] + _job["objectid"] = _card["property"]["objectid"] + _job_list.append(_job) + continue + if _card["type"] == "workid": + continue + return _job_list, _job_info diff --git a/api/exceptions.py b/api/exceptions.py new file mode 100644 index 00000000..5ee4788c --- /dev/null +++ b/api/exceptions.py @@ -0,0 +1,12 @@ +from loguru import logger + +class BaseException(Exception): + def __init__(self, _msg: str = None): + if _msg: + logger.error(_msg) + +class LoginError(BaseException): + pass + +class FormatError(Exception): + pass \ No newline at end of file diff --git a/api/logger.py b/api/logger.py new file mode 100644 index 00000000..0d27d394 --- /dev/null +++ b/api/logger.py @@ -0,0 +1,2 @@ +from loguru import logger +logger.add("chaoxing.log", rotation="10 MB", level="TRACE") \ No newline at end of file diff --git a/api/process.py b/api/process.py new file mode 100644 index 00000000..ed81f876 --- /dev/null +++ b/api/process.py @@ -0,0 +1,28 @@ +import time +from api.config import GlobalConst as gc + +def sec2time(sec): + ret = "" + if sec // 3600 > 0: + ret += f"{sec // 3600}h " + sec = sec - sec // 3600 * 3600 + if sec // 60 > 0: + ret += f"{sec // 60}min " + sec = sec - sec // 60 * 60 + if sec: + ret += f"{sec}s" + if not ret: + ret = "0s" + return ret + + +def show_progress(name, start: int, span: int, total: int, _speed): + start_time = time.time() + while int(time.time() - start_time) < int(span // _speed): + current = start + int(time.time() - start_time) + percent = int(current / total * 100) + length = int(percent * 40 // 100) + progress = ("#" * length).ljust(40, " ") + # remain = (total - current) + print("\r" + f"当前任务: {name} |{progress}| {percent}% {sec2time(current)}/{sec2time(total)} ", end="", flush=True) + time.sleep(gc.THRESHOLD) \ No newline at end of file diff --git a/config_template.ini b/config_template.ini new file mode 100644 index 00000000..4389d35a --- /dev/null +++ b/config_template.ini @@ -0,0 +1,12 @@ +[common] +; 手机号账号(必填) +username = xxx + +; 登录密码(必填) +password = xxx + +; 要学习的课程ID列表, 逗号隔开(选填) +course_list = [xxx,xxx,xxx,xxx,xxx] + +; 视频播放倍速(默认1,最大2) +speed = 1 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 0df0a47f..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: "3" -services: - app: - build: - context: . - dockerfile: Dockerfile - volumes: - - ./logs:/app/logs - - ./saves:/app/saves \ No newline at end of file diff --git a/icon.ico b/icon.ico deleted file mode 100644 index 95673894..00000000 Binary files a/icon.ico and /dev/null differ diff --git a/main.py b/main.py index c29e228c..3eb373b7 100644 --- a/main.py +++ b/main.py @@ -1,202 +1,83 @@ -import random -import time +# -*- coding: utf-8 -*- import argparse +import configparser -import utils.functions as ft -from api.chaoxing import Chaoxing +from api.logger import logger +from api.base import Chaoxing, Account +from api.exceptions import LoginError, FormatError -def do_work(chaoxingAPI): - re_login_try = 0 - # done = list(ft.load_finished(chaoxingAPI.usernm)) - logger.info("已选课程:"+str(chaoxingAPI.selected_course['content']['course']['data'][0]['name'])) - logger.info("开始获取所有章节") - chaoxingAPI.get_selected_course_data() # 读取所有章节 - mission_num = len(chaoxingAPI.missions) - mission_index = 0 - while mission_index < mission_num: - mission = chaoxingAPI.missions[mission_index] - mission_index += 1 - logger.debug("开始读取章节信息") - knowledge_raw = chaoxingAPI.get_mission(mission['id'], chaoxingAPI.selected_course['key']) # 读取章节信息 - if "data" not in knowledge_raw and "error" in knowledge_raw: - logger.debug("---knowledge_raw info begin---") - logger.debug(knowledge_raw) - logger.debug("---knowledge_raw info end---") - if re_login_try < 2: - logger.warn("章节数据错误,可能是课程存在验证码,正在尝试重新登录") - chaoxingAPI.re_init_login() - mission_index -= 1 - re_login_try += 1 - continue - else: - logger.error("章节数据错误,可能是课程存在验证码,重新登录尝试无效") - input("请截图并携带日志提交Issue反馈") - re_login_try = 0 - tabs = len(knowledge_raw['data'][0]['card']['data']) - for tab_index in range(tabs): - print("开始读取标签信息") - knowledge_card_text = chaoxingAPI.get_knowledge( - chaoxingAPI.selected_course['key'], - chaoxingAPI.selected_course['content']['course']['data'][0]['id'], - mission["id"], - tab_index - ) - attachments: dict = chaoxingAPI.get_attachments(knowledge_card_text) - if not attachments: - continue - if not attachments.get('attachments'): - continue - print(f'\n当前章节:{mission["label"]}:{mission["name"]}') - for attachment in attachments['attachments']: - if attachment.get('type') != 'video': # 非视频任务跳过 - print("跳过非视频任务") - continue - if attachment.get('property', False): - name = attachment['property']['name'] - else: - name = attachment['objectId'] - print(f"\n当前视频:{name}") - if attachment.get('isPassed'): - print("当前视频任务已完成") - ft.show_progress(name, 1, 1) - time.sleep(1) - continue - video_info = chaoxingAPI.get_d_token( - attachment['objectId'], - attachments['defaults']['fid'] - ) - if not video_info: - continue - jobid = None - if "jobid" in attachments: - jobid = attachments["jobid"] - else: - if "jobid" in attachment: - jobid = attachment["jobid"] - elif attachment.get('property', False): - if "jobid" in attachment['property']: - jobid = attachment['property']['jobid'] - else: - if "'_jobid'" in attachment['property']: - jobid = attachment['property']['_jobid'] - if not jobid: - print("未找到jobid,已跳过当前任务点") - continue - # if adopt: - # logger.debug("已启用自适应速率") - # if "doublespeed" in attachment['property']: - # if attachment['property']['doublespeed']: - # print("当前视频支持倍速播放,已切换速率") - # chaoxing.speed = 2 - # else: - # print("当前视频不支持倍速播放,跳过") - # chaoxing.speed = set_speed - dtype = 'Video' - if 'audio' in attachment['property']['module']: - dtype = 'Audio' - - chaoxingAPI.pass_video( - video_info['duration'], - attachments['defaults']['cpi'], - video_info['dtoken'], - attachment['otherInfo'], - chaoxingAPI.selected_course['key'], - attachment['jobid'], - video_info['objectid'], - chaoxingAPI.uid, - attachment['property']['name'], - chaoxingAPI.speed, - dtype, - chaoxingAPI.get_current_ms - ) - ft.pause(10, 13) - # chaoxing.speed = set_speed # 预防ERR +def init_config(): + parser = argparse.ArgumentParser(description='Samueli924/chaoxing') # 命令行传参 + parser.add_argument("-c", "--config", type=str, default=None, help="使用配置文件运行程序") + parser.add_argument("-u", "--username", type=str, default=None, help="手机号账号") + parser.add_argument("-p", "--password", type=str, default=None, help="登录密码") + parser.add_argument("-l", "--list", type=str, default=None, help="要学习的课程ID列表") + parser.add_argument("-s", "--speed", type=int, default=1, help="视频播放倍速(默认1,最大2)") + args = parser.parse_args() + if args.config: + config = configparser.ConfigParser() + config.read(args.config, encoding="utf8") + return (config.get("common", "username"), + config.get("common", "password"), + config.get("common", "course_list"), + int(config.get("common", "speed"))) + else: + return (args.username, args.password, args.list.split(","), int(args.speed)) if __name__ == '__main__': - parser = argparse.ArgumentParser(description='chaoxing-xuexitong') # 命令行传参 - parser.add_argument('-debug','--debug', action='store_true', help='Enable debug output in console') -# parser.add_argument('-default','--default-speed', action='store_true', help='Choose Default Speed,Enable adaptive speed(Disable by --no-adopt)') -# parser.add_argument('--use-adopt', action='store_true', help='Use adaptive speed') -# parser.add_argument('--no-adopt', action='store_true', help='Disable adaptive speed') - parser.add_argument('--no-log', action='store_false', help='Disable Console log') - parser.add_argument('--no-logo', action='store_false', help='Disable Boot logo') - parser.add_argument('--no-sec', action='store_false', help='Disable all security feature') - - args = parser.parse_args() # 定义专用参数变量 - debug = args.debug # debug输出 Default:False -# disable_adopt = args.no_adopt # 禁用自适应速率 Default:False - show = args.no_log # 显示控制台log Default:True - logo = args.no_logo # 展示启动LOGO Default:True - hideinfo = args.no_sec # 启用隐私保护 Default:True -# use_adopt = args.use_adopt # 使用自适应速率 Default:False -# use_default = args.default_speed # 启用默认速度 Default:False - - try: - ft.init_all_path(["saves", "logs"]) # 检查文件夹 - logger = ft.Logger("main",debug,show) # 初始化日志类 - if debug: - logger.debug("已启用debug输出") - if not show: - logger.debug("已关闭控制台日志") - ft.title_show(logo) # 显示头 - if not logo: - logger.debug("已关闭启动LOGO") -# if use_default: -# logger.debug("已选择默认速率") -# if disable_adopt: -# logger.debug("已关闭自适应速率") -# else: -# if use_adopt: -# logger.debug("已使用自适应速率") - logger.info("正在读取本地用户数据...") - usernm, secname, passwd = ft.load_users(hideinfo) # 获取账号密码 - chaoxing = Chaoxing(usernm, passwd, debug, show) # 实例化超星API - chaoxing.init_explorer() # 实例化浏览Explorer - logger.info("登陆中") - if chaoxing.login(): # 登录 - logger.info("已登录账户:" +secname) - logger.info("正在读取所有课程") - if chaoxing.get_all_courses(): # 读取所有的课程 - logger.info("进行选课") - if chaoxing.select_course(): # 选择要学习的课程 -# if not use_default: -# set_speed = input("默认倍速: 1 倍速 \n在不紧急的情况下建议使用 1 倍速,因使用不合理的多倍速造成的一切风险与作者无关\n请输入您想要的学习倍速(倍数需为整数,0或直接回车将使用默认1倍速):") -# else: -# set_speed = 0 -# if not set_speed or set_speed == 0: -# chaoxing.speed = 1 -# set_speed = 1 -# logger.info("已使用默认速率") -# else: -# chaoxing.speed = int(set_speed) -# set_speed = int(set_speed) - speed_input = input("默认倍速: 1 倍速 \n在不紧急的情况下建议使用 1 倍速,因使用不合理的多倍速造成的一切风险与作者无关\n请输入您想要的学习倍速(倍数需为整数,0或直接回车将使用默认1倍速):") - if speed_input: - chaoxing.speed = int(speed_input) - else: - chaoxing.speed = 1 - logger.debug("当前设置速率:"+str(chaoxing.speed)+"倍速") -# if not disable_adopt and set_speed == 1: # Only God and I knew how it worked. -# if not use_default and not use_adopt: -# set_adopt = input("是否启用自适应速率(当播放速率为1且视频支持倍速播放时,自动切换为两倍速)\n!注意 该功能可能存在风险!输入(Y/y/Yes/yes)启用").lower() -# if use_default or use_adopt or set_adopt.startswith('y'): -# adopt = True -# else: -# adopt = False -# else: -# adopt = False -# if adopt: -# logger.info("已启用自适应速率") -# else: -# logger.info("已禁用自适应速率") - logger.info("开始学习") - do_work(chaoxing) # 开始学习 - input("任务已结束,请点击回车键退出程序") - except Exception as e: - print(f"出现报错{e.__class__}") - print(f"错误文件名:{e.__traceback__.tb_frame.f_globals['__file__']}") - print(f"错误行数:{e.__traceback__.tb_lineno}") - print(f"错误原因:{e}") - input("请截图提交至Github或Telegram供作者修改代码\n点击回车键退出程序") + # 初始化登录信息 + username, password, course_list, speed = init_config() + # 强行限制倍速最大为2倍速 + speed = 2 if speed > 2 else speed + if username and password: + account = Account(username, password) + # 实例化超星API + chaoxing = Chaoxing(account=account) + # 检查当前登录状态,并检查账号密码 + _login_state = chaoxing.login() + if not _login_state["status"]: + raise LoginError(_login_state["msg"]) + # 获取所有的课程列表 + all_course = chaoxing.get_course_list() + course_task = [] + # 手动输入要学习的课程ID列表 + if not course_list: + print("*"*10 + "课程列表" + "*"*10) + for course in all_course: + print(f"ID: {course['courseId']} 课程名: {course['title']}") + print("*" * 28) + try: + course_list = str(input("请输入想要学习的课程列表,以逗号分隔,例: 2151141,189191,198198\n")).split(",") + except: + raise FormatError("输入格式错误") + # 筛选需要学习的课程 + for course in all_course: + if course["courseId"] in course_list: + course_task.append(course) + # 开始遍历要学习的课程列表 + logger.info(f"课程列表过滤完毕,当前课程任务数量: {len(course_task)}") + for course in course_task: + # 获取当前课程的所有章节 + point_list = chaoxing.get_course_point(course["courseId"], course["clazzId"], course["cpi"]) + for point in point_list["points"]: + # 获取当前章节的所有任务点 + jobs, job_info = chaoxing.get_job_list(course["clazzId"], course["courseId"], course["cpi"], point["id"]) + # 可能存在章节无任何内容的情况 + if not jobs: + continue + # 遍历所有任务点 + for job in jobs: + # 视频任务 + if job["type"] == "video": + logger.trace(f"识别到视频任务, 任务章节: {course['title']} 任务ID: {job['jobid']}") + chaoxing.study_video(course, job, job_info, _speed=speed) + # 文档任务 + elif job["type"] == "document": + logger.trace(f"识别到文档任务, 任务章节: {course['title']} 任务ID: {job['jobid']}") + chaoxing.study_document(course, job) + # 测验任务 + elif job["type"] == "workid": + logger.trace(f"识别到测验任务, 任务章节: {course['title']}") + pass diff --git a/requirements.txt b/requirements.txt index 7e1d2a95..0a89ed71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -maskpass -natsort requests -urllib3==1.25.11 -pyDes -pycryptodome \ No newline at end of file +pyaes +beautifulsoup4 +lxml +argparse +loguru \ No newline at end of file diff --git a/utils/.gitignore b/utils/.gitignore deleted file mode 100644 index 0e4f7f49..00000000 --- a/utils/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -__pycache__/ -*.py[cod] -*$py.class \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/utils/functions.py b/utils/functions.py deleted file mode 100644 index 5535136b..00000000 --- a/utils/functions.py +++ /dev/null @@ -1,232 +0,0 @@ -import json -import logging -import os -import time -import maskpass -from hashlib import md5 -from os import mkdir -from os.path import exists -import random -from natsort import natsorted - - -def title_show(logo): - if logo: - print("-"*120 + "\n") - print(""" ,---.-, ,--, - .--.--. ____ ,--, ' ,' '. ,----, ,--.'| - / / '. ,' , `. ,--.'| ,--, / / \ .' .' \ ,--, | : -| : /`. / ,-+-,.' _ | ,--, | | : ,--.'|. ; ,/. : ,----,' |,---.'| : ' -; | |--` ,-+-. ; , || ,'_ /| : : ' | |, ' | | : ; | : . ;; : | | ; -| : ;_ ,--.--. ,--.'|' | || .--. | | : ,---. | ' | `--'_ ' | ./ : ; |.' / | | : _' | - \ \ `. / \ | | ,', | |,'_ /| : . | / \ ' | | ,' ,'|| : , `----'/ ; : : |.' | - `----. \.--. .-. | | | / | |--'| ' | | . . / / || | : ' | | \ \ | / ; / | ' ' ; : - __ \ \ | \__\/: . . | : | | , | | ' | | | . ' / |' : |__ | | : `---`--- ; ; / /-, \ \ .'. | - / /`--' / ," .--.; | | : | |/ : | : ; ; | ' ; /|| | '.'|' : |__ | | | / / /.`| `---`: | ' -'--'. / / / ,. | | | |`-' ' : `--' \' | / |; : ;| | '.'| ' : ;./__; : ' ; | - `--'---' ; : .' \| ;/ : , .-./| : || , / ; : ; | | '| : .' | : ; - | , .-./'---' `--`----' \ \ / ---`-' | , / ; |.' ; | .' ' ,/ - `--`---' `----' ---`-' '---' `---' '--' """) - print("\n" + "-"*120) - else: - print("\n") - print("欢迎使用Samueli924/chaoxing\n对代码有任何疑问或建议,请前往https://github.com/Samueli924/chaoxing进行反馈") - print("如果喜欢这个项目,请给我的repo一个小小的Star,谢谢\n") - - -def check_path(path: str, file: bool = True): - """ - 检查路径是否存在 - :param path: 路径(str) - :param file: 是否为文件(bool) - :return: True - """ - path_list = path.split("/") - for i in range(len(path_list) - 1): # 循环判断路径是否存在 - __temp = "/".join(path_list[:i + 1]) - if not exists(__temp): # 不存在即新建路径 - mkdir(__temp) - if file: - with open(path, "w") as f: # 新建文件 - f.write("") - else: - if not exists(path): - mkdir(path) - return True - - -def init_all_path(init_path): - for path in init_path: - check_path(path, file=False) - return True - - -class Logger: - def __init__(self, name, debug, show, save=True ): - """ - 日志记录系统 - :param name: 日志保存时使用的Name - :param debug: 控制台输出等级传参 #有人懒得在外面传loghandler - :param show: 是否在控制台显示日志 - :param save: 是否将日志保存至本地 - """ - log_path = f"logs/{name}.log" - self.logger = logging.getLogger(name) - # self.logger.handlers.clear() - self.logger.setLevel(logging.DEBUG) - if not self.logger.handlers: - if show: - sh = logging.StreamHandler() - if debug: - sh.setLevel(logging.DEBUG) - else: - sh.setLevel(logging.INFO) - sh.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) - self.logger.addHandler(sh) - if save: - fh = logging.FileHandler(log_path) - fh.setLevel(logging.DEBUG) - fh.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) - self.logger.addHandler(fh) - - def debug(self, message): - self.logger.debug(message) - - def info(self, message): - self.logger.info(message) - - def warn(self, message): - self.logger.warning(message) - - def error(self, message): - self.logger.error(message) - - def critical(self, message): - self.logger.critical(message) - - -def save_users(usernm, passwd): - user_folder = f"saves/{usernm}" - check_path(user_folder, file=False) - with open(f"{user_folder}/user.json", "w") as f: - json.dump({"usernm": usernm, "passwd": passwd}, f) - return True - - -def load_users(hide): - if os.listdir("saves"): - users = os.listdir("saves") - print("-" * 40) - for index, user in enumerate(users): - if hide: - sec_user = "%s****%s"%(user[:3],user[7:]) - else: - sec_user = user - print(f"{index + 1}. {sec_user}") - print("-" * 40) - num = input("请输入要登录的用户序号,新建用户请直接点击回车键") - if not num: - usernm = input("请输入手机号") - if hide: - sec_user = "%s****%s"%(usernm[:3],usernm[7:]) - passwd = maskpass.askpass(prompt="请输入密码(已自动隐藏)", mask="#") - else: - sec_user = usernm - passwd = input("请输入密码") - else: - with open(f"saves/{users[int(num) - 1]}/user.json", "r") as f: - __temp = json.loads(f.read()) - usernm = __temp["usernm"] - sec_user = "%s****%s" % (usernm[:3], usernm[7:]) - passwd = __temp["passwd"] - else: - usernm = input("请输入手机号") - if hide: - sec_user = "%s****%s"%(usernm[:3],usernm[7:]) - passwd = maskpass.askpass(prompt="请输入密码(已自动隐藏)", mask="#") - else: - sec_user = usernm - passwd = input("请输入密码") - return usernm, sec_user, passwd - - -def load_finished(usernm): - path = f"saves/{usernm}/done.json" - if exists(path): - with open(path, "r") as f: - done = json.loads(f.read()) - else: - with open(path, "w") as f: - json.dump([], f) - done = list() - return done - - -def save_finished(usernm, done): - path = f"saves/{usernm}/done.json" - with open(path, "w") as f: - json.dump(done, f) - - -def pretty_print(courses_raw: list): - titles = ["序号", "课程ID", "课程名称"] - data = list() - for course_index, course in enumerate(courses_raw): - if "course" in course["content"]: - data.append([str(course_index + 1), str(course["key"]), str(course['content']['course']['data'][0]['name'])]) - format_row = "{:>16}" * (len(titles) + 1) - print("-"*100) - print(format_row.format("", *titles)) - for row in data: - print(format_row.format("", *row)) - print("-" * 100) - - -def sort_missions(missions): - data = {} - for mission in missions: - data[mission['label']] = mission - keys_sorted = natsorted(data.keys()) - return [data[k] for k in keys_sorted] - - -def get_enc_time(): - m_time = str(int(time.time() * 1000)) - m_token = '4faa8662c59590c6f43ae9fe5b002b42' - m_encrypt_str = 'token=' + m_token + '&_time=' + m_time + '&DESKey=Z(AfY@XS' - m_inf_enc = md5(m_encrypt_str.encode('utf-8')).hexdigest() - return m_time, m_inf_enc - - -def sec2time(sec): - ret = "" - if sec // 3600 > 0: - ret += f"{sec // 3600}h " - sec = sec - sec // 3600 * 3600 - if sec // 60 > 0: - ret += f"{sec // 60}min " - sec = sec - sec // 60 * 60 - if sec: - ret += f"{sec}s" - if not ret: - ret = "0s" - return ret - - -def show_progress(name, current, total): - percent = int(current / total * 100) - length = int(percent * 40 // 100) - progress = ("#" * length).ljust(40, " ") - remain = (total - current) - if current >= total and remain < 1: - print("\r" + f"当前任务: {name} 已完成".ljust(100, " ")) - else: - # print("\r" + f"当前任务: {name} 剩余时间:{sec2time(remain / speed)} |{progress}| {percent}% {sec2time(current)}/{sec2time(total)}", end="", flush=True) - print("\r" + f"当前任务: {name} |{progress}| {percent}% {sec2time(current)}/{sec2time(total)} ", end="", flush=True) - - -def pause(start: int, end: int): - __temp = random.randint(start, end) - print(f"等待{__temp}秒") - time.sleep(__temp) - return True