Initial commit
This commit is contained in:
commit
4319c8b6d7
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@ -0,0 +1,11 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[*.py]
|
||||
indent_style = space
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.venv/
|
||||
dist/
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
28
pyproject.toml
Normal file
28
pyproject.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[project]
|
||||
name = "ncmlyrics"
|
||||
authors = [{ name = "Puqns67", email = "me@puqns67.icu" }]
|
||||
dependencies = [
|
||||
"httpx>=0.27.2",
|
||||
"h2>=4.1.0",
|
||||
"rich>=13.8.1",
|
||||
"brotli>=1.1.0",
|
||||
"zstandard>=0.23.0",
|
||||
"click>=8.1.7",
|
||||
"platformdirs>=4.3.6",
|
||||
]
|
||||
requires-python = ">=3.12"
|
||||
readme = "README.md"
|
||||
license = { text = "GPL-3.0-or-later" }
|
||||
dynamic = ["version"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "src/ncmlyrics/constant.py"
|
||||
pattern = "^APP_VERSION = \"(?P<version>[^\"]+)\"$"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
line-length = 120
|
3
src/ncmlyrics/__init__.py
Normal file
3
src/ncmlyrics/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .constant import APP_VERSION as __version__
|
||||
|
||||
__all__ = ["__version__"]
|
137
src/ncmlyrics/__main__.py
Normal file
137
src/ncmlyrics/__main__.py
Normal file
@ -0,0 +1,137 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from click import argument, command, confirm, option, Path as clickPath
|
||||
from rich.console import Console
|
||||
from rich.theme import Theme
|
||||
|
||||
from ncmlyrics.error import UnsupportLinkError
|
||||
|
||||
from .api import NCMApi, NCMTrack
|
||||
from .util import Link, LinkType, parseLink, pickOutput
|
||||
|
||||
NCMLyricsAppTheme = Theme(
|
||||
{
|
||||
"songTitle": "bold chartreuse1",
|
||||
"songArrow": "chartreuse3",
|
||||
"albumTitle": "bold orchid1",
|
||||
"albumArrow": "orchid2",
|
||||
"playListTitle": "bold aquamarine1",
|
||||
"playListArrow": "aquamarine3",
|
||||
"warning": "bold red1",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NCMLyricsApp:
|
||||
console: Console
|
||||
|
||||
outputs: list[Path]
|
||||
exist: bool
|
||||
overwrite: bool
|
||||
quiet: bool
|
||||
|
||||
tracks: list[tuple[NCMTrack, Path]]
|
||||
|
||||
def addWithPath(self, track: NCMTrack, savePath: Path | None, arrowStyle: str) -> None:
|
||||
if savePath is None:
|
||||
if not self.quiet:
|
||||
self.console.print("--->", style=arrowStyle, end=" ")
|
||||
self.console.print("未能找到源文件,将跳过此曲目。", style="warning")
|
||||
elif not self.overwrite and savePath.exists():
|
||||
if not self.quiet:
|
||||
self.console.print("--->", style=arrowStyle, end=" ")
|
||||
self.console.print("歌词文件已存在,将跳过此曲目。", style="warning")
|
||||
else:
|
||||
self.tracks.append((track, savePath))
|
||||
|
||||
def add(self, track: NCMTrack, arrowStyle: str) -> None:
|
||||
savePath = pickOutput(track, self.outputs, self.exist)
|
||||
|
||||
if not self.quiet:
|
||||
self.console.print("-->", style=arrowStyle, end=" ")
|
||||
self.console.print(f"{"/".join(track.artists)} - {track.name}", style=f"link {track.link()}")
|
||||
|
||||
self.addWithPath(track, savePath, arrowStyle)
|
||||
|
||||
|
||||
@command
|
||||
@option(
|
||||
"-o",
|
||||
"--outputs",
|
||||
type=clickPath(exists=True, file_okay=False, dir_okay=True, writable=True, path_type=Path),
|
||||
multiple=True,
|
||||
help="Output paths, repeating multiple times will automatically match existing audio files in input order and use the last input as a fallback output.",
|
||||
)
|
||||
@option("-e", "--exist", is_flag=True, help="仅在源文件存在时保存歌词文件。")
|
||||
@option("-O", "--overwrite", is_flag=True, help="在歌词文件已存在时重新获取歌词并覆盖写入。")
|
||||
@option("-q", "--quiet", is_flag=True, help="不进行任何提示并跳过所有确认。")
|
||||
@argument(
|
||||
"links",
|
||||
nargs=-1,
|
||||
)
|
||||
def main(outputs: list[Path], exist: bool, overwrite: bool, quiet: bool, links: list[str]) -> int:
|
||||
console = Console(theme=NCMLyricsAppTheme)
|
||||
|
||||
if len(links) == 0:
|
||||
console.print(
|
||||
"请输入至少一个链接以解析曲目以获取其歌词!支持输入单曲,专辑与歌单的分享或网页链接。", style="warning"
|
||||
)
|
||||
return 1
|
||||
|
||||
api = NCMApi()
|
||||
app = NCMLyricsApp(console=console, outputs=outputs, exist=exist, overwrite=overwrite, quiet=quiet, tracks=[])
|
||||
|
||||
for link in links:
|
||||
parsed: Link | None = None
|
||||
|
||||
try:
|
||||
parsed = parseLink(link)
|
||||
except UnsupportLinkError:
|
||||
continue
|
||||
|
||||
match parsed.type:
|
||||
case LinkType.Song:
|
||||
newTrack = api.getDetailsForTrack(parsed.id)
|
||||
savePath = pickOutput(newTrack, outputs, exist)
|
||||
|
||||
if not quiet:
|
||||
console.print("-- 单曲 -->", style="songTitle", end=" ")
|
||||
console.print(f"{"/".join(newTrack.artists)} - {newTrack.name}", style=f"link {newTrack.link()}")
|
||||
|
||||
app.addWithPath(newTrack, savePath, "songArrow")
|
||||
|
||||
case LinkType.Album:
|
||||
newAlbum = api.getDetailsForAlbum(parsed.id)
|
||||
|
||||
if not quiet:
|
||||
console.print("== 专辑 ==>", style="albumTitle", end=" ")
|
||||
console.print(newAlbum.name, style=f"link {newAlbum.link()}")
|
||||
|
||||
for newTrack in newAlbum.tracks:
|
||||
app.add(newTrack, "albumArrow")
|
||||
|
||||
case LinkType.Playlist:
|
||||
newPlaylist = api.getDetailsForPlaylist(parsed.id)
|
||||
newPlaylist.fillDetailsOfTracks(api)
|
||||
|
||||
if not quiet:
|
||||
console.print("== 歌单 ==>", style="playListTitle", end=" ")
|
||||
console.print(newPlaylist.name, style=f"link {newPlaylist.link()}")
|
||||
|
||||
for newTrack in newPlaylist.tracks:
|
||||
app.add(newTrack, "playListArrow")
|
||||
|
||||
if len(app.tracks) == 0:
|
||||
console.print("无曲目的歌词可被获取,请检查上方的输出!", style="warning")
|
||||
return 1
|
||||
|
||||
if not quiet:
|
||||
confirm("继续操作?", default=True, abort=True)
|
||||
|
||||
for track, path in app.tracks:
|
||||
api.getLyricsByTrack(track.id).lrc().saveAs(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
249
src/ncmlyrics/api.py
Normal file
249
src/ncmlyrics/api.py
Normal file
@ -0,0 +1,249 @@
|
||||
from dataclasses import dataclass
|
||||
from http.cookiejar import MozillaCookieJar
|
||||
from json import dumps as dumpJson
|
||||
from typing import Any, Self
|
||||
|
||||
from httpx import Client as HttpClient
|
||||
|
||||
from .constant import CONFIG_API_DETAIL_TRACK_PER_REQUEST, NCM_API_BASE_URL, PLATFORM
|
||||
from .error import NCMApiParseError, NCMApiRequestError
|
||||
from .lrc import Lrc, LrcType
|
||||
|
||||
REQUEST_HEADERS = {
|
||||
"Accept": "application/json",
|
||||
"Accept-Encoding": "zstd, br, gzip, deflate",
|
||||
"Connection": "keep-alive",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class NCMTrack:
|
||||
id: int
|
||||
name: str
|
||||
artists: list[str]
|
||||
|
||||
def fromApi(data: dict) -> list[Self]:
|
||||
if data.get("code") != 200:
|
||||
raise NCMApiParseError(f"响应码不为 200: {data["code"]}")
|
||||
|
||||
data = data.get("songs")
|
||||
if data is None:
|
||||
raise NCMApiParseError("不存在 Track 对应的结构")
|
||||
|
||||
result: list[NCMTrack] = []
|
||||
|
||||
for track in data:
|
||||
result.append(NCMTrack.fromData(track))
|
||||
|
||||
return result
|
||||
|
||||
def fromData(data: dict) -> Self:
|
||||
try:
|
||||
return NCMTrack(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
artists=[artist["name"] for artist in data["ar"]],
|
||||
)
|
||||
except KeyError as e:
|
||||
raise NCMApiParseError(f"需要的键不存在: {e}")
|
||||
|
||||
def link(self) -> str:
|
||||
return f"https://music.163.com/song?id={self.id}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class NCMAlbum:
|
||||
id: int
|
||||
name: str
|
||||
tracks: list[NCMTrack]
|
||||
|
||||
def fromApi(data: dict) -> Self:
|
||||
if data.get("code") != 200:
|
||||
raise NCMApiParseError(f"响应码不为 200: {data["code"]}")
|
||||
|
||||
album = data.get("album")
|
||||
if album is None:
|
||||
raise NCMApiParseError("不存在 Album 对应的结构")
|
||||
|
||||
try:
|
||||
return NCMAlbum(
|
||||
id=album["id"],
|
||||
name=album["name"],
|
||||
tracks=[NCMTrack.fromData(track) for track in data["songs"]],
|
||||
)
|
||||
except KeyError as e:
|
||||
raise NCMApiParseError(f"需要的键不存在: {e}")
|
||||
|
||||
def link(self) -> str:
|
||||
return f"https://music.163.com/album?id={self.id}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class NCMPlaylist:
|
||||
id: int
|
||||
name: str
|
||||
tracks: list[NCMTrack]
|
||||
trackIds: list[int]
|
||||
|
||||
def fromApi(data: dict) -> Self:
|
||||
if data.get("code") != 200:
|
||||
raise NCMApiParseError(f"响应码不为 200: {data["code"]}")
|
||||
|
||||
playlist = data.get("playlist")
|
||||
if playlist is None:
|
||||
raise NCMApiParseError("不存在 Playlist 对应的结构")
|
||||
|
||||
try:
|
||||
tracks: list[NCMTrack] = []
|
||||
trackIds: list[int] = [track["id"] for track in playlist["trackIds"]]
|
||||
|
||||
for track in playlist["tracks"]:
|
||||
parsedTrack: NCMTrack = NCMTrack.fromData(track)
|
||||
trackIds.remove(parsedTrack.id)
|
||||
tracks.append(parsedTrack)
|
||||
|
||||
return NCMPlaylist(
|
||||
id=playlist["id"],
|
||||
name=playlist["name"],
|
||||
tracks=tracks,
|
||||
trackIds=trackIds,
|
||||
)
|
||||
except KeyError as e:
|
||||
raise NCMApiParseError(f"需要的键不存在: {e}")
|
||||
|
||||
def link(self) -> str:
|
||||
return f"https://music.163.com/playlist?id={self.id}"
|
||||
|
||||
def fillDetailsOfTracks(self, api) -> None:
|
||||
self.tracks.extend(api.getDetailsForTracks(self.trackIds))
|
||||
self.trackIds.clear()
|
||||
|
||||
|
||||
@dataclass
|
||||
class NCMLyrics:
|
||||
isPureMusic: bool
|
||||
data: Any | None
|
||||
|
||||
def fromApi(data: dict) -> Self:
|
||||
if data.get("code") != 200:
|
||||
raise NCMApiParseError(f"响应码不为 200: {data["code"]}")
|
||||
|
||||
if data.get("pureMusic") is True:
|
||||
return NCMLyrics(isPureMusic=True, data=None)
|
||||
|
||||
return NCMLyrics(isPureMusic=False, data=data)
|
||||
|
||||
def lrc(self) -> Lrc:
|
||||
result = Lrc()
|
||||
|
||||
for lrcType in LrcType:
|
||||
try:
|
||||
lrcStr = self.data[lrcType.value]["lyric"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if lrcStr != "":
|
||||
result.serializeLyricString(lrcType, lrcStr)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class NCMApi:
|
||||
_cookieJar: MozillaCookieJar
|
||||
_httpClient: HttpClient
|
||||
|
||||
def __init__(self, http2: bool = True) -> None:
|
||||
self._cookieJar = MozillaCookieJar()
|
||||
|
||||
try:
|
||||
self._cookieJar.load(str(PLATFORM.user_config_path / "cookies.txt"))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
self._httpClient = HttpClient(
|
||||
base_url=NCM_API_BASE_URL,
|
||||
cookies=self._cookieJar,
|
||||
headers=REQUEST_HEADERS,
|
||||
http2=http2,
|
||||
)
|
||||
|
||||
def __del__(self) -> None:
|
||||
self._cookieJar.save(str(PLATFORM.user_config_path / "cookies.txt"))
|
||||
self._httpClient.close()
|
||||
|
||||
def getDetailsForTrack(self, trackId: int) -> NCMTrack:
|
||||
params = {"c": f"[{{'id':{trackId}}}]"}
|
||||
|
||||
try:
|
||||
response = self._httpClient.request("GET", "/v3/song/detail", params=params)
|
||||
except BaseException:
|
||||
raise NCMApiRequestError
|
||||
|
||||
return NCMTrack.fromApi(response.json()).pop()
|
||||
|
||||
def getDetailsForTracks(self, trackIds: list[int]) -> list[NCMTrack]:
|
||||
result: list[NCMTrack] = []
|
||||
seek = 0
|
||||
|
||||
while True:
|
||||
seekedTrackIds = trackIds[seek : seek + CONFIG_API_DETAIL_TRACK_PER_REQUEST]
|
||||
|
||||
if len(seekedTrackIds) == 0:
|
||||
break
|
||||
|
||||
params = {
|
||||
"c": dumpJson(
|
||||
[{"id": trackId} for trackId in seekedTrackIds],
|
||||
separators=(",", ":"),
|
||||
)
|
||||
}
|
||||
|
||||
try:
|
||||
response = self._httpClient.request("GET", "/v3/song/detail", params=params)
|
||||
except BaseException:
|
||||
raise NCMApiRequestError
|
||||
|
||||
result.extend(NCMTrack.fromApi(response.json()))
|
||||
|
||||
seek += CONFIG_API_DETAIL_TRACK_PER_REQUEST
|
||||
|
||||
return result
|
||||
|
||||
def getDetailsForAlbum(self, albumId: int) -> NCMAlbum:
|
||||
try:
|
||||
response = self._httpClient.request("GET", f"/v1/album/{albumId}")
|
||||
except BaseException:
|
||||
raise NCMApiRequestError
|
||||
|
||||
return NCMAlbum.fromApi(response.json())
|
||||
|
||||
def getDetailsForPlaylist(self, playlistId: int) -> NCMPlaylist:
|
||||
params = {"id": playlistId}
|
||||
|
||||
try:
|
||||
response = self._httpClient.request("GET", "/v6/playlist/detail", params=params)
|
||||
except BaseException:
|
||||
raise NCMApiRequestError
|
||||
|
||||
return NCMPlaylist.fromApi(response.json())
|
||||
|
||||
def getLyricsByTrack(self, trackId: int) -> NCMLyrics:
|
||||
params = {
|
||||
"id": trackId,
|
||||
"cp": False,
|
||||
"lv": 0,
|
||||
"tv": 0,
|
||||
"rv": 0,
|
||||
"kv": 0,
|
||||
"yv": 0,
|
||||
"ytv": 0,
|
||||
"yrv": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
response = self._httpClient.request("GET", "/song/lyric/v1", params=params)
|
||||
except BaseException:
|
||||
raise NCMApiRequestError
|
||||
|
||||
return NCMLyrics.fromApi(response.json())
|
14
src/ncmlyrics/constant.py
Normal file
14
src/ncmlyrics/constant.py
Normal file
@ -0,0 +1,14 @@
|
||||
from httpx import URL
|
||||
from platformdirs import PlatformDirs
|
||||
|
||||
APP_NAME = "ncmlyrics"
|
||||
APP_VERSION = "0.1.0"
|
||||
|
||||
NCM_API_BASE_URL = URL("https://interface.music.163.com/api")
|
||||
|
||||
CONFIG_LRC_AUTO_MERGE = True
|
||||
CONFIG_LRC_AUTO_MERGE_OFFSET = 50
|
||||
|
||||
CONFIG_API_DETAIL_TRACK_PER_REQUEST = 150
|
||||
|
||||
PLATFORM = PlatformDirs(appname=APP_NAME, ensure_exists=True)
|
21
src/ncmlyrics/error.py
Normal file
21
src/ncmlyrics/error.py
Normal file
@ -0,0 +1,21 @@
|
||||
from httpx import RequestError
|
||||
|
||||
|
||||
class NCMApiError(Exception):
|
||||
"""使用网易云音乐 API 时出现错误"""
|
||||
|
||||
|
||||
class NCMApiRequestError(NCMApiError, RequestError):
|
||||
"""请求网易云音乐 API 时出现错误"""
|
||||
|
||||
|
||||
class NCMApiParseError(NCMApiError):
|
||||
"""解析网易云音乐 API 返回的数据时出现错误"""
|
||||
|
||||
|
||||
class ParseLinkError(Exception):
|
||||
"""无法解析此分享链接"""
|
||||
|
||||
|
||||
class UnsupportLinkError(Exception):
|
||||
"""不支持的分享链接"""
|
158
src/ncmlyrics/lrc.py
Normal file
158
src/ncmlyrics/lrc.py
Normal file
@ -0,0 +1,158 @@
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from re import Match
|
||||
from re import compile as reCompile
|
||||
|
||||
from .constant import CONFIG_LRC_AUTO_MERGE, CONFIG_LRC_AUTO_MERGE_OFFSET
|
||||
|
||||
LRC_RE_COMMIT = reCompile(r"^\s*#")
|
||||
LRC_RE_META = reCompile(r"^\s*\[(?P<type>ti|ar|al|au|length|by|offset):\s*(?P<content>.+?)\s*\]\s*$")
|
||||
LRC_RE_META_NCM_SPICAL = reCompile(r"^\s*\{.*\}\s*$")
|
||||
LRC_RE_LYRIC = reCompile(r"^\s*(?P<timelabels>(?:\s*\[\d{1,2}:\d{1,2}(?:\.\d{1,3})?\])+)\s*(?P<lyric>.+?)\s*$")
|
||||
LRC_RE_LYRIC_TIMELABEL = reCompile(r"\[(?P<minutes>\d{1,2}):(?P<seconds>\d{1,2}(?:\.\d{1,3})?)\]")
|
||||
|
||||
|
||||
class LrcType(Enum):
|
||||
Origin = "lrc"
|
||||
Translation = "tlyric"
|
||||
Romaji = "romalrc"
|
||||
|
||||
def preety(self) -> str:
|
||||
match self:
|
||||
case LrcType.Origin:
|
||||
return "源"
|
||||
case LrcType.Translation:
|
||||
return "译"
|
||||
case LrcType.Romaji:
|
||||
return "音"
|
||||
|
||||
|
||||
class LrcMetaType(Enum):
|
||||
Title = "ti"
|
||||
Artist = "ar"
|
||||
Album = "al"
|
||||
Author = "au"
|
||||
Length = "length"
|
||||
LrcAuthor = "by"
|
||||
Offset = "offset"
|
||||
|
||||
|
||||
class Lrc:
|
||||
metadata: dict[LrcMetaType, dict[LrcType, str]] = {} # metaType: lrcType: metaContent
|
||||
lyrics: dict[int, dict[LrcType, str]] = {} # timestamp: lrcType: lrcContent
|
||||
|
||||
def serializeLyricString(self, lrcType: LrcType, lrcStr: str) -> None:
|
||||
for line in lrcStr.splitlines():
|
||||
# Skip commit lines
|
||||
if LRC_RE_COMMIT.match(line) is not None:
|
||||
continue
|
||||
|
||||
# Skip NCM spical metadata lines
|
||||
if LRC_RE_META_NCM_SPICAL.match(line) is not None:
|
||||
continue
|
||||
|
||||
matchedMetaDataRow = LRC_RE_META.match(line)
|
||||
if matchedMetaDataRow is not None:
|
||||
self.appendMatchedMetaDataRow(lrcType, matchedMetaDataRow)
|
||||
continue
|
||||
|
||||
matchedLyricRow = LRC_RE_LYRIC.match(line)
|
||||
if matchedLyricRow is not None:
|
||||
self.appendMatchedLyricRow(lrcType, matchedLyricRow)
|
||||
continue
|
||||
|
||||
def appendLyric(self, lrcType: LrcType, timestamps: list[int], lyric: str):
|
||||
for timestamp in timestamps:
|
||||
if timestamp in self.lyrics:
|
||||
self.lyrics[timestamp][lrcType] = lyric
|
||||
else:
|
||||
self.lyrics[timestamp] = {lrcType: lyric}
|
||||
|
||||
def appendMatchedMetaDataRow(self, lrcType: LrcType, matchedLine: Match[str]) -> None:
|
||||
metaType, metaContent = matchedLine.groups()
|
||||
|
||||
try:
|
||||
metaType = LrcMetaType(metaType)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"未知的元数据类型:{e}")
|
||||
|
||||
if metaType in self.metadata:
|
||||
self.metadata[metaType][lrcType] = metaContent
|
||||
else:
|
||||
self.metadata[metaType] = {lrcType: metaContent}
|
||||
|
||||
def appendMatchedLyricRow(self, lrcType: LrcType, matchedLine: Match[str]) -> None:
|
||||
timelabels, lyric = matchedLine.groups()
|
||||
timestamps: list[int] = []
|
||||
|
||||
for timelabel in LRC_RE_LYRIC_TIMELABEL.finditer(timelabels):
|
||||
timestamps.append(self._timelabel2timestamp(timelabel))
|
||||
|
||||
if CONFIG_LRC_AUTO_MERGE:
|
||||
mergedTimestamps: list[int] = []
|
||||
|
||||
for timestamp in timestamps:
|
||||
if timestamp in self.lyrics:
|
||||
mergedTimestamps.append(timestamp)
|
||||
else:
|
||||
mergedTimestamps.append(self._mergeOffset(timestamp))
|
||||
|
||||
timestamps = mergedTimestamps
|
||||
|
||||
self.appendLyric(lrcType, timestamps, lyric)
|
||||
|
||||
def deserializeLyricFile(self) -> str:
|
||||
return "\n".join(self.deserializeLyricRows())
|
||||
|
||||
def deserializeLyricRows(self) -> list[str]:
|
||||
result = []
|
||||
|
||||
result.extend(self.generateLyricMetaDataRows())
|
||||
|
||||
for timestamp in sorted(self.lyrics.keys()):
|
||||
result.extend(self.generateLyricRows(timestamp))
|
||||
|
||||
return result
|
||||
|
||||
def generateLyricMetaDataRows(self) -> list[str]:
|
||||
result = []
|
||||
|
||||
for type in LrcMetaType:
|
||||
if type in self.metadata:
|
||||
for lrcType in self.metadata[type].keys():
|
||||
result.append(f"[{type.value}: {lrcType.preety()}/{self.metadata[type][lrcType]}]")
|
||||
|
||||
return result
|
||||
|
||||
def generateLyricRows(self, timestamp: int) -> list[str]:
|
||||
result = []
|
||||
|
||||
for lrcType in self.lyrics[timestamp].keys():
|
||||
result.append(self._timestamp2timelabel(timestamp) + self.lyrics[timestamp][lrcType])
|
||||
|
||||
return result
|
||||
|
||||
def saveAs(self, path: Path):
|
||||
with path.open("w+") as fs:
|
||||
fs.write(self.deserializeLyricFile())
|
||||
|
||||
def _timelabel2timestamp(self, timelabel: Match[str]) -> int:
|
||||
minutes, seconds = timelabel.groups()
|
||||
return round((int(minutes) * 60 + float(seconds)) * 1000)
|
||||
|
||||
def _timestamp2timelabel(self, timestamp: int) -> str:
|
||||
seconds = timestamp / 1000
|
||||
return f"[{seconds//60:02.0f}:{seconds%60:06.3f}]"
|
||||
|
||||
def _mergeOffset(self, timestamp: int) -> int:
|
||||
result = timestamp
|
||||
|
||||
timestampMin = timestamp - CONFIG_LRC_AUTO_MERGE_OFFSET
|
||||
timestampMax = timestamp + CONFIG_LRC_AUTO_MERGE_OFFSET
|
||||
|
||||
for existLyric in self.lyrics.keys():
|
||||
if timestampMin <= existLyric and existLyric <= timestampMax:
|
||||
result = existLyric
|
||||
break
|
||||
|
||||
return result
|
103
src/ncmlyrics/util.py
Normal file
103
src/ncmlyrics/util.py
Normal file
@ -0,0 +1,103 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from re import compile as reCompile
|
||||
from urllib.parse import parse_qs as parseQuery
|
||||
from urllib.parse import urlparse as parseUrl
|
||||
from httpx import get as httpGet
|
||||
|
||||
from .api import NCMTrack
|
||||
from .error import ParseLinkError, UnsupportLinkError
|
||||
|
||||
RE_ANDROID_ALBUM_SHARE_LINK_PATH = reCompile(r"^/album/(?P<id>\d*)/?$")
|
||||
|
||||
|
||||
class LinkType(Enum):
|
||||
Song = auto()
|
||||
Album = auto()
|
||||
Playlist = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Link:
|
||||
type: LinkType
|
||||
id: int
|
||||
|
||||
|
||||
def parseLink(url: str) -> Link:
|
||||
parsedUrl = parseUrl(url, allow_fragments=False)
|
||||
contentType: LinkType | None = None
|
||||
contentId: int | None = None
|
||||
|
||||
match parsedUrl.netloc:
|
||||
case "music.163.com":
|
||||
match parsedUrl.path:
|
||||
case "/playlist" | "/#/playlist":
|
||||
contentType = LinkType.Playlist
|
||||
case "/album" | "/#/album":
|
||||
contentType = LinkType.Album
|
||||
case "/song" | "/#/song":
|
||||
contentType = LinkType.Song
|
||||
case _:
|
||||
# Hack for android client shared album link
|
||||
matchedPath = RE_ANDROID_ALBUM_SHARE_LINK_PATH.match(parsedUrl.path)
|
||||
if matchedPath is not None:
|
||||
contentType = LinkType.Album
|
||||
contentId = int(matchedPath["id"])
|
||||
else:
|
||||
raise UnsupportLinkError(parsedUrl)
|
||||
case "y.music.163.com":
|
||||
match parsedUrl.path:
|
||||
case "/m/playlist":
|
||||
contentType = LinkType.Playlist
|
||||
case "/m/song":
|
||||
contentType = LinkType.Song
|
||||
case _:
|
||||
raise UnsupportLinkError(parsedUrl)
|
||||
case "163cn.tv":
|
||||
response = httpGet(url)
|
||||
if response.status_code != 302:
|
||||
raise ParseLinkError(f"未知的 Api 响应: {response.status_code}")
|
||||
newUrl = response.headers.get("Location")
|
||||
if newUrl is None:
|
||||
raise ParseLinkError("Api 未返回重定向结果")
|
||||
return parseLink(newUrl)
|
||||
case _:
|
||||
raise UnsupportLinkError(parsedUrl)
|
||||
|
||||
if contentId is None:
|
||||
try:
|
||||
contentId = int(parseQuery(parsedUrl.query).get("id")[0])
|
||||
except Exception:
|
||||
raise ParseLinkError
|
||||
|
||||
return Link(contentType, contentId)
|
||||
|
||||
|
||||
def testTrackSourceExists(track: NCMTrack, path: Path) -> Path | None:
|
||||
for name in (f"{",".join(track.artists)} - {track.name}", f"{" ".join(track.artists)} - {track.name}"):
|
||||
for suffix in (".mp3", ".flac", ".ncm"):
|
||||
result = path.joinpath(name).with_suffix(suffix)
|
||||
if result.exists():
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
def pickOutput(track: NCMTrack, outputs: list[Path], forceSourceExists: bool = False) -> Path | None:
|
||||
match len(outputs):
|
||||
case 0:
|
||||
result = testTrackSourceExists(track, Path())
|
||||
if result is not None:
|
||||
return result.with_suffix(".lrc")
|
||||
return None if forceSourceExists else Path(f"{",".join(track.artists)} - {track.name}.lrc")
|
||||
case 1:
|
||||
result = testTrackSourceExists(track, outputs[0])
|
||||
if result is not None:
|
||||
return result.with_suffix(".lrc")
|
||||
return None if forceSourceExists else outputs[0] / f"{",".join(track.artists)} - {track.name}.lrc"
|
||||
case _:
|
||||
for output in outputs:
|
||||
result = testTrackSourceExists(track, output)
|
||||
if result is not None:
|
||||
return result.with_suffix(".lrc")
|
||||
return None if forceSourceExists else outputs[-1] / f"{",".join(track.artists)} - {track.name}.lrc"
|
3
tests/__init__.py
Normal file
3
tests/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .test_utils import TestUtils
|
||||
|
||||
__all__ = ["TestUtils"]
|
159
tests/test_utils.py
Normal file
159
tests/test_utils.py
Normal file
@ -0,0 +1,159 @@
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
|
||||
from ncmlyrics.api import NCMTrack
|
||||
from ncmlyrics.error import ParseLinkError, UnsupportLinkError
|
||||
from ncmlyrics.util import Link, LinkType, parseLink, testTrackSourceExists, pickOutput
|
||||
|
||||
|
||||
class TestUtils(TestCase):
|
||||
def test_parseLink_Windows(self):
|
||||
self.assertEqual(
|
||||
parseLink("https://music.163.com/playlist?id=444817519"),
|
||||
Link(LinkType.Playlist, 444817519),
|
||||
msg="Shared playlist from NCM Windows Client",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
parseLink("https://music.163.com/album?id=34609577"),
|
||||
Link(LinkType.Album, 34609577),
|
||||
msg="Shared album from NCM Windows Client",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
parseLink("https://music.163.com/song?id=2621105420"),
|
||||
Link(LinkType.Song, 2621105420),
|
||||
msg="Shared song from NCM Windows Client",
|
||||
)
|
||||
|
||||
def test_parseLink_Website(self):
|
||||
self.assertEqual(
|
||||
parseLink("https://music.163.com/#/playlist?id=444817519"),
|
||||
Link(LinkType.Playlist, 444817519),
|
||||
msg="Playlist from NCM Website",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
parseLink("https://music.163.com/#/album?id=34609577"),
|
||||
Link(LinkType.Album, 34609577),
|
||||
msg="Album from NCM Website",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
parseLink("https://music.163.com/#/song?id=2621105420"),
|
||||
Link(LinkType.Song, 2621105420),
|
||||
msg="Song from NCM Website",
|
||||
)
|
||||
|
||||
def test_parseLink_Android(self):
|
||||
self.assertEqual(
|
||||
parseLink("https://y.music.163.com/m/playlist?id=2224276126"),
|
||||
Link(LinkType.Playlist, 2224276126),
|
||||
msg="Shared playlist from NCM Android Client",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
parseLink("http://music.163.com/album/3139945/"),
|
||||
Link(LinkType.Album, 3139945),
|
||||
msg="Shared album from NCM Android Client",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
parseLink("https://y.music.163.com/m/song?id=2604307454"),
|
||||
Link(LinkType.Song, 2604307454),
|
||||
msg="Shared song from NCM Android Client",
|
||||
)
|
||||
|
||||
def test_parseLink_302(self):
|
||||
self.assertEqual(
|
||||
parseLink("http://163cn.tv/xpaQwii"),
|
||||
Link(LinkType.Song, 413077069),
|
||||
msg="Shared song from NCM Android Client player",
|
||||
)
|
||||
|
||||
def test_parseLink_UnsupportShareLinkError(self):
|
||||
self.assertRaises(
|
||||
UnsupportLinkError,
|
||||
parseLink,
|
||||
"https://www.google.com/",
|
||||
)
|
||||
|
||||
self.assertRaises(
|
||||
UnsupportLinkError,
|
||||
parseLink,
|
||||
"https://music.163.com/unsupport?id=123",
|
||||
)
|
||||
|
||||
self.assertRaises(
|
||||
UnsupportLinkError,
|
||||
parseLink,
|
||||
"https://music.163.com/album/123a",
|
||||
)
|
||||
|
||||
def test_parseLink_ParseShareLinkError(self):
|
||||
self.assertRaises(
|
||||
ParseLinkError,
|
||||
parseLink,
|
||||
"https://music.163.com/playlist?id=123a",
|
||||
)
|
||||
|
||||
def test_testTrackSourceExists(self):
|
||||
resources = Path("tests/resource/util/testTrackSourceExists")
|
||||
|
||||
self.assertEqual(
|
||||
testTrackSourceExists(NCMTrack(0, "Mp3Name", ["Mp3Artist"]), resources),
|
||||
resources / "Mp3Artist - Mp3Name.mp3",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
testTrackSourceExists(NCMTrack(0, "Mp3Name", ["Mp3Artist1", "Mp3Artist2"]), resources),
|
||||
resources / "Mp3Artist1,Mp3Artist2 - Mp3Name.mp3",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
testTrackSourceExists(NCMTrack(0, "FlacName", ["FlacArtist1", "FlacArtist2"]), resources),
|
||||
resources / "FlacArtist1 FlacArtist2 - FlacName.flac",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
testTrackSourceExists(NCMTrack(0, "NcmName", ["NcmArtist1", "NcmArtist2"]), resources),
|
||||
resources / "NcmArtist1,NcmArtist2 - NcmName.flac",
|
||||
)
|
||||
|
||||
def test_pickOutput(self):
|
||||
resources = Path("tests/resource/util/pickOutput")
|
||||
|
||||
self.assertEqual(
|
||||
pickOutput(NCMTrack(0, "testTrack", ["testArtist"]), []),
|
||||
Path("testArtist - testTrack.lrc"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
pickOutput(NCMTrack(0, "testTrack", ["testArtist"]), [resources / "1"]),
|
||||
resources / "1" / "testArtist - testTrack.lrc",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
pickOutput(NCMTrack(0, "testTrack1", ["testArtist"]), [resources / "1", resources / "2"]),
|
||||
resources / "1" / "testArtist - testTrack1.lrc",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
pickOutput(NCMTrack(0, "testTrack2", ["testArtist"]), [resources / "1", resources / "2"]),
|
||||
resources / "2" / "testArtist - testTrack2.lrc",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
pickOutput(NCMTrack(0, "testTrack1", ["testArtist"]), [resources / "2", resources / "1"]),
|
||||
resources / "1" / "testArtist - testTrack1.lrc",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
pickOutput(NCMTrack(0, str(), [str()]), [], True),
|
||||
None,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
pickOutput(NCMTrack(0, "testTrack0", ["testArtist"]), [resources / "1"], True),
|
||||
None,
|
||||
)
|
314
uv.lock
generated
Normal file
314
uv.lock
generated
Normal file
@ -0,0 +1,314 @@
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/49/f3f17ec11c4a91fe79275c426658e509b07547f874b14c1a526d86a83fc8/anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb", size = 170983 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a", size = 89631 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "4.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "hpack" },
|
||||
{ name = "hyperframe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/32/fec683ddd10629ea4ea46d206752a95a2d8a48c22521edd70b142488efe1/h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb", size = 2145593 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e5/db6d438da759efbb488c4f3fbdab7764492ff3c3f953132efa6b9f0e9e53/h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d", size = 57488 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hpack"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/fda93fb4d957db19b0f6b370e79d586b3e8528b20252c729c476a2c02954/hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095", size = 49117 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", size = 83234 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", size = 77926 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyperframe"
|
||||
version = "6.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/2a/4747bff0a17f7281abe73e955d60d80aae537a5d203f417fa1c2e7578ebb/hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914", size = 25008 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/de/85a784bcc4a3779d1753a7ec2dee5de90e18c7bcf402e71b51fcf150b129/hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", size = 12389 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ncmlyrics"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "brotli" },
|
||||
{ name = "click" },
|
||||
{ name = "h2" },
|
||||
{ name = "httpx" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "rich" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "brotli", specifier = ">=1.1.0" },
|
||||
{ name = "click", specifier = ">=8.1.7" },
|
||||
{ name = "h2", specifier = ">=4.1.0" },
|
||||
{ name = "httpx", specifier = ">=0.27.2" },
|
||||
{ name = "platformdirs", specifier = ">=4.3.6" },
|
||||
{ name = "rich", specifier = ">=13.8.1" },
|
||||
{ name = "zstandard", specifier = ">=0.23.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/76/40f084cb7db51c9d1fa29a7120717892aeda9a7711f6225692c957a93535/rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a", size = 222080 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/11/dadb85e2bd6b1f1ae56669c3e1f0410797f9605d752d68fb47b77f525b31/rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", size = 241608 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstandard"
|
||||
version = "0.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation == 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 },
|
||||
]
|
Loading…
Reference in New Issue
Block a user