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