mirror of
https://github.com/lifegpc/pixiv_downloader.git
synced 2026-06-06 05:49:01 +08:00
Add file
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
.vscode/
|
||||
1640
Cargo.lock
generated
Normal file
1640
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "pixiv_downloader"
|
||||
version = "0.0.1"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
dateparser = "0.1.6"
|
||||
getopts = "0.2"
|
||||
gettext = "0.4"
|
||||
html_parser = "0.6.2"
|
||||
lazy_static = "1.4"
|
||||
json = "0.12"
|
||||
reqwest = { version = "0.11", features = ["brotli", "deflate", "gzip", "rustls-tls", "socks", "stream"] }
|
||||
spin_on = "0.1.1"
|
||||
tokio = { version = "1.17", features = ["rt", "macros", "rt-multi-thread"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["winnls", "stringapiset"] }
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Server
|
||||
Copyright (C) 2021 REVOLT
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
226
Language/pixiv_downloader.pot
Normal file
226
Language/pixiv_downloader.pot
Normal file
@@ -0,0 +1,226 @@
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: bili\n"
|
||||
"POT-Creation-Date: 2022-02-26 23:42+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <[email protected]>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.0\n"
|
||||
"X-Poedit-Basepath: ../src\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
#: cookies.rs:75 cookies.rs:217
|
||||
msgid "Warning: Failed to parse URL:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:90
|
||||
msgid "Warning: Failed to parse cookie's key and value:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:107
|
||||
msgid "Warning: Expires need a date."
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:117
|
||||
msgid "Failed to parse UTC string:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:127
|
||||
msgid "Warning: Max-Age need a duration."
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:134
|
||||
msgid "Failed to parse Max-Age:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:142
|
||||
msgid "Warning: Domain need a domain."
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:148
|
||||
msgid "Warning: Path need a path."
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:155
|
||||
msgid "Warning: path is not starts with \"/\":"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:168
|
||||
msgid "Warning: Failed to get domain."
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:314
|
||||
msgid "Can not find file:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:319
|
||||
msgid "Can not open file:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:332
|
||||
msgid "Invalid cookie:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:344
|
||||
msgid "Can not parse expired time:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:366 settings.rs:392
|
||||
msgid "Failed to remove file:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:372 settings.rs:399
|
||||
msgid "Failed to create file:"
|
||||
msgstr ""
|
||||
|
||||
#: cookies.rs:379 settings.rs:405
|
||||
msgid "Failed to write file:"
|
||||
msgstr ""
|
||||
|
||||
#: download.rs:9 pixiv.rs:47
|
||||
msgid "Failed to initialize pixiv web api client."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:69
|
||||
msgid "Warning: The specified config file not found."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:93
|
||||
msgid "Usage:"
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:95
|
||||
msgid "Download an artwork"
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:97
|
||||
msgid "Fix the config file"
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:99
|
||||
msgid "Print all available settings"
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:107
|
||||
msgid "Print help message."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:111
|
||||
msgid "The location of config file."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:117 settings_list.rs:8
|
||||
msgid "The location of cookies file. Used for web API."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:123 settings_list.rs:9
|
||||
msgid "The language of translated tags."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:145
|
||||
msgid "Unknown command."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:156
|
||||
msgid "No URL specified."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:164
|
||||
msgid "No detailed command specified."
|
||||
msgstr ""
|
||||
|
||||
#: opts.rs:177
|
||||
msgid "Unknown config subcommand."
|
||||
msgstr ""
|
||||
|
||||
#: parser\mainpage.rs:53 parser/mainpage.rs:53
|
||||
msgid "Failed to parse JSON:"
|
||||
msgstr ""
|
||||
|
||||
#: parser\mainpage.rs:75 parser/mainpage.rs:75
|
||||
msgid "Failed to parse HTML:"
|
||||
msgstr ""
|
||||
|
||||
#: parser\mainpage.rs:80 parser/mainpage.rs:80
|
||||
msgid "Some errors occured during parsing:"
|
||||
msgstr ""
|
||||
|
||||
#: pixiv.rs:63 pixiv.rs:68
|
||||
msgid "Failed to get main page:"
|
||||
msgstr ""
|
||||
|
||||
#: pixiv.rs:74
|
||||
msgid "Failed to parse main page."
|
||||
msgstr ""
|
||||
|
||||
#: pixiv.rs:88
|
||||
msgid "Warning: Failed to save cookies file:"
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:29
|
||||
msgid "Multiple type"
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:264
|
||||
msgid "Can not insert setting to JSON object."
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:327
|
||||
msgid "Settings file is empty."
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:334
|
||||
msgid "Can not read from settings file."
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:343
|
||||
msgid "Can not parse settings file."
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:352
|
||||
msgid "Unknown settings file."
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:363
|
||||
msgid "\"<key>\" is invalid, you can use \"pixiv_downloader config fix\" to remove all invalid value."
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:382
|
||||
msgid "Can not convert settings to JSON object."
|
||||
msgstr ""
|
||||
|
||||
#: settings.rs:410
|
||||
msgid "Failed to flush file:"
|
||||
msgstr ""
|
||||
|
||||
#: settings_list.rs:7
|
||||
msgid "Pixiv's refresh tokens. Used to login."
|
||||
msgstr ""
|
||||
|
||||
#: webclient.rs:59
|
||||
msgid "Failed to parse Set-Cookie header."
|
||||
msgstr ""
|
||||
|
||||
#: webclient.rs:64
|
||||
msgid "Failed to convert to string:"
|
||||
msgstr ""
|
||||
|
||||
#: webclient.rs:94
|
||||
msgid "Error when request:"
|
||||
msgstr ""
|
||||
|
||||
#: main.rs:46
|
||||
msgid "Failed to save config file:"
|
||||
msgstr ""
|
||||
|
||||
#: main.rs:57
|
||||
msgid "All available settings:"
|
||||
msgstr ""
|
||||
|
||||
#: main.rs:89
|
||||
msgid "Can not read config file:"
|
||||
msgstr ""
|
||||
231
Language/pixiv_downloader.zh_CN.po
Normal file
231
Language/pixiv_downloader.zh_CN.po
Normal file
@@ -0,0 +1,231 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: bili\n"
|
||||
"POT-Creation-Date: 2022-02-26 23:42+0800\n"
|
||||
"PO-Revision-Date: 2022-02-26 23:43+0800\n"
|
||||
"Last-Translator: lifegpc <[email protected]>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.0\n"
|
||||
"X-Poedit-Basepath: ../src\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
#: cookies.rs:75 cookies.rs:217
|
||||
msgid "Warning: Failed to parse URL:"
|
||||
msgstr "警告:无法解析 URL:"
|
||||
|
||||
#: cookies.rs:90
|
||||
msgid "Warning: Failed to parse cookie's key and value:"
|
||||
msgstr "警告:无法解析 cookies 的键与值:"
|
||||
|
||||
#: cookies.rs:107
|
||||
msgid "Warning: Expires need a date."
|
||||
msgstr "警告:Expires 需要指定时间。"
|
||||
|
||||
#: cookies.rs:117
|
||||
msgid "Failed to parse UTC string:"
|
||||
msgstr "无法解析UTC时间:"
|
||||
|
||||
#: cookies.rs:127
|
||||
msgid "Warning: Max-Age need a duration."
|
||||
msgstr "警告:Max-Age 需要指定时长。"
|
||||
|
||||
#: cookies.rs:134
|
||||
msgid "Failed to parse Max-Age:"
|
||||
msgstr "无法解析 Max-Age:"
|
||||
|
||||
#: cookies.rs:142
|
||||
msgid "Warning: Domain need a domain."
|
||||
msgstr "警告:Domain 需要指定域名。"
|
||||
|
||||
#: cookies.rs:148
|
||||
msgid "Warning: Path need a path."
|
||||
msgstr "警告:Path 需要一个路径。"
|
||||
|
||||
#: cookies.rs:155
|
||||
msgid "Warning: path is not starts with \"/\":"
|
||||
msgstr "警告:路径没有以 \"/\" 开头:"
|
||||
|
||||
#: cookies.rs:168
|
||||
msgid "Warning: Failed to get domain."
|
||||
msgstr "警告:无法获取域名。"
|
||||
|
||||
#: cookies.rs:314
|
||||
msgid "Can not find file:"
|
||||
msgstr "无法找到文件:"
|
||||
|
||||
#: cookies.rs:319
|
||||
msgid "Can not open file:"
|
||||
msgstr "无法打开文件:"
|
||||
|
||||
#: cookies.rs:332
|
||||
msgid "Invalid cookie:"
|
||||
msgstr "无效的Cookie:"
|
||||
|
||||
#: cookies.rs:344
|
||||
msgid "Can not parse expired time:"
|
||||
msgstr "无法解析过期时间:"
|
||||
|
||||
#: cookies.rs:366 settings.rs:392
|
||||
msgid "Failed to remove file:"
|
||||
msgstr "无法删除文件:"
|
||||
|
||||
#: cookies.rs:372 settings.rs:399
|
||||
msgid "Failed to create file:"
|
||||
msgstr "无法创建文件:"
|
||||
|
||||
#: cookies.rs:379 settings.rs:405
|
||||
msgid "Failed to write file:"
|
||||
msgstr "无法写入文件:"
|
||||
|
||||
#: download.rs:9 pixiv.rs:47
|
||||
msgid "Failed to initialize pixiv web api client."
|
||||
msgstr "无法初始化 Pixiv 网页 API 客户端。"
|
||||
|
||||
#: opts.rs:69
|
||||
msgid "Warning: The specified config file not found."
|
||||
msgstr "警告:没有找到指定的设置文件。"
|
||||
|
||||
#: opts.rs:93
|
||||
msgid "Usage:"
|
||||
msgstr "使用方法:"
|
||||
|
||||
#: opts.rs:95
|
||||
msgid "Download an artwork"
|
||||
msgstr "下载一个作品"
|
||||
|
||||
#: opts.rs:97
|
||||
msgid "Fix the config file"
|
||||
msgstr "修复设置文件"
|
||||
|
||||
#: opts.rs:99
|
||||
msgid "Print all available settings"
|
||||
msgstr "打印所有可用的设置"
|
||||
|
||||
#: opts.rs:107
|
||||
msgid "Print help message."
|
||||
msgstr "打印帮助信息。"
|
||||
|
||||
#: opts.rs:111
|
||||
msgid "The location of config file."
|
||||
msgstr "设置文件的位置。"
|
||||
|
||||
#: opts.rs:117 settings_list.rs:8
|
||||
msgid "The location of cookies file. Used for web API."
|
||||
msgstr "cookies 文件的位置。用于网页 API。"
|
||||
|
||||
#: opts.rs:123 settings_list.rs:9
|
||||
msgid "The language of translated tags."
|
||||
msgstr "翻译后的标签语言。"
|
||||
|
||||
#: opts.rs:145
|
||||
msgid "Unknown command."
|
||||
msgstr "未知指令。"
|
||||
|
||||
#: opts.rs:156
|
||||
msgid "No URL specified."
|
||||
msgstr "没有指定网址。"
|
||||
|
||||
#: opts.rs:164
|
||||
msgid "No detailed command specified."
|
||||
msgstr "没有指定更详细的指令。"
|
||||
|
||||
#: opts.rs:177
|
||||
msgid "Unknown config subcommand."
|
||||
msgstr "未知的 config 子指令。"
|
||||
|
||||
#: parser\mainpage.rs:53 parser/mainpage.rs:53
|
||||
msgid "Failed to parse JSON:"
|
||||
msgstr "无法解析 JSON:"
|
||||
|
||||
#: parser\mainpage.rs:75 parser/mainpage.rs:75
|
||||
msgid "Failed to parse HTML:"
|
||||
msgstr "无法解析 HTML:"
|
||||
|
||||
#: parser\mainpage.rs:80 parser/mainpage.rs:80
|
||||
msgid "Some errors occured during parsing:"
|
||||
msgstr "在解析中发生了一些错误:"
|
||||
|
||||
#: pixiv.rs:63 pixiv.rs:68
|
||||
msgid "Failed to get main page:"
|
||||
msgstr "无法获取主页:"
|
||||
|
||||
#: pixiv.rs:74
|
||||
msgid "Failed to parse main page."
|
||||
msgstr "无法解析主页。"
|
||||
|
||||
#: pixiv.rs:88
|
||||
msgid "Warning: Failed to save cookies file:"
|
||||
msgstr "警告:无法保存 cookies 文件:"
|
||||
|
||||
#: settings.rs:29
|
||||
msgid "Multiple type"
|
||||
msgstr "多种类型"
|
||||
|
||||
#: settings.rs:264
|
||||
msgid "Can not insert setting to JSON object."
|
||||
msgstr "无法将设置插入 JSON 对象。"
|
||||
|
||||
#: settings.rs:327
|
||||
msgid "Settings file is empty."
|
||||
msgstr "设置文件是空的。"
|
||||
|
||||
#: settings.rs:334
|
||||
msgid "Can not read from settings file."
|
||||
msgstr "无法读取设置文件。"
|
||||
|
||||
#: settings.rs:343
|
||||
msgid "Can not parse settings file."
|
||||
msgstr "无法解析设置文件。"
|
||||
|
||||
#: settings.rs:352
|
||||
msgid "Unknown settings file."
|
||||
msgstr "未知的设置文件。"
|
||||
|
||||
#: settings.rs:363
|
||||
msgid ""
|
||||
"\"<key>\" is invalid, you can use \"pixiv_downloader config fix\" to remove "
|
||||
"all invalid value."
|
||||
msgstr ""
|
||||
"\"<key>\" 不合法,您可以使用 \"pixiv_downloader config fix\" 来移除所有不合"
|
||||
"法的值。"
|
||||
|
||||
#: settings.rs:382
|
||||
msgid "Can not convert settings to JSON object."
|
||||
msgstr "无法将设置转换为 JSON 对象。"
|
||||
|
||||
#: settings.rs:410
|
||||
msgid "Failed to flush file:"
|
||||
msgstr "无法刷新文件缓冲区:"
|
||||
|
||||
#: settings_list.rs:7
|
||||
msgid "Pixiv's refresh tokens. Used to login."
|
||||
msgstr "Pixiv 的 refresh tokens。用于登录。"
|
||||
|
||||
#: webclient.rs:59
|
||||
msgid "Failed to parse Set-Cookie header."
|
||||
msgstr "无法解析 Set-Cookie 头部。"
|
||||
|
||||
#: webclient.rs:64
|
||||
msgid "Failed to convert to string:"
|
||||
msgstr "无法转换为字符串:"
|
||||
|
||||
#: webclient.rs:94
|
||||
msgid "Error when request:"
|
||||
msgstr "请求时发生错误:"
|
||||
|
||||
#: main.rs:46
|
||||
msgid "Failed to save config file:"
|
||||
msgstr "无法保存设置文件:"
|
||||
|
||||
#: main.rs:57
|
||||
msgid "All available settings:"
|
||||
msgstr "所有可用的设置:"
|
||||
|
||||
#: main.rs:89
|
||||
msgid "Can not read config file:"
|
||||
msgstr "无法读取设置文件:"
|
||||
389
src/cookies.rs
Normal file
389
src/cookies.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
use crate::gettext;
|
||||
use chrono::DateTime;
|
||||
use chrono::TimeZone;
|
||||
use chrono::Utc;
|
||||
use reqwest::IntoUrl;
|
||||
use std::fs::{remove_file, File};
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::iter::Iterator;
|
||||
use std::path::Path;
|
||||
|
||||
trait ToNetscapeStr {
|
||||
fn to_netscape_str(&self) -> &'static str;
|
||||
}
|
||||
|
||||
impl ToNetscapeStr for bool {
|
||||
fn to_netscape_str(&self) -> &'static str {
|
||||
if *self {
|
||||
"TRUE"
|
||||
} else {
|
||||
"FALSE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
/// Cookies structure
|
||||
pub struct Cookie {
|
||||
/// Cookie's name
|
||||
_name: String,
|
||||
/// Cookie's value
|
||||
_value: String,
|
||||
/// Whether to include subdomains
|
||||
_subdomains: bool,
|
||||
/// Cookie's Path
|
||||
_path: String,
|
||||
/// HTTP only
|
||||
_http_only: bool,
|
||||
/// Expired time
|
||||
_expired: Option<DateTime<Utc>>,
|
||||
/// Domain name
|
||||
_domain: String,
|
||||
}
|
||||
|
||||
impl Cookie {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
value: &str,
|
||||
domain: &str,
|
||||
subdomains: bool,
|
||||
path: &str,
|
||||
http_only: bool,
|
||||
expired: Option<DateTime<Utc>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
_name: name.to_string(),
|
||||
_value: value.to_string(),
|
||||
_subdomains: subdomains,
|
||||
_path: path.to_string(),
|
||||
_http_only: http_only,
|
||||
_expired: expired.clone(),
|
||||
_domain: domain.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_set_cookie<U: IntoUrl>(url: U, header: &str) -> Option<Self> {
|
||||
let mut subdomain = false;
|
||||
let mut http_only = false;
|
||||
let mut expired: i64 = 0;
|
||||
let u = url.into_url();
|
||||
if u.is_err() {
|
||||
println!(
|
||||
"{} {}",
|
||||
gettext("Warning: Failed to parse URL:"),
|
||||
u.unwrap_err()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let u = u.unwrap();
|
||||
let mut path = u.path().to_string();
|
||||
let t = String::from(header);
|
||||
let l: Vec<&str> = t.split(";").collect();
|
||||
let m = l[0];
|
||||
let t = String::from(m);
|
||||
let l2: Vec<&str> = t.split("=").collect();
|
||||
if l2.len() < 2 {
|
||||
println!(
|
||||
"{} {}",
|
||||
gettext("Warning: Failed to parse cookie's key and value:"),
|
||||
m
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let key = l2[0];
|
||||
let value = l2[1];
|
||||
let mut domain = match u.host_str() {
|
||||
Some(s) => Some(String::from(s)),
|
||||
None => None,
|
||||
};
|
||||
for v in l.iter().skip(2) {
|
||||
let t = String::from(*v).trim().to_string();
|
||||
let ll: Vec<&str> = t.split("=").collect();
|
||||
let k = ll[0].to_lowercase();
|
||||
if k == "expires" {
|
||||
if ll.len() < 2 {
|
||||
println!("{}", gettext("Warning: Expires need a date."));
|
||||
return None;
|
||||
}
|
||||
let mut re = dateparser::parse(ll[1]);
|
||||
if re.is_err() {
|
||||
let s = ll[1].replace("-", " ");
|
||||
re = dateparser::parse(s.as_str());
|
||||
if re.is_err() {
|
||||
println!(
|
||||
"{} {}",
|
||||
gettext("Failed to parse UTC string:"),
|
||||
re.unwrap_err()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let r = re.unwrap();
|
||||
expired = r.timestamp();
|
||||
} else if k == "max-age" {
|
||||
if ll.len() < 2 {
|
||||
println!("{}", gettext("Warning: Max-Age need a duration."));
|
||||
return None;
|
||||
}
|
||||
let re = ll[1].parse::<i64>();
|
||||
if re.is_err() {
|
||||
println!(
|
||||
"{} {}",
|
||||
gettext("Failed to parse Max-Age:"),
|
||||
re.unwrap_err()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
expired = re.unwrap() + Utc::now().timestamp();
|
||||
} else if k == "domain" {
|
||||
if ll.len() < 2 {
|
||||
println!("{}", gettext("Warning: Domain need a domain."));
|
||||
return None;
|
||||
}
|
||||
domain = Some(String::from(ll[1]));
|
||||
} else if k == "path" {
|
||||
if ll.len() < 2 {
|
||||
println!("{}", gettext("Warning: Path need a path."));
|
||||
return None;
|
||||
}
|
||||
let p = ll[1].to_string();
|
||||
if !p.starts_with("/") {
|
||||
println!(
|
||||
"{} {}",
|
||||
gettext("Warning: path is not starts with \"/\":"),
|
||||
p.as_str()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
path = p;
|
||||
} else if k == "httponly" {
|
||||
http_only = true;
|
||||
} else if k == "secure" || k == "samesite" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if domain.is_none() {
|
||||
println!("{}", gettext("Warning: Failed to get domain."));
|
||||
return None;
|
||||
}
|
||||
let domain = domain.unwrap();
|
||||
if domain.starts_with(".") {
|
||||
subdomain = true;
|
||||
}
|
||||
let expired = if expired == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Utc.timestamp(expired, 0))
|
||||
};
|
||||
Some(Self::new(
|
||||
key,
|
||||
value,
|
||||
domain.as_str(),
|
||||
subdomain,
|
||||
path.as_str(),
|
||||
http_only,
|
||||
expired,
|
||||
))
|
||||
}
|
||||
|
||||
/// Get name and value string: name=value;
|
||||
pub fn get_name_value(&self) -> String {
|
||||
format!("{}={};", self._name.as_str(), self._value.as_str())
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
if self._expired.is_some() {
|
||||
let now = Utc::now();
|
||||
if now > self._expired.as_ref().unwrap().clone() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_same_key(&self, other: &Self) -> bool {
|
||||
self._name == other._name && self._domain == other._domain
|
||||
}
|
||||
|
||||
/// Check if url is matched
|
||||
/// * `url` - URL
|
||||
pub fn matched<U: IntoUrl>(&self, url: U) -> bool {
|
||||
let u = url.into_url();
|
||||
if u.is_err() {
|
||||
println!(
|
||||
"{} {}",
|
||||
gettext("Warning: Failed to parse URL:"),
|
||||
u.unwrap_err()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if self.is_expired() {
|
||||
return false;
|
||||
}
|
||||
let u = u.unwrap();
|
||||
let host = u.host_str();
|
||||
if host.is_none() {
|
||||
return false;
|
||||
}
|
||||
let host = host.unwrap();
|
||||
let subdomain = self._subdomains || self._domain.starts_with(".");
|
||||
let domain = if subdomain && !self._domain.starts_with(".") {
|
||||
String::from(".") + &self._domain
|
||||
} else {
|
||||
self._domain.clone()
|
||||
};
|
||||
if subdomain && !host.starts_with(&domain) && host != &domain[1..] {
|
||||
return false;
|
||||
}
|
||||
if !subdomain && host != domain {
|
||||
return false;
|
||||
}
|
||||
let path = u.path();
|
||||
if !path.starts_with(&self._path) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn expired_time(&self) -> i64 {
|
||||
match &self._expired {
|
||||
Some(k) => k.timestamp(),
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_netscape_str(&self) -> String {
|
||||
format!(
|
||||
"{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
|
||||
self._domain,
|
||||
self._subdomains.to_netscape_str(),
|
||||
self._path,
|
||||
self._http_only.to_netscape_str(),
|
||||
self.expired_time(),
|
||||
self._name,
|
||||
self._value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Cookies Jar
|
||||
pub struct CookieJar {
|
||||
cookies: Vec<Cookie>,
|
||||
}
|
||||
|
||||
impl CookieJar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cookies: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, c: Cookie) {
|
||||
let mut i = 0;
|
||||
while i < self.cookies.len() {
|
||||
let a = &self.cookies[i];
|
||||
if a.is_same_key(&c) {
|
||||
self.cookies[i] = c;
|
||||
return;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
self.cookies.push(c);
|
||||
}
|
||||
|
||||
/// Check and remove all expired cookies
|
||||
pub fn check_expired(&mut self) {
|
||||
let mut i = 0;
|
||||
while i < self.cookies.len() {
|
||||
let c = &self.cookies[i];
|
||||
if c.is_expired() {
|
||||
self.cookies.remove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self, file_name: &str) -> bool {
|
||||
self.cookies.clear();
|
||||
let p = Path::new(file_name);
|
||||
if !p.exists() {
|
||||
println!("{} {}", gettext("Can not find file:"), file_name);
|
||||
return false;
|
||||
}
|
||||
let re = File::open(p);
|
||||
if re.is_err() {
|
||||
println!("{} {}", gettext("Can not open file:"), file_name);
|
||||
return false;
|
||||
}
|
||||
let f = re.unwrap();
|
||||
let r = BufReader::new(f);
|
||||
for line in r.lines() {
|
||||
let mut l = line.unwrap();
|
||||
l = l.trim().to_string();
|
||||
if l.starts_with("#") {
|
||||
continue;
|
||||
}
|
||||
let mut s = l.split('\t');
|
||||
if s.clone().count() < 7 {
|
||||
println!("{} {}", gettext("Invalid cookie:"), l);
|
||||
return false;
|
||||
}
|
||||
let domain = s.next().unwrap();
|
||||
let subdomains = s.next().unwrap() != "FALSE";
|
||||
let path = s.next().unwrap();
|
||||
let http_only = s.next().unwrap() != "FALSE";
|
||||
let expired = s.next().unwrap();
|
||||
let name = s.next().unwrap();
|
||||
let value = s.next().unwrap();
|
||||
let tmp = expired.trim().parse::<i64>();
|
||||
if tmp.is_err() {
|
||||
println!("{} {}", gettext("Can not parse expired time:"), expired);
|
||||
return false;
|
||||
}
|
||||
let tmp = tmp.unwrap();
|
||||
let expired = if tmp == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Utc.timestamp(tmp, 0))
|
||||
};
|
||||
let c = Cookie::new(name, value, domain, subdomains, path, http_only, expired);
|
||||
self.add(c);
|
||||
}
|
||||
self.check_expired();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn save(&mut self, file_name: &str) -> bool {
|
||||
let p = Path::new(file_name);
|
||||
self.check_expired();
|
||||
if p.exists() {
|
||||
let re = remove_file(p);
|
||||
if re.is_err() {
|
||||
println!("{} {}", gettext("Failed to remove file:"), re.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let re = File::create(p);
|
||||
if re.is_err() {
|
||||
println!("{} {}", gettext("Failed to create file:"), re.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
let mut f = re.unwrap();
|
||||
for c in self.cookies.iter() {
|
||||
let r = write!(f, "{}", c.to_netscape_str().as_str());
|
||||
if r.is_err() {
|
||||
println!("{} {}", gettext("Failed to write file:"), r.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> core::slice::Iter<Cookie> {
|
||||
self.cookies.iter()
|
||||
}
|
||||
}
|
||||
15
src/download.rs
Normal file
15
src/download.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::gettext;
|
||||
use crate::pixiv::PixivWebClient;
|
||||
use crate::Main;
|
||||
|
||||
impl Main {
|
||||
pub fn download(&mut self) -> i32 {
|
||||
let mut pw = PixivWebClient::new(&self);
|
||||
if !pw.init() {
|
||||
println!("{}", gettext("Failed to initialize pixiv web api client."));
|
||||
return 1;
|
||||
}
|
||||
pw.check_login();
|
||||
0
|
||||
}
|
||||
}
|
||||
167
src/i18n.rs
Normal file
167
src/i18n.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
#[cfg(windows)]
|
||||
extern crate winapi;
|
||||
|
||||
use crate::utils::get_exe_path_else_current;
|
||||
use gettext::Catalog;
|
||||
use std::fs::File;
|
||||
|
||||
pub fn get_lang() -> String {
|
||||
let lan = std::env::var("LANG");
|
||||
match lan {
|
||||
Ok(l) => {
|
||||
if l.len() > 0 {
|
||||
return l;
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::alloc::alloc;
|
||||
use std::alloc::dealloc;
|
||||
use std::alloc::Layout;
|
||||
use std::mem::align_of;
|
||||
use std::mem::size_of;
|
||||
use std::ptr::null_mut;
|
||||
use winapi::um::stringapiset::WideCharToMultiByte;
|
||||
use winapi::um::winnls::GetUserDefaultLCID;
|
||||
use winapi::um::winnls::LCIDToLocaleName;
|
||||
use winapi::um::winnls::CP_UTF8;
|
||||
use winapi::um::winnls::WC_ERR_INVALID_CHARS;
|
||||
use winapi::um::winnt::LPSTR;
|
||||
use winapi::um::winnt::LPWSTR;
|
||||
use winapi::um::winnt::WCHAR;
|
||||
unsafe {
|
||||
let lcid = GetUserDefaultLCID();
|
||||
let len = LCIDToLocaleName(lcid, null_mut(), 0, 0);
|
||||
if len > 0 {
|
||||
let align = align_of::<WCHAR>();
|
||||
let s = size_of::<WCHAR>();
|
||||
let layout = Layout::from_size_align(len as usize * s, align);
|
||||
match layout {
|
||||
Ok(lay) => {
|
||||
let pstr = alloc(lay) as LPWSTR;
|
||||
let re = LCIDToLocaleName(lcid, pstr, len, 0);
|
||||
let mut result = String::from("");
|
||||
if re > 0 {
|
||||
let mlen = WideCharToMultiByte(
|
||||
CP_UTF8,
|
||||
WC_ERR_INVALID_CHARS,
|
||||
pstr,
|
||||
len,
|
||||
null_mut(),
|
||||
0,
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
);
|
||||
if mlen > 0 {
|
||||
let ali = align_of::<u8>();
|
||||
let layout = Layout::from_size_align(mlen as usize, ali);
|
||||
match layout {
|
||||
Ok(lay) => {
|
||||
let pmstr = alloc(lay) as LPSTR;
|
||||
let re = WideCharToMultiByte(
|
||||
CP_UTF8,
|
||||
WC_ERR_INVALID_CHARS,
|
||||
pstr,
|
||||
len,
|
||||
pmstr,
|
||||
mlen,
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
);
|
||||
if re > 0 {
|
||||
result = String::from_raw_parts(
|
||||
pmstr as *mut u8,
|
||||
mlen as usize,
|
||||
mlen as usize,
|
||||
);
|
||||
} else {
|
||||
dealloc(pmstr as *mut u8, lay);
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
dealloc(pstr as *mut u8, lay);
|
||||
if result.len() > 0 {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return String::from("en-US");
|
||||
}
|
||||
|
||||
pub struct I18n {
|
||||
catalog: Option<Catalog>,
|
||||
}
|
||||
|
||||
fn open_mo_file(molang: &str) -> Option<File> {
|
||||
let mut pb = get_exe_path_else_current();
|
||||
let base = String::from("pixiv_downloader");
|
||||
let fname = base + "." + molang.replace("-", "_").as_str() + ".mo";
|
||||
pb.push(fname);
|
||||
let p = pb.as_path();
|
||||
if p.exists() {
|
||||
let f = File::open(p);
|
||||
match f {
|
||||
Ok(f) => {
|
||||
return Some(f);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
impl I18n {
|
||||
pub fn new() -> I18n {
|
||||
let s = get_lang();
|
||||
let mut molang = s.as_str();
|
||||
if s.starts_with("zh") {
|
||||
let t = s.to_lowercase();
|
||||
if t == "zh-tw" || t == "zh-hant" || t == "zh-hk" {
|
||||
molang = "zh_TW";
|
||||
} else {
|
||||
molang = "zh_CN";
|
||||
}
|
||||
}
|
||||
let mut catalog: Option<Catalog> = None;
|
||||
let re = open_mo_file(molang);
|
||||
match re {
|
||||
Some(f) => {
|
||||
let re = Catalog::parse(f);
|
||||
match re {
|
||||
Ok(c) => {
|
||||
catalog = Some(c);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
return I18n { catalog: catalog };
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref I18NT: I18n = I18n::new();
|
||||
}
|
||||
|
||||
/// Get translation of text
|
||||
/// * `s` - Origin text
|
||||
pub fn gettext(s: &str) -> &str {
|
||||
match &I18NT.catalog {
|
||||
Some(c) => {
|
||||
return c.gettext(s);
|
||||
}
|
||||
None => {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/main.rs
Normal file
111
src/main.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
extern crate chrono;
|
||||
extern crate dateparser;
|
||||
extern crate json;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
extern crate tokio;
|
||||
extern crate reqwest;
|
||||
|
||||
mod cookies;
|
||||
mod download;
|
||||
mod i18n;
|
||||
mod opthelper;
|
||||
mod opts;
|
||||
mod parser;
|
||||
mod pixiv;
|
||||
mod settings;
|
||||
mod settings_list;
|
||||
mod utils;
|
||||
mod webclient;
|
||||
|
||||
use i18n::gettext;
|
||||
use opts::Command;
|
||||
use opts::CommandOpts;
|
||||
use opts::ConfigCommand;
|
||||
use settings::SettingStore;
|
||||
|
||||
pub struct Main {
|
||||
pub cmd: Option<CommandOpts>,
|
||||
pub settings: Option<SettingStore>,
|
||||
}
|
||||
|
||||
impl Main {
|
||||
pub fn deal_config_cmd(&mut self) -> i32 {
|
||||
let cmd = self.cmd.as_ref().unwrap();
|
||||
let subcmd = cmd.config_cmd.as_ref().unwrap();
|
||||
match subcmd {
|
||||
ConfigCommand::Fix => {
|
||||
let s = self.settings.as_ref().unwrap();
|
||||
let conf = cmd.config();
|
||||
if conf.is_some() {
|
||||
if s.save(conf.as_ref().unwrap()) {
|
||||
0
|
||||
} else {
|
||||
println!(
|
||||
"{} {}",
|
||||
gettext("Failed to save config file:"),
|
||||
conf.as_ref().unwrap()
|
||||
);
|
||||
1
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
ConfigCommand::Help => {
|
||||
let s = self.settings.as_ref().unwrap();
|
||||
println!("{}", gettext("All available settings:"));
|
||||
s.basic.print_help();
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cmd: None,
|
||||
settings: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> i32 {
|
||||
self.cmd = opts::parse_cmd();
|
||||
if self.cmd.is_none() {
|
||||
return 0;
|
||||
}
|
||||
let cmd = self.cmd.as_ref().unwrap();
|
||||
self.settings = Some(SettingStore::default());
|
||||
match cmd.config() {
|
||||
Some(conf) => {
|
||||
let fix_invalid = if cmd.cmd == Command::Config
|
||||
&& cmd.config_cmd.as_ref().unwrap() == ConfigCommand::Fix
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let r = self.settings.as_mut().unwrap().read(&conf, fix_invalid);
|
||||
if !r {
|
||||
println!("{} {}", gettext("Can not read config file:"), conf.as_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match cmd.cmd {
|
||||
Command::Config => {
|
||||
self.deal_config_cmd();
|
||||
}
|
||||
Command::Download => {
|
||||
return self.download();
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut m = Main::new();
|
||||
std::process::exit(m.run());
|
||||
}
|
||||
34
src/opthelper.rs
Normal file
34
src/opthelper.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use crate::opts::CommandOpts;
|
||||
use crate::settings::SettingStore;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OptHelper<'a> {
|
||||
opt: &'a CommandOpts,
|
||||
settings: &'a SettingStore,
|
||||
}
|
||||
|
||||
impl<'a> OptHelper<'a> {
|
||||
pub fn cookies(&self) -> Option<String> {
|
||||
if self.opt.cookies.is_some() {
|
||||
self.opt.cookies.clone()
|
||||
} else if self.settings.have_str("cookies") {
|
||||
self.settings.get_str("cookies")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language(&self) -> Option<String> {
|
||||
if self.opt.language.is_some() {
|
||||
self.opt.language.clone()
|
||||
} else if self.settings.have_str("language") {
|
||||
self.settings.get_str("language")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(opt: &'a CommandOpts, settings: &'a SettingStore) -> Self {
|
||||
Self { opt, settings }
|
||||
}
|
||||
}
|
||||
193
src/opts.rs
Normal file
193
src/opts.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
extern crate getopts;
|
||||
|
||||
use crate::gettext;
|
||||
use crate::utils::check_file_exists;
|
||||
use crate::utils::get_exe_path_else_current;
|
||||
use getopts::Options;
|
||||
use std::env;
|
||||
|
||||
/// Command Line command
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Command {
|
||||
/// Do something for the config
|
||||
Config,
|
||||
/// Download an artwork
|
||||
Download,
|
||||
}
|
||||
|
||||
/// Subcommand for config
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ConfigCommand {
|
||||
/// Fix the config file
|
||||
Fix,
|
||||
/// Print all available settings
|
||||
Help,
|
||||
}
|
||||
|
||||
impl PartialEq<ConfigCommand> for &ConfigCommand {
|
||||
fn eq(&self, other: &ConfigCommand) -> bool {
|
||||
other == *self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Command Line Options
|
||||
pub struct CommandOpts {
|
||||
/// Command
|
||||
pub cmd: Command,
|
||||
/// URLs
|
||||
pub urls: Vec<String>,
|
||||
/// Config location
|
||||
pub _config: Option<String>,
|
||||
/// Config command
|
||||
pub config_cmd: Option<ConfigCommand>,
|
||||
/// The location of cookies file
|
||||
pub cookies: Option<String>,
|
||||
/// The language of translated tags
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
impl CommandOpts {
|
||||
pub fn new(cmd: Command) -> Self {
|
||||
Self {
|
||||
cmd,
|
||||
urls: Vec::new(),
|
||||
_config: None,
|
||||
config_cmd: None,
|
||||
cookies: None,
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Option<String> {
|
||||
if self._config.is_some() {
|
||||
if check_file_exists(&self._config.as_ref().unwrap()) {
|
||||
self._config.clone()
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
gettext("Warning: The specified config file not found.")
|
||||
);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let mut pb = get_exe_path_else_current();
|
||||
pb = pb.join("pixiv_downloader.json");
|
||||
if pb.exists() {
|
||||
return Some(String::from(pb.to_str().unwrap()));
|
||||
}
|
||||
if check_file_exists("config.json") {
|
||||
return Some(String::from("config.json"));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_usage(prog: &str, opts: &Options) {
|
||||
let brief = format!(
|
||||
"{}
|
||||
{} download [options] <url> [<url>] {}
|
||||
{} config fix [options] {}
|
||||
{} config help [options] {}",
|
||||
gettext("Usage:"),
|
||||
prog,
|
||||
gettext("Download an artwork"),
|
||||
prog,
|
||||
gettext("Fix the config file"),
|
||||
prog,
|
||||
gettext("Print all available settings"),
|
||||
);
|
||||
println!("{}", opts.usage(brief.as_str()));
|
||||
}
|
||||
|
||||
pub fn parse_cmd() -> Option<CommandOpts> {
|
||||
let argv: Vec<String> = env::args().collect();
|
||||
let mut opts = Options::new();
|
||||
opts.optflag("h", "help", gettext("Print help message."));
|
||||
opts.optopt(
|
||||
"c",
|
||||
"config",
|
||||
gettext("The location of config file."),
|
||||
"FILE",
|
||||
);
|
||||
opts.optopt(
|
||||
"C",
|
||||
"cookies",
|
||||
gettext("The location of cookies file. Used for web API."),
|
||||
"FILE",
|
||||
);
|
||||
opts.optopt(
|
||||
"l",
|
||||
"language",
|
||||
gettext("The language of translated tags."),
|
||||
"LANG",
|
||||
);
|
||||
let result = match opts.parse(&argv[1..]) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
panic!("{}", err.to_string())
|
||||
}
|
||||
};
|
||||
if result.opt_present("h") || result.free.len() == 0 {
|
||||
print_usage(&argv[0], &opts);
|
||||
return None;
|
||||
}
|
||||
let cmd = &result.free[0];
|
||||
let mut re = if cmd == "download" {
|
||||
Some(CommandOpts::new(Command::Download))
|
||||
} else if cmd == "config" {
|
||||
Some(CommandOpts::new(Command::Config))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if re.is_none() {
|
||||
println!("{}", gettext("Unknown command."));
|
||||
print_usage(&argv[0], &opts);
|
||||
return None;
|
||||
}
|
||||
match re.as_ref().unwrap().cmd {
|
||||
Command::Download => {
|
||||
let mut urls = Vec::new();
|
||||
for url in result.free.iter().skip(1) {
|
||||
urls.push(url.to_string());
|
||||
}
|
||||
if urls.is_empty() {
|
||||
println!("{}", gettext("No URL specified."));
|
||||
print_usage(&argv[0], &opts);
|
||||
return None;
|
||||
}
|
||||
re.as_mut().unwrap().urls = urls;
|
||||
}
|
||||
Command::Config => {
|
||||
if result.free.len() < 2 {
|
||||
println!("{}", gettext("No detailed command specified."));
|
||||
print_usage(&argv[0], &opts);
|
||||
return None;
|
||||
}
|
||||
let subcmd = &result.free[1];
|
||||
re.as_mut().unwrap().config_cmd = if subcmd == "fix" {
|
||||
Some(ConfigCommand::Fix)
|
||||
} else if subcmd == "help" {
|
||||
Some(ConfigCommand::Help)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if re.as_ref().unwrap().config_cmd.is_none() {
|
||||
println!("{}", gettext("Unknown config subcommand."));
|
||||
print_usage(&argv[0], &opts);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.opt_present("config") {
|
||||
re.as_mut().unwrap()._config = Some(result.opt_str("config").unwrap());
|
||||
}
|
||||
if result.opt_present("cookies") {
|
||||
re.as_mut().unwrap().cookies = Some(result.opt_str("cookies").unwrap());
|
||||
}
|
||||
if result.opt_present("language") {
|
||||
re.as_mut().unwrap().language = Some(result.opt_str("language").unwrap());
|
||||
}
|
||||
re
|
||||
}
|
||||
87
src/parser/mainpage.rs
Normal file
87
src/parser/mainpage.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use crate::gettext;
|
||||
use html_parser::Dom;
|
||||
use html_parser::Node;
|
||||
use json::JsonValue;
|
||||
|
||||
pub struct MainPageParser {
|
||||
pub value: Option<JsonValue>,
|
||||
}
|
||||
|
||||
impl MainPageParser {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&mut self, node: &Node) -> bool {
|
||||
match node {
|
||||
Node::Element(e) => {
|
||||
if e.name == "meta" {
|
||||
let name = e.attributes.get("name");
|
||||
if name.is_none() {
|
||||
return false;
|
||||
}
|
||||
let name = name.unwrap();
|
||||
if name.is_none() {
|
||||
return false;
|
||||
}
|
||||
if name.as_ref().unwrap() != "global-data" {
|
||||
return false;
|
||||
}
|
||||
if e.id.is_none() {
|
||||
return false;
|
||||
}
|
||||
if e.id.as_ref().unwrap() != "meta-global-data" {
|
||||
return false;
|
||||
}
|
||||
let c = e.attributes.get("content");
|
||||
if c.is_none() {
|
||||
return false;
|
||||
}
|
||||
let c = c.unwrap();
|
||||
if c.is_none() {
|
||||
return false;
|
||||
}
|
||||
let r = json::parse(c.as_ref().unwrap());
|
||||
if r.is_err() {
|
||||
println!("{} {}", gettext("Failed to parse JSON:"), r.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
self.value = Some(r.unwrap());
|
||||
true
|
||||
} else {
|
||||
for c in e.children.iter() {
|
||||
if self.iter(c) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
Node::Comment(_) => { false }
|
||||
Node::Text(_) => { false }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&mut self, context: &str) -> bool {
|
||||
let r = Dom::parse(context);
|
||||
if r.is_err() {
|
||||
println!("{} {}", gettext("Failed to parse HTML:"), r.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
let dom = r.unwrap();
|
||||
if dom.errors.len() > 0 {
|
||||
println!("{}", gettext("Some errors occured during parsing:"));
|
||||
for i in dom.errors.iter() {
|
||||
println!("{}", i);
|
||||
}
|
||||
}
|
||||
for n in dom.children.iter() {
|
||||
if self.iter(n) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
3
src/parser/mod.rs
Normal file
3
src/parser/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
extern crate html_parser;
|
||||
|
||||
pub mod mainpage;
|
||||
91
src/pixiv.rs
Normal file
91
src/pixiv.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::gettext;
|
||||
use crate::opthelper::OptHelper;
|
||||
use crate::parser::mainpage::MainPageParser;
|
||||
use crate::webclient::WebClient;
|
||||
use crate::Main;
|
||||
use spin_on::spin_on;
|
||||
|
||||
/// A client which use Pixiv's web API
|
||||
pub struct PixivWebClient<'a> {
|
||||
client: WebClient,
|
||||
helper: OptHelper<'a>,
|
||||
/// true if in is initialized
|
||||
inited: bool,
|
||||
}
|
||||
|
||||
impl<'a> PixivWebClient<'a> {
|
||||
pub fn new(m: &'a Main) -> Self {
|
||||
Self {
|
||||
client: WebClient::new(),
|
||||
helper: OptHelper::new(m.cmd.as_ref().unwrap(), m.settings.as_ref().unwrap()),
|
||||
inited: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self) -> bool {
|
||||
let c = self.helper.cookies();
|
||||
if c.is_some() {
|
||||
if !self.client.read_cookies(c.as_ref().unwrap()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.client.set_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36");
|
||||
let l = self.helper.language();
|
||||
if l.is_some() {
|
||||
self.client.set_header("Accept-Language", l.as_ref().unwrap());
|
||||
} else {
|
||||
self.client.set_header("Accept-Language", "ja");
|
||||
}
|
||||
self.inited = true;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn auto_init(&mut self) {
|
||||
if !self.inited {
|
||||
let r = self.init();
|
||||
if !r {
|
||||
panic!("{}", gettext("Failed to initialize pixiv web api client."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_login(&mut self) -> bool {
|
||||
self.auto_init();
|
||||
let r = self.client.get("https://www.pixiv.net/");
|
||||
if r.is_none() {
|
||||
return false;
|
||||
}
|
||||
let r = r.unwrap();
|
||||
let status = r.status();
|
||||
let code = status.as_u16();
|
||||
if code >= 400 {
|
||||
println!("{} {}", gettext("Failed to get main page:"), status);
|
||||
return false;
|
||||
}
|
||||
let data = spin_on(r.text_with_charset("UTF-8"));
|
||||
if data.is_err() {
|
||||
println!("{} {}", gettext("Failed to get main page:"), data.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
let data = data.unwrap();
|
||||
let mut p = MainPageParser::new();
|
||||
if !p.parse(data.as_str()) {
|
||||
println!("{}", gettext("Failed to parse main page."));
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl <'a> Drop for PixivWebClient<'a> {
|
||||
fn drop(&mut self) {
|
||||
if self.inited {
|
||||
let c = self.helper.cookies();
|
||||
if c.is_some() {
|
||||
if !self.client.save_cookies(c.as_ref().unwrap()) {
|
||||
println!("{} {}", gettext("Warning: Failed to save cookies file:"), c.as_ref().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
421
src/settings.rs
Normal file
421
src/settings.rs
Normal file
@@ -0,0 +1,421 @@
|
||||
use crate::gettext;
|
||||
use crate::settings_list::get_settings_list;
|
||||
use json::JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::fs::{remove_file, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
/// Json value type
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum JsonValueType {
|
||||
Str,
|
||||
Number,
|
||||
Boolean,
|
||||
Object,
|
||||
Array,
|
||||
Multiple,
|
||||
}
|
||||
|
||||
impl JsonValueType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
JsonValueType::Str => "String",
|
||||
JsonValueType::Number => "Number",
|
||||
JsonValueType::Boolean => "Boolean",
|
||||
JsonValueType::Object => "Object",
|
||||
JsonValueType::Array => "Array",
|
||||
JsonValueType::Multiple => gettext("Multiple type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for JsonValueType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for JsonValueType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// An callback to check if a json value is valid
|
||||
pub type SettingDesCallback = fn(obj: &JsonValue) -> bool;
|
||||
|
||||
/// An object to describe a setting
|
||||
#[derive(Clone)]
|
||||
pub struct SettingDes {
|
||||
/// The name of the setting
|
||||
_name: String,
|
||||
/// The description of the setting
|
||||
_description: String,
|
||||
/// The type of the setting
|
||||
_type: JsonValueType,
|
||||
/// The callback function of the setting
|
||||
_fun: Option<SettingDesCallback>,
|
||||
}
|
||||
|
||||
impl SettingDes {
|
||||
/// Create a new setting description
|
||||
pub fn new(
|
||||
name: &str,
|
||||
description: &str,
|
||||
typ: JsonValueType,
|
||||
callback: Option<SettingDesCallback>,
|
||||
) -> Option<SettingDes> {
|
||||
if (typ == JsonValueType::Array
|
||||
|| typ == JsonValueType::Object
|
||||
|| typ == JsonValueType::Multiple)
|
||||
&& callback.is_none()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(SettingDes {
|
||||
_name: String::from(name),
|
||||
_description: String::from(description),
|
||||
_type: typ.clone(),
|
||||
_fun: callback,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
self._name.as_str()
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
self._description.as_str()
|
||||
}
|
||||
|
||||
pub fn type_name(&self) -> &'static str {
|
||||
self._type.to_str()
|
||||
}
|
||||
/// Check if a value is valid
|
||||
pub fn is_vaild_value(&self, value: &JsonValue) -> bool {
|
||||
if self._type == JsonValueType::Array {
|
||||
if value.is_array() {
|
||||
return self._fun.unwrap()(&value);
|
||||
}
|
||||
} else if self._type == JsonValueType::Boolean {
|
||||
if value.is_boolean() {
|
||||
return true;
|
||||
}
|
||||
} else if self._type == JsonValueType::Multiple {
|
||||
return self._fun.unwrap()(&value);
|
||||
} else if self._type == JsonValueType::Number {
|
||||
if value.is_number() {
|
||||
match self._fun {
|
||||
Some(fun) => {
|
||||
return fun(&value);
|
||||
}
|
||||
None => {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self._type == JsonValueType::Object {
|
||||
if value.is_object() {
|
||||
return self._fun.unwrap()(&value);
|
||||
}
|
||||
} else if self._type == JsonValueType::Str {
|
||||
if value.is_string() {
|
||||
match self._fun {
|
||||
Some(fun) => {
|
||||
return fun(&value);
|
||||
}
|
||||
None => {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SettingDes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"SettingDes {{ name: {}, description: {}, type: {} }}",
|
||||
self._name, self._description, self._type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a list of settings
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SettingDesStore {
|
||||
list: Vec<SettingDes>,
|
||||
}
|
||||
|
||||
impl SettingDesStore {
|
||||
pub fn new(list: Vec<SettingDes>) -> SettingDesStore {
|
||||
SettingDesStore { list }
|
||||
}
|
||||
|
||||
pub fn check_valid(&self, key: &str, value: &JsonValue) -> Option<bool> {
|
||||
for i in self.list.iter() {
|
||||
if i.name() == key {
|
||||
return Some(i.is_vaild_value(value));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.list.len()
|
||||
}
|
||||
|
||||
pub fn print_help(&self) {
|
||||
let mut s = String::from("");
|
||||
for i in self.list.iter() {
|
||||
let mut t = format!("{}: {}", i.name(), i.type_name());
|
||||
if t.len() >= 20 {
|
||||
t += "\t";
|
||||
} else {
|
||||
t += " ".repeat(20 - t.len()).as_str();
|
||||
}
|
||||
t += i.description();
|
||||
if s.len() > 0 {
|
||||
s += "\n";
|
||||
}
|
||||
s += t.as_str();
|
||||
}
|
||||
println!("{}", s);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SettingDesStore {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
list: get_settings_list(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An settings option
|
||||
#[derive(Clone)]
|
||||
pub struct SettingOpt {
|
||||
_name: String,
|
||||
_value: JsonValue,
|
||||
}
|
||||
|
||||
impl SettingOpt {
|
||||
pub fn new(name: String, value: JsonValue) -> SettingOpt {
|
||||
SettingOpt {
|
||||
_name: name.clone(),
|
||||
_value: value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self._name.clone()
|
||||
}
|
||||
|
||||
pub fn value(&self) -> JsonValue {
|
||||
self._value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SettingOpt {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
Display::fmt(&self._value, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SettingJar {
|
||||
pub settings: HashMap<String, SettingOpt>,
|
||||
}
|
||||
|
||||
impl SettingJar {
|
||||
pub fn new() -> SettingJar {
|
||||
SettingJar {
|
||||
settings: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, key: &str, opt: JsonValue) {
|
||||
self.settings
|
||||
.insert(String::from(key), SettingOpt::new(String::from(key), opt));
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.settings.clear();
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<JsonValue> {
|
||||
if self.settings.contains_key(key) {
|
||||
return Some(self.settings.get(key).unwrap().value());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Option<JsonValue> {
|
||||
let mut v = JsonValue::new_object();
|
||||
for (_, val) in self.settings.iter() {
|
||||
match v.insert(val.name().as_str(), val.value()) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
println!("{}", gettext("Can not insert setting to JSON object."));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SettingStore {
|
||||
pub basic: SettingDesStore,
|
||||
pub data: SettingJar,
|
||||
}
|
||||
|
||||
impl SettingStore {
|
||||
pub fn new(list: Vec<SettingDes>) -> Self {
|
||||
Self {
|
||||
basic: SettingDesStore::new(list),
|
||||
data: SettingJar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_str(&self, key: &str) -> Option<String> {
|
||||
let obj = self.data.get(key);
|
||||
if obj.is_none() {
|
||||
return None;
|
||||
}
|
||||
let obj = obj.unwrap();
|
||||
if !obj.is_string() {
|
||||
None
|
||||
} else {
|
||||
Some(String::from(obj.as_str().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn have_str(&self, key: &str) -> bool {
|
||||
let obj = self.data.get(key);
|
||||
if obj.is_none() {
|
||||
return false;
|
||||
}
|
||||
let obj = obj.unwrap();
|
||||
obj.is_string()
|
||||
}
|
||||
|
||||
pub fn read(&mut self, file_name: &str, fix_invalid: bool) -> bool {
|
||||
self.data.clear();
|
||||
let path = Path::new(file_name);
|
||||
if !path.exists() {
|
||||
return false;
|
||||
}
|
||||
let re = File::open(path);
|
||||
if re.is_err() {
|
||||
println!("{}", re.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
let mut f = re.unwrap();
|
||||
let mut s = String::from("");
|
||||
let r = f.read_to_string(&mut s);
|
||||
match r {
|
||||
Ok(le) => {
|
||||
if le == 0 {
|
||||
if !fix_invalid {
|
||||
println!("{}", gettext("Settings file is empty."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", gettext("Can not read from settings file."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let re = json::parse(s.as_str());
|
||||
match re {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
if !fix_invalid {
|
||||
println!("{}", gettext("Can not parse settings file."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let obj = re.unwrap();
|
||||
if !obj.is_object() {
|
||||
if !fix_invalid {
|
||||
println!("{}", gettext("Unknown settings file."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (key, o) in obj.entries() {
|
||||
let re = self.basic.check_valid(key, o);
|
||||
match re {
|
||||
Some(re) => {
|
||||
if !re {
|
||||
if !fix_invalid {
|
||||
let s = gettext("\"<key>\" is invalid, you can use \"pixiv_downloader config fix\" to remove all invalid value.").replace("<key>", key);
|
||||
println!("{}", s.as_str());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
self.data.add(key, o.clone());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.data.add(key, o.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn save(&self, file_name: &str) -> bool {
|
||||
let obj = self.data.to_json();
|
||||
if obj.is_none() {
|
||||
println!("{}", gettext("Can not convert settings to JSON object."));
|
||||
return false;
|
||||
}
|
||||
let obj = obj.unwrap();
|
||||
let s = json::stringify(obj);
|
||||
let path = Path::new(file_name);
|
||||
if path.exists() {
|
||||
match remove_file(path) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
println!("{} {}", gettext("Failed to remove file:"), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
let r = File::create(path);
|
||||
if r.is_err() {
|
||||
println!("{} {}", gettext("Failed to create file:"), r.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
let mut f = r.unwrap();
|
||||
let r = f.write(s.as_bytes());
|
||||
if r.is_err() {
|
||||
println!("{} {}", gettext("Failed to write file:"), r.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
let r = f.flush();
|
||||
if r.is_err() {
|
||||
println!("{} {}", gettext("Failed to flush file:"), r.unwrap_err());
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SettingStore {
|
||||
fn default() -> Self {
|
||||
Self::new(get_settings_list())
|
||||
}
|
||||
}
|
||||
11
src/settings_list.rs
Normal file
11
src/settings_list.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use crate::gettext;
|
||||
use crate::settings::SettingDes;
|
||||
use crate::settings::JsonValueType;
|
||||
|
||||
pub fn get_settings_list() -> Vec<SettingDes> {
|
||||
vec![
|
||||
SettingDes::new("refresh_tokens", gettext("Pixiv's refresh tokens. Used to login."), JsonValueType::Str, None).unwrap(),
|
||||
SettingDes::new("cookies", gettext("The location of cookies file. Used for web API."), JsonValueType::Str, None).unwrap(),
|
||||
SettingDes::new("language", gettext("The language of translated tags."), JsonValueType::Str, None).unwrap(),
|
||||
]
|
||||
}
|
||||
24
src/utils.rs
Normal file
24
src/utils.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Get executable location, if not found, return current directory (./)
|
||||
pub fn get_exe_path_else_current() -> PathBuf {
|
||||
let re = env::current_exe();
|
||||
match re {
|
||||
Ok(pa) => {
|
||||
let mut p = pa.clone();
|
||||
p.pop();
|
||||
p
|
||||
}
|
||||
Err(_) => {
|
||||
let p = Path::new("./");
|
||||
p.to_path_buf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_file_exists(path: &str) -> bool {
|
||||
let p = Path::new(path);
|
||||
p.exists()
|
||||
}
|
||||
115
src/webclient.rs
Normal file
115
src/webclient.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
extern crate spin_on;
|
||||
|
||||
use crate::cookies::Cookie;
|
||||
use crate::cookies::CookieJar;
|
||||
use crate::gettext;
|
||||
use reqwest::{Client, IntoUrl, RequestBuilder, Response};
|
||||
use std::collections::HashMap;
|
||||
use spin_on::spin_on;
|
||||
|
||||
/// Generate `cookie` header for a url
|
||||
/// * `c` - Cookies
|
||||
/// * `url` - URL
|
||||
pub fn gen_cookie_header<U: IntoUrl>(c: &mut CookieJar, url: U) -> String {
|
||||
c.check_expired();
|
||||
let mut s = String::from("");
|
||||
let u = url.as_str();
|
||||
for a in c.iter() {
|
||||
if a.matched(u) {
|
||||
if s.len() > 0 {
|
||||
s += " ";
|
||||
}
|
||||
s += a.get_name_value().as_str();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// A Web Client
|
||||
pub struct WebClient {
|
||||
client: Client,
|
||||
/// HTTP Headers
|
||||
pub headers: HashMap<String, String>,
|
||||
cookies: CookieJar,
|
||||
}
|
||||
|
||||
impl WebClient {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
headers: HashMap::new(),
|
||||
cookies: CookieJar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_set_cookie(&mut self, r: &Response) {
|
||||
let u = r.url();
|
||||
let h = r.headers();
|
||||
let v = h.get_all("Set-Cookie");
|
||||
for val in v {
|
||||
let val = val.to_str();
|
||||
match val {
|
||||
Ok(val) => {
|
||||
let c = Cookie::from_set_cookie(u.as_str(), val);
|
||||
match c {
|
||||
Some(c) => {
|
||||
self.cookies.add(c);
|
||||
}
|
||||
None => {
|
||||
println!("{}", gettext("Failed to parse Set-Cookie header."));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} {}", gettext("Failed to convert to string:"), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_cookies(&mut self, file_name: &str) -> bool {
|
||||
let r = self.cookies.read(file_name);
|
||||
if !r {
|
||||
self.cookies = CookieJar::new();
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
pub fn save_cookies(&mut self, file_name: &str) -> bool {
|
||||
self.cookies.save(file_name)
|
||||
}
|
||||
|
||||
pub fn set_header(&mut self, key: &str, value: &str) -> Option<String> {
|
||||
self.headers.insert(String::from(key), String::from(value))
|
||||
}
|
||||
|
||||
/// Send GET requests
|
||||
pub fn get<U: IntoUrl>(&mut self, url: U) -> Option<Response> {
|
||||
let r = self.aget(url);
|
||||
let r = r.send();
|
||||
let r = spin_on(r);
|
||||
match r {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
println!("{} {}", gettext("Error when request:"), e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let r = r.unwrap();
|
||||
self.handle_set_cookie(&r);
|
||||
Some(r)
|
||||
}
|
||||
|
||||
pub fn aget<U: IntoUrl>(&mut self, url: U) -> RequestBuilder {
|
||||
let s = url.as_str();
|
||||
let mut r = self.client.get(s);
|
||||
for (k, v) in self.headers.iter() {
|
||||
r = r.header(k, v);
|
||||
}
|
||||
let c = gen_cookie_header(&mut self.cookies, s);
|
||||
if c.len() > 0 {
|
||||
r = r.header("Cookie", c.as_str());
|
||||
}
|
||||
r
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user