Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb6a71f6b6 | ||
|
|
b562f76c69 | ||
|
|
8aaf95fd36 | ||
|
|
983d72d65a | ||
|
|
86378b9fba | ||
|
|
6b18e7fba1 | ||
|
|
ec11132240 | ||
|
|
dc877bd634 | ||
|
|
00b4bf4068 | ||
|
|
51c43564b6 | ||
|
|
820a2671cf | ||
|
|
3b4443110e | ||
|
|
ffd5b29030 | ||
|
|
779f56dd91 | ||
|
|
2beecdade2 | ||
|
|
bca2e110de | ||
|
|
8d55a86c06 | ||
|
|
f61199bed6 | ||
|
|
2d75bbb5c3 | ||
|
|
55d3f06cb6 | ||
|
|
1809847b8a | ||
|
|
da8e8d9641 | ||
|
|
ead622d95e | ||
|
|
c47fcba36b | ||
|
|
54c0da081c | ||
|
|
4bead0752d | ||
|
|
bd7828b73f | ||
|
|
4706540475 | ||
|
|
0daec66fa6 | ||
|
|
379ae22db7 | ||
|
|
2d1fc4273a | ||
|
|
55b67a0efa | ||
|
|
ace4625a27 | ||
|
|
6fb0474154 | ||
|
|
86ef0d3331 | ||
|
|
cfa9d4929f | ||
|
|
3c40ada451 | ||
|
|
9ec4eca558 | ||
|
|
f295fb6b64 | ||
|
|
3dc4322258 | ||
|
|
3910d0ffb0 | ||
|
|
af75f1ce4f | ||
|
|
a016465bea | ||
|
|
fd5e289c87 | ||
|
|
2a2ca7eb4e | ||
|
|
a7ec61b8e2 | ||
|
|
84c882d573 | ||
|
|
567eb2903d | ||
|
|
31073eb57e | ||
|
|
5613e21138 | ||
|
|
0a516b2f3c | ||
|
|
d3d8983307 | ||
|
|
59e2b1b267 | ||
|
|
4ebbc2347f | ||
|
|
25ab8edd20 | ||
|
|
f4bc3c7b53 | ||
|
|
ec89dc362f | ||
|
|
821d9949ab | ||
|
|
e97120cf06 | ||
|
|
67f11d2b93 | ||
|
|
3be6b8cd91 | ||
|
|
db3ff8e0d2 | ||
|
|
405d0bbdb2 | ||
|
|
6c21e37ce4 | ||
|
|
b74e2a2bf6 | ||
|
|
deb3e83082 | ||
|
|
ff90c4ff03 | ||
|
|
7f3d63532c | ||
|
|
bd2fa75cde | ||
|
|
27f9fb0def | ||
|
|
7a07456b2f | ||
|
|
3bce1f0332 | ||
|
|
5a92d7beb7 | ||
|
|
f28cb69826 | ||
|
|
14d18ad310 | ||
|
|
ee6698a8e8 | ||
|
|
7f2b99b51f | ||
|
|
da9039ea9c | ||
|
|
dc46668f0d | ||
|
|
142a2a84b4 | ||
|
|
84b6a142fc | ||
|
|
a853f1d991 | ||
|
|
dc261bb6ce | ||
|
|
d0ea8e4fab | ||
|
|
c1cce920a4 | ||
|
|
f0495c6858 | ||
|
|
a37bde428d | ||
|
|
7793f83ea3 | ||
|
|
29cc879b85 | ||
|
|
abfdb76589 | ||
|
|
04e4f0e9cc | ||
|
|
625cfbc474 | ||
|
|
d8857bd4a2 | ||
|
|
a247c708f6 | ||
|
|
85781a150a | ||
|
|
3f07bae796 | ||
|
|
6086bd7086 | ||
|
|
62bf0e2308 | ||
|
|
0bb1a21a76 | ||
|
|
575e2d8904 | ||
|
|
db21134550 | ||
|
|
3407490f82 | ||
|
|
dad06f6cd6 | ||
|
|
de70fc66b4 | ||
|
|
2282f72b2f | ||
|
|
e00a7c9044 | ||
|
|
e79a7ba2fe | ||
|
|
791e50411d | ||
|
|
df8eb0e4cd | ||
|
|
3e291171c2 | ||
|
|
bd8f8e80c9 | ||
|
|
8ed7e144e1 | ||
|
|
69f8224453 | ||
|
|
3c0e51a9e2 | ||
|
|
0a6679b983 | ||
|
|
6d1024806c | ||
|
|
388e3f46a4 | ||
|
|
f7c8e9f7db | ||
|
|
7ca484f45d | ||
|
|
cecb13fa90 | ||
|
|
331478d370 | ||
|
|
2481407093 | ||
|
|
e024a812d0 | ||
|
|
f5b5767997 | ||
|
|
5e7022ec81 | ||
|
|
e54177e8bf | ||
|
|
69cc5383d1 | ||
|
|
11f88e86e3 | ||
|
|
9e87e64223 | ||
|
|
9ffef9db8e | ||
|
|
6d2705112d | ||
|
|
f92d898136 | ||
|
|
d6b4c37138 | ||
|
|
b87ff38353 | ||
|
|
d9259bb259 | ||
|
|
8756e5357a | ||
|
|
4c09b8f3be | ||
|
|
6f3e361ee6 | ||
|
|
93d9dbe32b | ||
|
|
fd89b7125c | ||
|
|
bb0e97c402 | ||
|
|
edd097855b | ||
|
|
4d35c44247 | ||
|
|
d7e34d9c21 | ||
|
|
fc06c29759 | ||
|
|
c65702e215 | ||
|
|
96300164da | ||
|
|
1b35475302 | ||
|
|
240bae9be9 | ||
|
|
46b1592a7f | ||
|
|
1cae714fd2 | ||
|
|
5e63624955 | ||
|
|
7584156262 | ||
|
|
d23c09748b | ||
|
|
936adca3f2 | ||
|
|
37424da698 | ||
|
|
7fd4877087 | ||
|
|
619ac47962 | ||
|
|
6b79585564 | ||
|
|
c1f05876e3 | ||
|
|
2981518cf4 | ||
|
|
511111f874 | ||
|
|
ca718deaad | ||
|
|
7a01544323 | ||
|
|
3082faaadc |
@@ -1 +0,0 @@
|
|||||||
VITE_APP_API=""
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
VITE_APP_API=""
|
|
||||||
42
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: "Bug Report \\ 问题反馈"
|
||||||
|
description: "Create a report to help us improve \\ 帮助改进"
|
||||||
|
labels: ["Bug"]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: title
|
||||||
|
attributes:
|
||||||
|
label: "Title \\ 标题"
|
||||||
|
description: "A brief summary of the bug. \\ 对于该错误的简要总结。"
|
||||||
|
placeholder: "Enter the bug title here. \\ 在此输入错误标题。"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: "To Reproduce \\ 操作流程"
|
||||||
|
description: "Steps to reproduce the behaviour. \\ 重现该行为的步骤。"
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...' \ 进入 '...'
|
||||||
|
2. Click on '....' \ 点击 '....'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behaviour
|
||||||
|
attributes:
|
||||||
|
label: "Expected Behaviour \\ 预期结果"
|
||||||
|
description: "A clear and concise description of what you expected to happen. \\ 对您期望发生的事情的清晰简明描述。"
|
||||||
|
placeholder: "A clear and concise description of what you expected to happen. \\ 对您期望发生的事情的清晰简明描述。"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: software-version
|
||||||
|
attributes:
|
||||||
|
label: "Software Version \\ 软件版本"
|
||||||
|
description: "Please specify the version of the software you are using. \\ 请指定您使用的软件版本。"
|
||||||
|
placeholder: "Enter the software version here. \\ 在此输入软件版本。"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: "Feature Request \\ 功能建议"
|
||||||
|
description: "Suggest an idea for this project \\ 为这个项目提出一个新想法"
|
||||||
|
labels: ["Enhancement"]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: title
|
||||||
|
attributes:
|
||||||
|
label: "Title \\ 标题"
|
||||||
|
description: "A brief summary of your feature request. \\ 对您功能建议的简要总结。"
|
||||||
|
placeholder: "Enter the feature title here. \\ 在此输入功能标题。 "
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-suggestion
|
||||||
|
attributes:
|
||||||
|
label: "Feature Suggestion \\ 功能建议"
|
||||||
|
description: "A clear and concise description of the feature you would like to suggest. \\ 对您想要建议的功能的清晰简明描述。"
|
||||||
|
placeholder: "Describe your feature suggestion here. \\ 在此描述您的功能建议。"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposed-solution
|
||||||
|
attributes:
|
||||||
|
label: "Proposed Solution \\ 你的方案"
|
||||||
|
description: "A clear and concise description of your proposed solution. \\ 对您提议的解决方案的清晰简明描述。"
|
||||||
|
placeholder: "Describe your proposed solution here. \\ 在此描述您的提议方案。"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
34
.gitignore
vendored
@@ -1,31 +1,5 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
temp
|
|
||||||
dist-ssr
|
|
||||||
dist-electron
|
|
||||||
release
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/.debug.env
|
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
build/bin
|
||||||
*.suo
|
node_modules
|
||||||
*.ntvs*
|
frontend/dist
|
||||||
*.njsproj
|
.DS_Store
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
.env
|
|
||||||
|
|
||||||
# lockfile
|
|
||||||
package-lock.json
|
|
||||||
pnpm-lock.yaml
|
|
||||||
214
LICENSE
@@ -1,21 +1,201 @@
|
|||||||
MIT License
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
Copyright (c) 2023 草鞋没号
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
1. Definitions.
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
copies or substantial portions of the Software.
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
the copyright owner that is granting the License.
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
other entities that control, are controlled by, or are under common
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
control with that entity. For the purposes of this definition,
|
||||||
SOFTWARE.
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
104
README-EN.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://github.com/putyy/res-downloader"><img src="build/appicon.png" width="120"/></a>
|
||||||
|
<h1>res-downloader</h1>
|
||||||
|
<h4>📖 English | <a href="https://github.com/putyy/res-downloader/blob/master/README.md">中文</a></h4>
|
||||||
|
|
||||||
|
[](https://github.com/putyy/res-downloader/stargazers)
|
||||||
|
[](https://github.com/putyy/res-downloader/fork)
|
||||||
|
[](https://github.com/putyy/res-downloader/releases)
|
||||||
|

|
||||||
|
[](https://github.com/putyy/res-downloader/blob/master/LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎉 Aixiang Resource Downloader
|
||||||
|
|
||||||
|
> A cross-platform resource downloader built with Go + [Wails](https://github.com/wailsapp/wails).
|
||||||
|
Clean UI, easy to use, and supports a wide range of resource sniffing and downloading.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🚀 **User-Friendly**: Simple operation with an intuitive and beautiful UI
|
||||||
|
- 🖥️ **Cross-Platform**: Available on Windows / macOS / Linux
|
||||||
|
- 🌐 **Supports Multiple Resource Types**: Video / Audio / Images / m3u8 / Live streams, and more
|
||||||
|
- 📱 **Wide Platform Compatibility**: Works with WeChat Channels, Mini Programs, Douyin, Kuaishou, Xiaohongshu, KuGou Music, QQ Music, and more
|
||||||
|
- 🌍 **Proxy Capture**: Built-in proxy allows fetching resources behind network restrictions
|
||||||
|
|
||||||
|
## 📚 Docs & Versions
|
||||||
|
|
||||||
|
- 📘 [Online Documentation (Chinese)](https://res.putyy.com/)
|
||||||
|
- 🧩 [Mini Version Ui Display using default browser](https://github.com/putyy/res-downloader) | [Old Electron Version Support Win7](https://github.com/putyy/res-downloader/tree/old)
|
||||||
|
- 💬 [Join the User Group (Chinese)](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
|
||||||
|
> *If full, you can add WeChat `AmorousWorld` with a note “github”*
|
||||||
|
|
||||||
|
## 🧩 Download Links
|
||||||
|
|
||||||
|
- 🆕 [Download from GitHub](https://github.com/putyy/res-downloader/releases)
|
||||||
|
- 🆕 [Download via Lanzou Cloud (Password: 9vs5)](https://wwjv.lanzoum.com/b04wgtfyb)
|
||||||
|
- ⚠️ *Windows 7 users: Please use version `2.3.0`*
|
||||||
|
|
||||||
|
|
||||||
|
## 🖼️ Preview
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🚀 How to Use
|
||||||
|
|
||||||
|
> Follow these steps to use the software correctly:
|
||||||
|
|
||||||
|
1. During installation, be sure to **allow certificate installation** and **grant network access**
|
||||||
|
2. Launch the software → Click **"Start Proxy"** at the top left
|
||||||
|
3. Choose the resource types to capture (default is all)
|
||||||
|
4. Open the target content externally (WeChat, Mini App, Browser, etc.)
|
||||||
|
5. Return to the homepage to view the captured resource list
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ FAQ
|
||||||
|
|
||||||
|
### 📺 m3u8 Video Resources
|
||||||
|
|
||||||
|
- Online Preview: [m3u8play](https://m3u8play.com/)
|
||||||
|
- Download Tool: [m3u8-down](https://m3u8-down.gowas.cn/)
|
||||||
|
|
||||||
|
### 📡 Live Stream Resources
|
||||||
|
|
||||||
|
- We recommend [OBS](https://obsproject.com/) for recording (search for setup tutorials)
|
||||||
|
|
||||||
|
### 🐢 Slow Downloads or Large File Failures?
|
||||||
|
|
||||||
|
- Recommended download managers:
|
||||||
|
- [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)
|
||||||
|
- [Motrix](https://motrix.app/download)
|
||||||
|
- For WeChat videos, click `Decrypt Video` after download
|
||||||
|
|
||||||
|
### 🧩 Unable to Intercept Resources?
|
||||||
|
|
||||||
|
- Check your system proxy settings:
|
||||||
|
Address: 127.0.0.1
|
||||||
|
Port: 8899
|
||||||
|
|
||||||
|
### 🌐 Can't Access Internet After Closing the App?
|
||||||
|
|
||||||
|
- Manually disable the system proxy settings
|
||||||
|
|
||||||
|
### 🧠 More Questions?
|
||||||
|
|
||||||
|
- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
|
||||||
|
- [Aixiang Forum Thread (Chinese)](https://s.gowas.cn/d/4089)
|
||||||
|
|
||||||
|
## 💡 Principles & Motivation
|
||||||
|
|
||||||
|
This tool captures traffic via a local proxy and filters useful resources.
|
||||||
|
Its working principle is similar to tools like Fiddler, Charles, or browser DevTools, but with a more user-friendly display and enhanced filtering, making it suitable for everyday users with minimal tech background.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
> This software is for educational and research purposes only.
|
||||||
|
Commercial or illegal use is strictly prohibited.
|
||||||
|
The author is not responsible for any consequences arising from misuse.
|
||||||
121
README.md
@@ -1,51 +1,100 @@
|
|||||||
# res-downloader
|
<div align="center">
|
||||||
|
|
||||||
🎯 基于 [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue.git)
|
<a href="https://github.com/putyy/res-downloader"><img src="build/appicon.png" width="120"/></a>
|
||||||
📦 操作简单、可获取不同类型的资源
|
<h1>res-downloader</h1>
|
||||||
💪 支持获取视频、音频、图片、m3u8
|
<h4>📖 中文 | <a href="https://github.com/putyy/res-downloader/blob/master/README-EN.md">English</a></h4>
|
||||||
🖥 支持获取视频号、抖音、快手、小红书、酷狗音乐、qq音乐等网络资源
|
|
||||||
|
|
||||||
## 软件下载
|
[](https://github.com/putyy/res-downloader/stargazers)
|
||||||
🆕 [github下载](https://github.com/putyy/res-downloader/releases)
|
[](https://github.com/putyy/res-downloader/fork)
|
||||||
🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
|
[](https://github.com/putyy/res-downloader/releases)
|
||||||
|

|
||||||
|
[](https://github.com/putyy/res-downloader/blob/master/LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎉 爱享素材下载器
|
||||||
|
|
||||||
|
> 一款基于 Go + [Wails](https://github.com/wailsapp/wails) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。
|
||||||
|
|
||||||
|
## ✨ 功能特色
|
||||||
|
|
||||||
|
- 🚀 **简单易用**:操作简单,界面清晰美观
|
||||||
|
- 🖥️ **多平台支持**:Windows / macOS / Linux
|
||||||
|
- 🌐 **多资源类型支持**:视频 / 音频 / 图片 / m3u8 / 直播流等
|
||||||
|
- 📱 **平台兼容广泛**:支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
|
||||||
|
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源
|
||||||
|
|
||||||
|
## 📚 文档 & 版本
|
||||||
|
|
||||||
|
- 📘 [在线文档](https://res.putyy.com/)
|
||||||
|
- 💬 [加入交流群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
|
||||||
|
- 🧩 [最新版](https://github.com/putyy/res-downloader/releases) | [Mini版 使用默认浏览器展示UI](https://github.com/putyy/resd-mini) | [Electron旧版 支持Win7](https://github.com/putyy/res-downloader/tree/old)
|
||||||
|
> *群满时可加微信 `AmorousWorld`,请备注“github”*
|
||||||
|
|
||||||
|
## 🧩 下载地址
|
||||||
|
|
||||||
|
- 🆕 [GitHub 下载](https://github.com/putyy/res-downloader/releases)
|
||||||
|
- 🆕 [蓝奏云下载(密码:9vs5)](https://wwjv.lanzoum.com/b04wgtfyb)
|
||||||
|
- ⚠️ *Win7 用户请下载 `2.3.0` 版本*
|
||||||
|
|
||||||
|
|
||||||
## 二次开发
|
## 🖼️ 预览
|
||||||
> ps: 打包慢的问题可以参考 https://www.putyy.com/articles/87
|
|
||||||
```sh
|
|
||||||
git clone https://github.com/putyy/res-downloader
|
|
||||||
|
|
||||||
cd res-downloader
|

|
||||||
|
---
|
||||||
|
|
||||||
yarn install
|
## 🚀 使用方法
|
||||||
|
|
||||||
yarn run dev
|
> 请按以下步骤操作以正确使用软件:
|
||||||
|
|
||||||
# 打包mac
|
1. 安装时务必 **允许安装证书文件** 并 **允许网络访问**
|
||||||
yarn run build --mac
|
2. 打开软件 → 首页左上角点击 **“启动代理”**
|
||||||
|
3. 选择要获取的资源类型(默认全部)
|
||||||
|
4. 在外部打开资源页面(如视频号、小程序、网页等)
|
||||||
|
5. 返回软件首页,即可看到资源列表
|
||||||
|
|
||||||
# 打包win
|
## ❓ 常见问题
|
||||||
yarn run build --win
|
|
||||||
```
|
|
||||||
|
|
||||||
## 软件截图
|
### 📺 m3u8 视频资源
|
||||||

|
|
||||||
|
|
||||||
## 使用方法
|
- 在线预览:[m3u8play](https://m3u8play.com/)
|
||||||
> 1. 打开本软件
|
- 视频下载:[m3u8-down](https://m3u8-down.gowas.cn/)
|
||||||
> 2. 软件首页选择要获取的资源类型(默认选中的视频)
|
|
||||||
> 3. 打开要捕获的源, 如:视频号、网页、小程序等等
|
|
||||||
> 4. 返回软件首页即可看到要下载的资源
|
|
||||||
|
|
||||||
## 常见问题
|
### 📡 直播流资源
|
||||||
> 1. 无法拦截获取
|
|
||||||
> > 手动检测系统代理是否设置正确 本软件代理地址: 127.0.0.1:8899
|
|
||||||
> 2. 关闭软件后无法正常上网
|
|
||||||
> > 手动关闭系统代理设置
|
|
||||||
|
|
||||||
## 实现原理
|
- 推荐使用 [OBS](https://obsproject.com/) 进行录制(教程请百度)
|
||||||
> 通过代理网络抓包拦截响应,筛选出有用的资源,同fiddler、charles等抓包软件、浏览器F12打开控制也能达到目的,只不过这些软件需要手动进行筛选,对于小白用户上手还是有点难度,所以就有了本项目这样的软件。
|
|
||||||
|
|
||||||
## 参考项目
|
### 🐢 下载慢、大文件失败?
|
||||||
|
|
||||||
- [WeChatVideoDownloader](https://github.com/lecepin/WeChatVideoDownloader) 原项目是react写的,本项目参考原项目用vue3重写了一下,核心逻辑没什么变化,主要是增加了一些新的功能,再次感谢!
|
- 推荐工具:
|
||||||
|
- [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)
|
||||||
|
- [Motrix](https://motrix.app/download)
|
||||||
|
- 视频号资源下载后可在操作项点击 `视频解密(视频号)`
|
||||||
|
|
||||||
|
### 🧩 软件无法拦截资源?
|
||||||
|
|
||||||
|
- 检查是否正确设置系统代理:
|
||||||
|
地址:127.0.0.1
|
||||||
|
端口:8899
|
||||||
|
|
||||||
|
### 🌐 关闭软件后无法上网?
|
||||||
|
|
||||||
|
- 手动关闭系统代理设置
|
||||||
|
|
||||||
|
### 🧠 更多问题
|
||||||
|
|
||||||
|
- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
|
||||||
|
- [爱享论坛讨论帖](https://s.gowas.cn/d/4089)
|
||||||
|
|
||||||
|
## 💡 实现原理 & 初衷
|
||||||
|
|
||||||
|
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 免责声明
|
||||||
|
|
||||||
|
> 本软件仅供学习与研究用途,禁止用于任何商业或违法用途。
|
||||||
|
如因此产生的任何法律责任,概与作者无关!
|
||||||
|
|||||||
9
auto-imports.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
// Generated by unplugin-auto-import
|
|
||||||
export {}
|
|
||||||
declare global {
|
|
||||||
|
|
||||||
}
|
|
||||||
95
build/README.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
## Mac
|
||||||
|
```bash
|
||||||
|
wails build -platform "darwin/universal"
|
||||||
|
create-dmg 'build/bin/res-downloader.app' --overwrite ./build/bin
|
||||||
|
mv -f "build/bin/res-downloader $(jq -r '.info.productVersion' wails.json).dmg" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_mac.dmg"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
```bash
|
||||||
|
wails build -f -nsis -platform "windows/amd64" -webview2 Embed -skipbindings && mv -f "build/bin/res-downloader-amd64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_amd64.exe"
|
||||||
|
wails build -f -nsis -platform "windows/arm64" -webview2 Embed -skipbindings && mv -f "build/bin/res-downloader-arm64-installer.exe" "build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_win_arm64.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
### docker方式
|
||||||
|
> x86_64
|
||||||
|
```bash
|
||||||
|
docker build --network host -f build/linux/dockerfile -t res-downloader-amd-linux .
|
||||||
|
docker run -it --name res-downloader-amd-build --network host --privileged -v ./:/www/res-downloader res-downloader-amd-linux /bin/bash
|
||||||
|
# 容器内
|
||||||
|
cd /www/res-downloader
|
||||||
|
wails build -platform "linux/amd64" -s -skipbindings
|
||||||
|
|
||||||
|
# 打包debian
|
||||||
|
cp build/bin/res-downloader build/linux/Debian/usr/local/bin/
|
||||||
|
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g" -e "s/{{Architecture}}/amd64/g")" > build/linux/Debian/DEBIAN/control
|
||||||
|
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64.deb
|
||||||
|
|
||||||
|
# 打包AppImage
|
||||||
|
cp build/bin/res-downloader build/linux/AppImage/usr/bin/
|
||||||
|
|
||||||
|
# 复制WebKit相关文件
|
||||||
|
pushd build/linux/AppImage
|
||||||
|
|
||||||
|
for f in WebKitNetworkProcess WebKitWebProcess libwebkit2gtkinjectedbundle.so; do
|
||||||
|
path=$(find /usr/lib* -name "$f" 2>/dev/null | head -n 1)
|
||||||
|
if [ -n "$path" ]; then
|
||||||
|
mkdir -p ./$(dirname "$path")
|
||||||
|
cp --parents "$path" .
|
||||||
|
else
|
||||||
|
echo "⚠️ $f not found, you may need to install libwebkit2gtk"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
popd
|
||||||
|
|
||||||
|
# 下载appimagetool
|
||||||
|
wget -O ./build/bin/appimagetool-x86_64.AppImage https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage
|
||||||
|
chmod +x ./build/bin/appimagetool-x86_64.AppImage
|
||||||
|
./build/bin/appimagetool-x86_64.AppImage build/linux/AppImage build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64.AppImage
|
||||||
|
|
||||||
|
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
> arm64
|
||||||
|
```bash
|
||||||
|
# arm
|
||||||
|
docker build --platform linux/arm64 --network host -f build/linux/dockerfile -t res-downloader-arm-linux .
|
||||||
|
docker run --platform linux/arm64 -it --name res-downloader-arm-build --network host --privileged -v ./:/www/res-downloader res-downloader-arm-linux /bin/bash
|
||||||
|
# 容器内
|
||||||
|
cd /www/res-downloader
|
||||||
|
wails build -platform "linux/arm64" -s -skipbindings
|
||||||
|
|
||||||
|
# 打包debian
|
||||||
|
cp build/bin/res-downloader build/linux/Debian/usr/local/bin/
|
||||||
|
echo "$(cat build/linux/Debian/DEBIAN/.control | sed -e "s/{{Version}}/$(jq -r '.info.productVersion' wails.json)/g" -e "s/{{Architecture}}/arm64/g")" > build/linux/Debian/DEBIAN/control
|
||||||
|
dpkg-deb --build ./build/linux/Debian build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64.deb
|
||||||
|
|
||||||
|
mv -f build/bin/res-downloader build/bin/res-downloader_$(jq -r '.info.productVersion' wails.json)_linux_arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arch Linux
|
||||||
|
|
||||||
|
[](https://repology.org/project/res-downloader/versions)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -Syu res-downloader
|
||||||
|
```
|
||||||
|
### Linux 本地编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/putyy/res-downloader.git
|
||||||
|
cd res-downloader
|
||||||
|
# -- GO Proxy --
|
||||||
|
# 如果国内编译时 go 下载慢或报错,可以设置 go 国内代理加速,否则不用设置
|
||||||
|
export GO111MODULE=on
|
||||||
|
export GOPROXY=https://goproxy.cn,direct
|
||||||
|
# -- Go Proxy --
|
||||||
|
wails build
|
||||||
|
cd build
|
||||||
|
sudo install -Dvm755 bin/res-downloader -t /usr/bin
|
||||||
|
sudo install -Dvm644 appicon.png /usr/share/icons/hicolor/512x512/apps/res-downloader.png
|
||||||
|
sudo install -Dvm644 build/linux/Arch/res-downloader.desktop /usr/share/applications/res-downloader.desktop
|
||||||
|
```
|
||||||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
68
build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
63
build/darwin/Info.plist
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
5
build/linux/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
!.gitkeep
|
||||||
|
debian/usr/local/bin/*
|
||||||
|
debian/DEBIAN/control
|
||||||
|
AppImage/usr/bin/*
|
||||||
|
AppImage/usr/lib/*
|
||||||
1
build/linux/AppImage/.DirIcon
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
res-downloader.png
|
||||||
8
build/linux/AppImage/res-downloader.desktop
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=res-downloader
|
||||||
|
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
|
||||||
|
Exec=/usr/bin/res-downloader
|
||||||
|
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
BIN
build/linux/AppImage/usr/.DS_Store
vendored
Normal file
0
build/linux/AppImage/usr/bin/.gitkeep
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=res-downloader
|
||||||
|
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
|
||||||
|
Exec=/usr/bin/res-downloader
|
||||||
|
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
|
After Width: | Height: | Size: 45 KiB |
8
build/linux/Arch/res-downloader.desktop
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=res-downloader
|
||||||
|
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
|
||||||
|
Exec=res-downloader
|
||||||
|
Icon=res-downloader.png
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
BIN
build/linux/Debian/.DS_Store
vendored
Normal file
BIN
build/linux/Debian/DEBIAN/.DS_Store
vendored
Normal file
9
build/linux/Debian/DEBIAN/.control
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Package: res-downloader
|
||||||
|
Version: {{Version}}
|
||||||
|
Section: utils
|
||||||
|
Priority: optional
|
||||||
|
Architecture: {{Architecture}}
|
||||||
|
Depends: libwebkit2gtk-4.0-37
|
||||||
|
Maintainer: putyy@qq.com
|
||||||
|
Homepage: https://github.com/putyy/res-downloader
|
||||||
|
Description: This is a high-value and high-performance and diverse resource downloader called res-downloader
|
||||||
BIN
build/linux/Debian/usr/local/.DS_Store
vendored
Normal file
0
build/linux/Debian/usr/local/bin/.gitkeep
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=res-downloader
|
||||||
|
Comment=This is a high-value and high-performance and diverse resource downloader called res-downloader
|
||||||
|
Exec=/usr/local/bin/res-downloader
|
||||||
|
Icon=/usr/share/icons/hicolor/256x256/apps/res-downloader.png
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
|
After Width: | Height: | Size: 45 KiB |
29
build/linux/dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM golang:1.24.2-bookworm
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --fix-missing \
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
jq \
|
||||||
|
kmod \
|
||||||
|
fuse \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libwebkit2gtk-4.0-dev \
|
||||||
|
nsis \
|
||||||
|
wget \
|
||||||
|
curl \
|
||||||
|
gnupg2 \
|
||||||
|
lsb-release \
|
||||||
|
libfuse-dev \
|
||||||
|
libfuse2 \
|
||||||
|
file \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y nodejs
|
||||||
|
|
||||||
|
RUN go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
|
|
||||||
|
RUN wails doctor
|
||||||
BIN
build/windows/icon.ico
Normal file
|
After Width: | Height: | Size: 264 KiB |
15
build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fixed": {
|
||||||
|
"file_version": "{{.Info.ProductVersion}}"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"0000": {
|
||||||
|
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||||
|
"CompanyName": "{{.Info.CompanyName}}",
|
||||||
|
"FileDescription": "{{.Info.ProductName}}",
|
||||||
|
"LegalCopyright": "{{.Info.Copyright}}",
|
||||||
|
"ProductName": "{{.Info.ProductName}}",
|
||||||
|
"Comments": "{{.Info.Comments}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
build/windows/installer/project.nsi
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
Unicode true
|
||||||
|
|
||||||
|
####
|
||||||
|
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||||
|
## mentioned underneath.
|
||||||
|
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||||
|
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||||
|
## from outside of Wails for debugging and development of the installer.
|
||||||
|
##
|
||||||
|
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||||
|
## > wails build --target windows/amd64 --nsis
|
||||||
|
## Then you can call makensis on this file with specifying the path to your binary:
|
||||||
|
## For a AMD64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a ARM64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a installer with both architectures:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||||
|
####
|
||||||
|
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||||
|
####
|
||||||
|
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||||
|
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||||
|
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||||
|
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||||
|
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||||
|
###
|
||||||
|
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||||
|
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
####
|
||||||
|
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||||
|
####
|
||||||
|
## Include the wails tools
|
||||||
|
####
|
||||||
|
!include "wails_tools.nsh"
|
||||||
|
|
||||||
|
# The version information for this two must consist of 4 parts
|
||||||
|
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
|
||||||
|
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||||
|
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||||
|
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||||
|
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||||
|
|
||||||
|
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||||
|
ManifestDPIAware true
|
||||||
|
|
||||||
|
!include "MUI.nsh"
|
||||||
|
|
||||||
|
!define MUI_ICON "..\icon.ico"
|
||||||
|
!define MUI_UNICON "..\icon.ico"
|
||||||
|
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||||
|
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||||
|
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||||
|
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||||
|
|
||||||
|
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||||
|
#!uninstfinalize 'signtool --file "%1"'
|
||||||
|
#!finalize 'signtool --file "%1"'
|
||||||
|
|
||||||
|
Name "${INFO_PRODUCTNAME}"
|
||||||
|
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||||
|
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||||
|
ShowInstDetails show # This will always show the installation details.
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
!insertmacro wails.checkArchitecture
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
!insertmacro wails.webview2runtime
|
||||||
|
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
|
||||||
|
!insertmacro wails.files
|
||||||
|
|
||||||
|
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
|
||||||
|
!insertmacro wails.associateFiles
|
||||||
|
!insertmacro wails.associateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.writeUninstaller
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "uninstall"
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||||
|
|
||||||
|
RMDir /r $INSTDIR
|
||||||
|
|
||||||
|
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
|
||||||
|
!insertmacro wails.unassociateFiles
|
||||||
|
!insertmacro wails.unassociateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.deleteUninstaller
|
||||||
|
SectionEnd
|
||||||
BIN
build/windows/installer/tmp/MicrosoftEdgeWebview2Setup.exe
Executable file
236
build/windows/installer/wails_tools.nsh
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# DO NOT EDIT - Generated automatically by `wails build`
|
||||||
|
|
||||||
|
!include "x64.nsh"
|
||||||
|
!include "WinVer.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
!ifndef INFO_PROJECTNAME
|
||||||
|
!define INFO_PROJECTNAME "res-downloader"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COMPANYNAME
|
||||||
|
!define INFO_COMPANYNAME "res-downloader"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTNAME
|
||||||
|
!define INFO_PRODUCTNAME "res-downloader"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTVERSION
|
||||||
|
!define INFO_PRODUCTVERSION "3.1.3"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COPYRIGHT
|
||||||
|
!define INFO_COPYRIGHT "Copyright © 2023"
|
||||||
|
!endif
|
||||||
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
!endif
|
||||||
|
!ifndef UNINST_KEY_NAME
|
||||||
|
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||||
|
|
||||||
|
!ifndef REQUEST_EXECUTION_LEVEL
|
||||||
|
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_AMD64_BINARY
|
||||||
|
!define SUPPORTS_AMD64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_ARM64_BINARY
|
||||||
|
!define SUPPORTS_ARM64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "amd64_arm64"
|
||||||
|
!else
|
||||||
|
!define ARCH "amd64"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "arm64"
|
||||||
|
!else
|
||||||
|
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||||
|
!endif
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro wails.checkArchitecture
|
||||||
|
!ifndef WAILS_WIN10_REQUIRED
|
||||||
|
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||||
|
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${If} ${AtLeastWin10}
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
IfSilent silentArch notSilentArch
|
||||||
|
silentArch:
|
||||||
|
SetErrorLevel 65
|
||||||
|
Abort
|
||||||
|
notSilentArch:
|
||||||
|
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||||
|
Quit
|
||||||
|
${else}
|
||||||
|
IfSilent silentWin notSilentWin
|
||||||
|
silentWin:
|
||||||
|
SetErrorLevel 64
|
||||||
|
Abort
|
||||||
|
notSilentWin:
|
||||||
|
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.files
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.writeUninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
|
||||||
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
|
IntFmt $0 "0x%08X" $0
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.deleteUninstaller
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.setShellContext
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||||
|
SetShellVarContext all
|
||||||
|
${else}
|
||||||
|
SetShellVarContext current
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Install webview2 by launching the bootstrapper
|
||||||
|
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||||
|
!macro wails.webview2runtime
|
||||||
|
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||||
|
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
# If the admin key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||||
|
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||||
|
SetDetailsPrint listonly
|
||||||
|
|
||||||
|
InitPluginsDir
|
||||||
|
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||||
|
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||||
|
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||||
|
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||||
|
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateFiles
|
||||||
|
; Create file associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateFiles
|
||||||
|
; Delete app associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateCustomProtocols
|
||||||
|
; Create custom protocols associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateCustomProtocols
|
||||||
|
; Delete app custom protocol associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
15
build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
</assembly>
|
||||||
28
components.d.ts
vendored
@@ -1,28 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
|
||||||
// Generated by unplugin-vue-components
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
|
||||||
export {}
|
|
||||||
|
|
||||||
declare module 'vue' {
|
|
||||||
export interface GlobalComponents {
|
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
|
||||||
ElFooter: typeof import('element-plus/es')['ElFooter']
|
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
|
||||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
|
||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
|
||||||
Footer: typeof import('./src/components/layout/Footer.vue')['default']
|
|
||||||
Index: typeof import('./src/components/layout/Index.vue')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
|
||||||
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
72
core/aes.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AESCipher struct {
|
||||||
|
key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAESCipher(key string) *AESCipher {
|
||||||
|
return &AESCipher{key: []byte(key)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AESCipher) Encrypt(plainText string) (string, error) {
|
||||||
|
block, err := aes.NewCipher(a.key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
padding := block.BlockSize() - len(plainText)%block.BlockSize()
|
||||||
|
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
plainText = plainText + string(padText)
|
||||||
|
|
||||||
|
cipherText := make([]byte, aes.BlockSize+len(plainText))
|
||||||
|
iv := cipherText[:aes.BlockSize]
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
mode.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText))
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(cipherText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AESCipher) Decrypt(cipherText string) (string, error) {
|
||||||
|
cipherTextBytes, err := base64.StdEncoding.DecodeString(cipherText)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(a.key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cipherTextBytes) < aes.BlockSize {
|
||||||
|
return "", errors.New("ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := cipherTextBytes[:aes.BlockSize]
|
||||||
|
cipherTextBytes = cipherTextBytes[aes.BlockSize:]
|
||||||
|
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
mode.CryptBlocks(cipherTextBytes, cipherTextBytes)
|
||||||
|
|
||||||
|
padding := int(cipherTextBytes[len(cipherTextBytes)-1])
|
||||||
|
if padding > len(cipherTextBytes) || padding > aes.BlockSize {
|
||||||
|
return "", errors.New("padding size error")
|
||||||
|
}
|
||||||
|
plainText := cipherTextBytes[:len(cipherTextBytes)-padding]
|
||||||
|
|
||||||
|
return string(plainText), nil
|
||||||
|
}
|
||||||
211
core/app.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"github.com/vrischmann/userdir"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
assets embed.FS
|
||||||
|
AppName string `json:"AppName"`
|
||||||
|
Version string `json:"Version"`
|
||||||
|
Description string `json:"Description"`
|
||||||
|
Copyright string `json:"Copyright"`
|
||||||
|
UserDir string `json:"-"`
|
||||||
|
LockFile string `json:"-"`
|
||||||
|
PublicCrt []byte `json:"-"`
|
||||||
|
PrivateKey []byte `json:"-"`
|
||||||
|
IsProxy bool `json:"IsProxy"`
|
||||||
|
IsReset bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
appOnce *App
|
||||||
|
globalConfig *Config
|
||||||
|
globalLogger *Logger
|
||||||
|
resourceOnce *Resource
|
||||||
|
systemOnce *SystemSetup
|
||||||
|
proxyOnce *Proxy
|
||||||
|
httpServerOnce *HttpServer
|
||||||
|
ruleOnce *RuleSet
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetApp(assets embed.FS, wjs string) *App {
|
||||||
|
if appOnce == nil {
|
||||||
|
matches := regexp.MustCompile(`"productVersion":\s*"([\d.]+)"`).FindStringSubmatch(wjs)
|
||||||
|
version := "1.0.1"
|
||||||
|
if len(matches) > 0 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
appOnce = &App{
|
||||||
|
assets: assets,
|
||||||
|
AppName: "res-downloader",
|
||||||
|
Version: version,
|
||||||
|
Description: "res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!",
|
||||||
|
Copyright: "Copyright © 2023~" + strconv.Itoa(time.Now().Year()),
|
||||||
|
IsReset: false,
|
||||||
|
PublicCrt: []byte(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDwzCCAqugAwIBAgIUFAnC6268dp/z1DR9E1UepiWgWzkwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwcDELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUNob25ncWluZzESMBAGA1UEBwwJ
|
||||||
|
Q2hvbmdxaW5nMQ4wDAYDVQQKDAVnb3dhczEWMBQGA1UECwwNSVQgRGVwYXJ0bWVu
|
||||||
|
dDERMA8GA1UEAwwIZ293YXMuY24wIBcNMjQwMjE4MDIwOTI2WhgPMjEyNDAxMjUw
|
||||||
|
MjA5MjZaMHAxCzAJBgNVBAYTAkNOMRIwEAYDVQQIDAlDaG9uZ3FpbmcxEjAQBgNV
|
||||||
|
BAcMCUNob25ncWluZzEOMAwGA1UECgwFZ293YXMxFjAUBgNVBAsMDUlUIERlcGFy
|
||||||
|
dG1lbnQxETAPBgNVBAMMCGdvd2FzLmNuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||||
|
MIIBCgKCAQEA3A7dt7eoqAaBxv2Npjo8Z7VkGvXT93jZfpgAuuNuQ5RLcnOnMzQC
|
||||||
|
CrrjPcLfsAMA0AIK3eUWsXXKSR9SZTJBLQRZCJHZ9AIPfA+58JVQPTjd8UIuQZJf
|
||||||
|
rDf6FjhPJTsLzcjTU+mT7t6lEimPEl2VWN9eXWqs9nkVrJtqLao6m1hoYfXOxRh6
|
||||||
|
96/WgBtPHcmjujryteBiSITVflDjx+YQzDGsbqw7fM52klMPd2+w/vmhJ4pxq6P7
|
||||||
|
Ni2OBvdXYDPIuLfPFFqG16arORjBkyNCJy19iOuh5LXh+EUX11wvbLwNgsTd8j9v
|
||||||
|
eBSD+4HUUNQhiXiXJbs7I7cdFYthvb609QIDAQABo1MwUTAdBgNVHQ4EFgQUdI8p
|
||||||
|
aY1A47rWCRvQKSTRCCk6FoMwHwYDVR0jBBgwFoAUdI8paY1A47rWCRvQKSTRCCk6
|
||||||
|
FoMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEArMCAfqidgXL7
|
||||||
|
cW5TAZTCqnUeKzbbqMJgk6iFsma8scMRsUXz9ZhF0UVf98376KvoJpy4vd81afbi
|
||||||
|
TehQ8wVBuKTtkHeh/MkXMWC/FU4HqSjtvxpic2+Or5dMjIrfa5VYPgzfqNaBIUh4
|
||||||
|
InD5lo8b/n5V+jdwX7RX9VYAKug6QZlCg5YSKIvgNRChb36JmrGcvsp5R0Vejnii
|
||||||
|
e3oowvgwikqm6XR6BEcRpPkztqcKST7jPFGHiXWsAqiibc+/plMW9qebhfMXEGhQ
|
||||||
|
5yVNeSxX2zqasZvP/fRy+3I5iVilxtKvJuVpPZ0UZzGS0CJ/lF67ntibktiPa3sR
|
||||||
|
D8HixYbEDg==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`),
|
||||||
|
PrivateKey: []byte(`-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDcDt23t6ioBoHG
|
||||||
|
/Y2mOjxntWQa9dP3eNl+mAC6425DlEtyc6czNAIKuuM9wt+wAwDQAgrd5RaxdcpJ
|
||||||
|
H1JlMkEtBFkIkdn0Ag98D7nwlVA9ON3xQi5Bkl+sN/oWOE8lOwvNyNNT6ZPu3qUS
|
||||||
|
KY8SXZVY315daqz2eRWsm2otqjqbWGhh9c7FGHr3r9aAG08dyaO6OvK14GJIhNV+
|
||||||
|
UOPH5hDMMaxurDt8znaSUw93b7D++aEninGro/s2LY4G91dgM8i4t88UWobXpqs5
|
||||||
|
GMGTI0InLX2I66HkteH4RRfXXC9svA2CxN3yP294FIP7gdRQ1CGJeJcluzsjtx0V
|
||||||
|
i2G9vrT1AgMBAAECggEAF0obfQ4a82183qqHC0iui+tOpOvPeyl3G0bLDPx09wIC
|
||||||
|
2iITV//xF2GgGzE8q0wmEd2leMZ+GFn3BrYh6kPfUfxbz+RfxMtTCDZB34xt6YzT
|
||||||
|
MG1op9ft+DQUa7WZ6r7NCQJwGzllRqqZncp4MeFlpPo+6nQXyh4WhSYNnredbENE
|
||||||
|
uPZ63Kme4RZfMvtVso+XgAQM3oDih0onv1YitmNQpL9rRzlthTfybAT4737DBINq
|
||||||
|
zsmBNE6QIsXnSKpzo11OtDgof2QM9ac6eAXf73oTpDxfodwCotILytKn+8WYvlR+
|
||||||
|
T15uuknb4M3XI1FPVolkF4qtK5SLAAbVzV4DsCmuIQKBgQD6bTKKbL2huvU6dEKx
|
||||||
|
bgS079LfQUxxOTClgwkhVsMxRtvcPBnHYMAsPK4mnMhEh9x+TF6wxMx0pmhQluPI
|
||||||
|
ZULNBj/qdoiBL0RwVLA+9jgE0NeWB3XXFDsEavQBr9Q8CC0uzrsgsxFcvHpqqs2Q
|
||||||
|
RtngxRWtJP06D6mKC23s4YjDHwKBgQDg9KUCFqOmWcRXyeg9gYMC4jFFQw4lUQBd
|
||||||
|
sYpqSMHDw1b+T1W/dCPbwbxZL/+d8y930BYy9QYDtQwHdLyXCH0pHM7S6rfgr5xk
|
||||||
|
2Szd8xBUIqmeV/zcR00mTeQHJ1M50VHfclAVgZgkpWSoLwbX+bXyx/mfqLAtynZ5
|
||||||
|
yU9RfrT5awKBgQC0uJ8TlFvZXjFgyMvkfY/5/2R/ZwFCaFI573FkVNeyNP+vVNQJ
|
||||||
|
tUGZ6wSGqvg/tIgjwPtIuA0QVZLMLcgeMy1dBhiUHIxwJetO4V77YPaWSxx5kdKx
|
||||||
|
r1DT5FdI7FnOJNxufhQ/CdsKwJ3bYn3Mk8TiV3hIJnx0LR9dltfybeQjYwKBgDOY
|
||||||
|
6aApATBOtrJMJXC2HA61QwfX8Y6tnZ/f8RefyJHWZEXAfLKFORRWw5TRZZgdB247
|
||||||
|
1Furx81h4Xh0Vi1uTQb5DJdkLvjiTsTy60+dSMmDidQ/6ke8Mv3uL7dUVcqVMGpI
|
||||||
|
FgZYy0TcitHot3EiXZFqPN9aGc7m+XXFruPKZEgxAoGBAMA96jsow7CzulU+GRW8
|
||||||
|
Njg4zWuAEVErgPoNBcOXAVWLCTU/qGIEMNpZL6Ok34kf13pJDMjQ8eDuQHu5CSqf
|
||||||
|
0ul5Zy85fwfVq2IvNAyYT8eflQprTejFw22CHhfPBfADVW9ro8dK/Jw+J/31Vh7V
|
||||||
|
ILKEQKmPPzKs7kp/7Nz+2cT3
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
appOnce.UserDir = filepath.Join(userdir.GetConfigHome(), appOnce.AppName)
|
||||||
|
err := os.MkdirAll(appOnce.UserDir, 0750)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Mkdir UserDir err: ", err.Error())
|
||||||
|
}
|
||||||
|
appOnce.LockFile = filepath.Join(appOnce.UserDir, "install.lock")
|
||||||
|
initLogger()
|
||||||
|
initConfig()
|
||||||
|
initProxy()
|
||||||
|
initResource()
|
||||||
|
initHttpServer()
|
||||||
|
initSystem()
|
||||||
|
initRule()
|
||||||
|
}
|
||||||
|
return appOnce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
go httpServerOnce.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) OnExit() {
|
||||||
|
a.UnsetSystemProxy()
|
||||||
|
globalLogger.Close()
|
||||||
|
if appOnce.IsReset {
|
||||||
|
err := a.ResetApp()
|
||||||
|
fmt.Println("err:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) installCert() (string, error) {
|
||||||
|
out, err := systemOnce.installCert()
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Esg(err, out)
|
||||||
|
return out, err
|
||||||
|
} else {
|
||||||
|
if err := a.lock(); err != nil {
|
||||||
|
globalLogger.Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) OpenSystemProxy() error {
|
||||||
|
if a.IsProxy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := systemOnce.setProxy()
|
||||||
|
if err == nil {
|
||||||
|
a.IsProxy = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) UnsetSystemProxy() error {
|
||||||
|
if !a.IsProxy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := systemOnce.unsetProxy()
|
||||||
|
if err == nil {
|
||||||
|
a.IsProxy = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) isInstall() bool {
|
||||||
|
return shared.FileExist(a.LockFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) lock() error {
|
||||||
|
err := os.WriteFile(a.LockFile, []byte("success"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ResetApp() error {
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
exePath, err = filepath.Abs(exePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = os.Remove(filepath.Join(appOnce.UserDir, "install.lock"))
|
||||||
|
_ = os.Remove(filepath.Join(appOnce.UserDir, "pass.cache"))
|
||||||
|
_ = os.Remove(filepath.Join(appOnce.UserDir, "config.json"))
|
||||||
|
_ = os.Remove(filepath.Join(appOnce.UserDir, "cert.crt"))
|
||||||
|
|
||||||
|
cmd := exec.Command(exePath)
|
||||||
|
cmd.Start()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
25
core/bind.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bind struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBind() *Bind {
|
||||||
|
return &Bind{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bind) Config() *ResponseData {
|
||||||
|
return httpServerOnce.buildResp(1, "ok", globalConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bind) AppInfo() *ResponseData {
|
||||||
|
return httpServerOnce.buildResp(1, "ok", appOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bind) ResetApp() {
|
||||||
|
appOnce.IsReset = true
|
||||||
|
runtime.Quit(appOnce.ctx)
|
||||||
|
}
|
||||||
310
core/config.go
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MimeInfo struct {
|
||||||
|
Type string `json:"Type"`
|
||||||
|
Suffix string `json:"Suffix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config struct
|
||||||
|
type Config struct {
|
||||||
|
storage *Storage
|
||||||
|
Theme string `json:"Theme"`
|
||||||
|
Locale string `json:"Locale"`
|
||||||
|
Host string `json:"Host"`
|
||||||
|
Port string `json:"Port"`
|
||||||
|
Quality int `json:"Quality"`
|
||||||
|
SaveDirectory string `json:"SaveDirectory"`
|
||||||
|
FilenameLen int `json:"FilenameLen"`
|
||||||
|
FilenameTime bool `json:"FilenameTime"`
|
||||||
|
UpstreamProxy string `json:"UpstreamProxy"`
|
||||||
|
OpenProxy bool `json:"OpenProxy"`
|
||||||
|
DownloadProxy bool `json:"DownloadProxy"`
|
||||||
|
AutoProxy bool `json:"AutoProxy"`
|
||||||
|
WxAction bool `json:"WxAction"`
|
||||||
|
TaskNumber int `json:"TaskNumber"`
|
||||||
|
DownNumber int `json:"DownNumber"`
|
||||||
|
UserAgent string `json:"UserAgent"`
|
||||||
|
UseHeaders string `json:"UseHeaders"`
|
||||||
|
InsertTail bool `json:"InsertTail"`
|
||||||
|
MimeMap map[string]MimeInfo `json:"MimeMap"`
|
||||||
|
Rule string `json:"Rule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mimeMux sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func initConfig() *Config {
|
||||||
|
if globalConfig != nil {
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig := &Config{
|
||||||
|
Theme: "lightTheme",
|
||||||
|
Locale: "zh",
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: "8899",
|
||||||
|
Quality: 0,
|
||||||
|
SaveDirectory: getDefaultDownloadDir(),
|
||||||
|
FilenameLen: 0,
|
||||||
|
FilenameTime: true,
|
||||||
|
UpstreamProxy: "",
|
||||||
|
OpenProxy: false,
|
||||||
|
DownloadProxy: false,
|
||||||
|
AutoProxy: false,
|
||||||
|
WxAction: true,
|
||||||
|
TaskNumber: runtime.NumCPU() * 2,
|
||||||
|
DownNumber: 3,
|
||||||
|
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||||
|
UseHeaders: "default",
|
||||||
|
InsertTail: true,
|
||||||
|
MimeMap: getDefaultMimeMap(),
|
||||||
|
Rule: "*",
|
||||||
|
}
|
||||||
|
|
||||||
|
rawDefaults, err := json.Marshal(defaultConfig)
|
||||||
|
if err != nil {
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
storage := NewStorage("config.json", rawDefaults)
|
||||||
|
defaultConfig.storage = storage
|
||||||
|
globalConfig = defaultConfig
|
||||||
|
|
||||||
|
data, err := storage.Load()
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Esg(err, "load config failed, using defaults")
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &cacheMap); err != nil {
|
||||||
|
globalLogger.Esg(err, "parse cached config failed, using defaults")
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultMap map[string]interface{}
|
||||||
|
defaultBytes, _ := json.Marshal(defaultConfig)
|
||||||
|
_ = json.Unmarshal(defaultBytes, &defaultMap)
|
||||||
|
|
||||||
|
for k, v := range cacheMap {
|
||||||
|
if _, ok := defaultMap[k]; ok {
|
||||||
|
defaultMap[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalBytes, err := json.Marshal(defaultMap)
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Esg(err, "marshal merged config failed")
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(finalBytes, globalConfig); err != nil {
|
||||||
|
globalLogger.Esg(err, "unmarshal merged config to struct failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultMimeMap() map[string]MimeInfo {
|
||||||
|
return map[string]MimeInfo{
|
||||||
|
"image/png": {Type: "image", Suffix: ".png"},
|
||||||
|
"image/webp": {Type: "image", Suffix: ".webp"},
|
||||||
|
"image/jpeg": {Type: "image", Suffix: ".jpeg"},
|
||||||
|
"image/jpg": {Type: "image", Suffix: ".jpg"},
|
||||||
|
"image/gif": {Type: "image", Suffix: ".gif"},
|
||||||
|
"image/avif": {Type: "image", Suffix: ".avif"},
|
||||||
|
"image/bmp": {Type: "image", Suffix: ".bmp"},
|
||||||
|
"image/tiff": {Type: "image", Suffix: ".tiff"},
|
||||||
|
"image/heic": {Type: "image", Suffix: ".heic"},
|
||||||
|
"image/x-icon": {Type: "image", Suffix: ".ico"},
|
||||||
|
"image/svg+xml": {Type: "image", Suffix: ".svg"},
|
||||||
|
"image/vnd.adobe.photoshop": {Type: "image", Suffix: ".psd"},
|
||||||
|
"image/jp2": {Type: "image", Suffix: ".jp2"},
|
||||||
|
"image/jpeg2000": {Type: "image", Suffix: ".jp2"},
|
||||||
|
"image/apng": {Type: "image", Suffix: ".apng"},
|
||||||
|
"audio/mpeg": {Type: "audio", Suffix: ".mp3"},
|
||||||
|
"audio/mp3": {Type: "audio", Suffix: ".mp3"},
|
||||||
|
"audio/wav": {Type: "audio", Suffix: ".wav"},
|
||||||
|
"audio/aiff": {Type: "audio", Suffix: ".aiff"},
|
||||||
|
"audio/x-aiff": {Type: "audio", Suffix: ".aiff"},
|
||||||
|
"audio/aac": {Type: "audio", Suffix: ".aac"},
|
||||||
|
"audio/ogg": {Type: "audio", Suffix: ".ogg"},
|
||||||
|
"audio/flac": {Type: "audio", Suffix: ".flac"},
|
||||||
|
"audio/midi": {Type: "audio", Suffix: ".mid"},
|
||||||
|
"audio/x-midi": {Type: "audio", Suffix: ".mid"},
|
||||||
|
"audio/x-ms-wma": {Type: "audio", Suffix: ".wma"},
|
||||||
|
"audio/opus": {Type: "audio", Suffix: ".opus"},
|
||||||
|
"audio/webm": {Type: "audio", Suffix: ".webm"},
|
||||||
|
"audio/mp4": {Type: "audio", Suffix: ".m4a"},
|
||||||
|
"audio/amr": {Type: "audio", Suffix: ".amr"},
|
||||||
|
"video/mp4": {Type: "video", Suffix: ".mp4"},
|
||||||
|
"video/webm": {Type: "video", Suffix: ".webm"},
|
||||||
|
"video/ogg": {Type: "video", Suffix: ".ogv"},
|
||||||
|
"video/x-msvideo": {Type: "video", Suffix: ".avi"},
|
||||||
|
"video/mpeg": {Type: "video", Suffix: ".mpeg"},
|
||||||
|
"video/quicktime": {Type: "video", Suffix: ".mov"},
|
||||||
|
"video/x-ms-wmv": {Type: "video", Suffix: ".wmv"},
|
||||||
|
"video/3gpp": {Type: "video", Suffix: ".3gp"},
|
||||||
|
"video/x-matroska": {Type: "video", Suffix: ".mkv"},
|
||||||
|
"audio/video": {Type: "live", Suffix: ".flv"},
|
||||||
|
"video/x-flv": {Type: "live", Suffix: ".flv"},
|
||||||
|
"application/dash+xml": {Type: "live", Suffix: ".mpd"},
|
||||||
|
"application/vnd.apple.mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||||
|
"application/x-mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||||
|
"application/x-mpeg": {Type: "m3u8", Suffix: ".m3u8"},
|
||||||
|
"audio/x-mpegurl": {Type: "m3u8", Suffix: ".m3u8"},
|
||||||
|
"application/pdf": {Type: "pdf", Suffix: ".pdf"},
|
||||||
|
"application/vnd.ms-powerpoint": {Type: "ppt", Suffix: ".ppt"},
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation": {Type: "ppt", Suffix: ".pptx"},
|
||||||
|
"application/vnd.ms-excel": {Type: "xls", Suffix: ".xls"},
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {Type: "xls", Suffix: ".xlsx"},
|
||||||
|
"text/csv": {Type: "xls", Suffix: ".csv"},
|
||||||
|
"application/msword": {Type: "doc", Suffix: ".doc"},
|
||||||
|
"application/rtf": {Type: "doc", Suffix: ".rtf"},
|
||||||
|
"text/rtf": {Type: "doc", Suffix: ".rtf"},
|
||||||
|
"application/vnd.oasis.opendocument.text": {Type: "doc", Suffix: ".odt"},
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {Type: "doc", Suffix: ".docx"},
|
||||||
|
"font/woff": {Type: "font", Suffix: ".woff"},
|
||||||
|
"application/octet-stream": {Type: "stream", Suffix: "default"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultDownloadDir() string {
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir := usr.HomeDir
|
||||||
|
var downloadDir string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows", "darwin":
|
||||||
|
downloadDir = filepath.Join(homeDir, "Downloads")
|
||||||
|
case "linux":
|
||||||
|
downloadDir = filepath.Join(homeDir, "Downloads")
|
||||||
|
if xdgDir := os.Getenv("XDG_DOWNLOAD_DIR"); xdgDir != "" {
|
||||||
|
downloadDir = xdgDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat, err := os.Stat(downloadDir); err == nil && stat.IsDir() {
|
||||||
|
return downloadDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) setConfig(config Config) {
|
||||||
|
oldProxy := c.UpstreamProxy
|
||||||
|
openProxy := c.OpenProxy
|
||||||
|
oldRule := c.Rule
|
||||||
|
c.Host = config.Host
|
||||||
|
c.Port = config.Port
|
||||||
|
c.Theme = config.Theme
|
||||||
|
c.Locale = config.Locale
|
||||||
|
c.Quality = config.Quality
|
||||||
|
c.SaveDirectory = config.SaveDirectory
|
||||||
|
c.FilenameLen = config.FilenameLen
|
||||||
|
c.FilenameTime = config.FilenameTime
|
||||||
|
c.UpstreamProxy = config.UpstreamProxy
|
||||||
|
c.UserAgent = config.UserAgent
|
||||||
|
c.OpenProxy = config.OpenProxy
|
||||||
|
c.DownloadProxy = config.DownloadProxy
|
||||||
|
c.AutoProxy = config.AutoProxy
|
||||||
|
c.TaskNumber = config.TaskNumber
|
||||||
|
c.DownNumber = config.DownNumber
|
||||||
|
c.WxAction = config.WxAction
|
||||||
|
c.UseHeaders = config.UseHeaders
|
||||||
|
c.InsertTail = config.InsertTail
|
||||||
|
c.Rule = config.Rule
|
||||||
|
if oldProxy != c.UpstreamProxy || openProxy != c.OpenProxy {
|
||||||
|
proxyOnce.setTransport()
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldRule != c.Rule {
|
||||||
|
err := ruleOnce.Load(c.Rule)
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Esg(err, "set rule failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeMux.Lock()
|
||||||
|
c.MimeMap = config.MimeMap
|
||||||
|
mimeMux.Unlock()
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(c)
|
||||||
|
if err == nil {
|
||||||
|
_ = globalConfig.storage.Store(jsonData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) getConfig(key string) interface{} {
|
||||||
|
switch key {
|
||||||
|
case "Host":
|
||||||
|
return c.Host
|
||||||
|
case "Port":
|
||||||
|
return c.Port
|
||||||
|
case "Theme":
|
||||||
|
return c.Theme
|
||||||
|
case "Locale":
|
||||||
|
return c.Locale
|
||||||
|
case "Quality":
|
||||||
|
return c.Quality
|
||||||
|
case "SaveDirectory":
|
||||||
|
return c.SaveDirectory
|
||||||
|
case "FilenameLen":
|
||||||
|
return c.FilenameLen
|
||||||
|
case "FilenameTime":
|
||||||
|
return c.FilenameTime
|
||||||
|
case "UpstreamProxy":
|
||||||
|
return c.UpstreamProxy
|
||||||
|
case "UserAgent":
|
||||||
|
return c.UserAgent
|
||||||
|
case "OpenProxy":
|
||||||
|
return c.OpenProxy
|
||||||
|
case "DownloadProxy":
|
||||||
|
return c.DownloadProxy
|
||||||
|
case "AutoProxy":
|
||||||
|
return c.AutoProxy
|
||||||
|
case "TaskNumber":
|
||||||
|
return c.TaskNumber
|
||||||
|
case "DownNumber":
|
||||||
|
return c.DownNumber
|
||||||
|
case "WxAction":
|
||||||
|
return c.WxAction
|
||||||
|
case "UseHeaders":
|
||||||
|
return c.UseHeaders
|
||||||
|
case "InsertTail":
|
||||||
|
return c.InsertTail
|
||||||
|
case "MimeMap":
|
||||||
|
mimeMux.RLock()
|
||||||
|
defer mimeMux.RUnlock()
|
||||||
|
return c.MimeMap
|
||||||
|
case "Rule":
|
||||||
|
return c.Rule
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) typeSuffix(mime string) (string, string) {
|
||||||
|
mimeMux.RLock()
|
||||||
|
defer mimeMux.RUnlock()
|
||||||
|
mime = strings.ToLower(strings.Split(mime, ";")[0])
|
||||||
|
if v, ok := c.MimeMap[mime]; ok {
|
||||||
|
return v.Type, v.Suffix
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
444
core/downloader.go
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaxRetries = 3 // 最大重试次数
|
||||||
|
RetryDelay = 3 * time.Second // 重试延迟
|
||||||
|
MinPartSize = 1 * 1024 * 1024 // 最小分片大小(1MB)
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProgressCallback func(totalDownloaded float64, totalSize float64, taskID int, taskProgress float64)
|
||||||
|
|
||||||
|
type ProgressChan struct {
|
||||||
|
taskID int
|
||||||
|
bytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadTask struct {
|
||||||
|
taskID int
|
||||||
|
rangeStart int64
|
||||||
|
rangeEnd int64
|
||||||
|
downloadedSize int64
|
||||||
|
isCompleted bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileDownloader struct {
|
||||||
|
Url string
|
||||||
|
Referer string
|
||||||
|
ProxyUrl *url.URL
|
||||||
|
FileName string
|
||||||
|
File *os.File
|
||||||
|
totalTasks int
|
||||||
|
TotalSize int64
|
||||||
|
IsMultiPart bool
|
||||||
|
RetryOnError bool
|
||||||
|
Headers map[string]string
|
||||||
|
DownloadTaskList []*DownloadTask
|
||||||
|
progressCallback ProgressCallback
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileDownloader(url, filename string, totalTasks int, headers map[string]string) *FileDownloader {
|
||||||
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||||
|
return &FileDownloader{
|
||||||
|
Url: url,
|
||||||
|
FileName: filename,
|
||||||
|
totalTasks: totalTasks,
|
||||||
|
IsMultiPart: false,
|
||||||
|
RetryOnError: false,
|
||||||
|
TotalSize: 0,
|
||||||
|
Headers: headers,
|
||||||
|
DownloadTaskList: make([]*DownloadTask, 0),
|
||||||
|
ctx: ctx,
|
||||||
|
cancelFunc: cancelFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) buildClient() *http.Client {
|
||||||
|
transport := &http.Transport{
|
||||||
|
MaxIdleConnsPerHost: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
}
|
||||||
|
if fd.ProxyUrl != nil {
|
||||||
|
transport.Proxy = http.ProxyURL(fd.ProxyUrl)
|
||||||
|
}
|
||||||
|
return &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var forbiddenDownloadHeaders = map[string]struct{}{
|
||||||
|
"accept-encoding": {},
|
||||||
|
"content-length": {},
|
||||||
|
"host": {},
|
||||||
|
"connection": {},
|
||||||
|
"keep-alive": {},
|
||||||
|
"proxy-connection": {},
|
||||||
|
"transfer-encoding": {},
|
||||||
|
|
||||||
|
"sec-fetch-site": {},
|
||||||
|
"sec-fetch-mode": {},
|
||||||
|
"sec-fetch-dest": {},
|
||||||
|
"sec-fetch-user": {},
|
||||||
|
"sec-ch-ua": {},
|
||||||
|
"sec-ch-ua-mobile": {},
|
||||||
|
"sec-ch-ua-platform": {},
|
||||||
|
|
||||||
|
"if-none-match": {},
|
||||||
|
"if-modified-since": {},
|
||||||
|
|
||||||
|
"x-forwarded-for": {},
|
||||||
|
"x-real-ip": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) setHeaders(request *http.Request) {
|
||||||
|
for key, value := range fd.Headers {
|
||||||
|
if globalConfig.UseHeaders == "default" {
|
||||||
|
lk := strings.ToLower(key)
|
||||||
|
if _, forbidden := forbiddenDownloadHeaders[lk]; forbidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
request.Header.Set(key, value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(globalConfig.UseHeaders, key) {
|
||||||
|
request.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) init() error {
|
||||||
|
parsedURL, err := url.Parse(fd.Url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse URL failed: %w", err)
|
||||||
|
}
|
||||||
|
if parsedURL.Scheme != "" && parsedURL.Host != "" {
|
||||||
|
fd.Referer = parsedURL.Scheme + "://" + parsedURL.Host + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalConfig.DownloadProxy && globalConfig.UpstreamProxy != "" && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
|
||||||
|
proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
|
||||||
|
if err == nil {
|
||||||
|
fd.ProxyUrl = proxyURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest("HEAD", fd.Url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create HEAD request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := fd.Headers["User-Agent"]; !ok {
|
||||||
|
fd.Headers["User-Agent"] = globalConfig.UserAgent
|
||||||
|
}
|
||||||
|
if _, ok := fd.Headers["Referer"]; !ok {
|
||||||
|
fd.Headers["Referer"] = fd.Referer
|
||||||
|
}
|
||||||
|
|
||||||
|
fd.setHeaders(request)
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
for retries := 0; retries < MaxRetries; retries++ {
|
||||||
|
resp, err = fd.buildClient().Do(request)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if retries < MaxRetries-1 {
|
||||||
|
time.Sleep(RetryDelay)
|
||||||
|
globalLogger.Warn().Msgf("HEAD request failed, retrying (%d/%d): %v", retries+1, MaxRetries, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HEAD request failed after %d retries: %w", MaxRetries, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fd.TotalSize = resp.ContentLength
|
||||||
|
if fd.TotalSize <= 0 {
|
||||||
|
fd.IsMultiPart = false
|
||||||
|
fd.TotalSize = -1
|
||||||
|
} else if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > MinPartSize {
|
||||||
|
fd.IsMultiPart = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(fd.FileName)
|
||||||
|
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("create directory failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fd.FileName = shared.GetUniqueFileName(fd.FileName)
|
||||||
|
|
||||||
|
fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("file open failed: %w", err)
|
||||||
|
}
|
||||||
|
if fd.TotalSize > 0 {
|
||||||
|
if err := fd.File.Truncate(fd.TotalSize); err != nil {
|
||||||
|
fd.File.Close()
|
||||||
|
return fmt.Errorf("file truncate failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) createDownloadTasks() {
|
||||||
|
if fd.IsMultiPart {
|
||||||
|
if fd.totalTasks <= 0 {
|
||||||
|
fd.totalTasks = 4
|
||||||
|
}
|
||||||
|
eachSize := fd.TotalSize / int64(fd.totalTasks)
|
||||||
|
if eachSize < MinPartSize {
|
||||||
|
fd.totalTasks = int(fd.TotalSize / MinPartSize)
|
||||||
|
if fd.totalTasks < 1 {
|
||||||
|
fd.totalTasks = 1
|
||||||
|
}
|
||||||
|
eachSize = fd.TotalSize / int64(fd.totalTasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < fd.totalTasks; i++ {
|
||||||
|
start := eachSize * int64(i)
|
||||||
|
end := eachSize*int64(i+1) - 1
|
||||||
|
if i == fd.totalTasks-1 {
|
||||||
|
end = fd.TotalSize - 1
|
||||||
|
}
|
||||||
|
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
|
||||||
|
taskID: i,
|
||||||
|
rangeStart: start,
|
||||||
|
rangeEnd: end,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fd.totalTasks = 1
|
||||||
|
rangeEnd := int64(-1)
|
||||||
|
if fd.TotalSize > 0 {
|
||||||
|
rangeEnd = fd.TotalSize - 1
|
||||||
|
}
|
||||||
|
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
|
||||||
|
taskID: 0,
|
||||||
|
rangeStart: 0,
|
||||||
|
rangeEnd: rangeEnd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) startDownload() error {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
progressChan := make(chan ProgressChan, len(fd.DownloadTaskList))
|
||||||
|
errorChan := make(chan error, len(fd.DownloadTaskList))
|
||||||
|
|
||||||
|
for _, task := range fd.DownloadTaskList {
|
||||||
|
wg.Add(1)
|
||||||
|
go fd.startDownloadTask(wg, progressChan, errorChan, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
taskProgress := make([]int64, len(fd.DownloadTaskList))
|
||||||
|
totalDownloaded := int64(0)
|
||||||
|
|
||||||
|
for progress := range progressChan {
|
||||||
|
taskProgress[progress.taskID] += progress.bytes
|
||||||
|
totalDownloaded += progress.bytes
|
||||||
|
|
||||||
|
if fd.progressCallback != nil {
|
||||||
|
taskPercentage := float64(0)
|
||||||
|
if task := fd.DownloadTaskList[progress.taskID]; task != nil {
|
||||||
|
taskSize := task.rangeEnd - task.rangeStart + 1
|
||||||
|
if taskSize > 0 {
|
||||||
|
taskPercentage = float64(taskProgress[progress.taskID]) / float64(taskSize) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fd.progressCallback(float64(totalDownloaded), float64(fd.TotalSize), progress.taskID, taskPercentage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(progressChan)
|
||||||
|
close(errorChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var errArr []error
|
||||||
|
for err := range errorChan {
|
||||||
|
errArr = append(errArr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errArr) > 0 {
|
||||||
|
if !fd.RetryOnError && fd.IsMultiPart {
|
||||||
|
// 降级
|
||||||
|
fd.RetryOnError = true
|
||||||
|
fd.DownloadTaskList = []*DownloadTask{}
|
||||||
|
fd.totalTasks = 1
|
||||||
|
fd.IsMultiPart = false
|
||||||
|
fd.createDownloadTasks()
|
||||||
|
return fd.startDownload()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("download failed with %d errors: %v", len(errArr), errArr[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fd.verifyDownload(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan chan ProgressChan, errorChan chan error, task *DownloadTask) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for retries := 0; retries < MaxRetries; retries++ {
|
||||||
|
err := fd.doDownloadTask(progressChan, task)
|
||||||
|
if err == nil {
|
||||||
|
task.isCompleted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(err.Error(), "cancelled") {
|
||||||
|
errorChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task.err = err
|
||||||
|
globalLogger.Warn().Msgf("Task %d failed (attempt %d/%d): %v", task.taskID, retries+1, MaxRetries, err)
|
||||||
|
|
||||||
|
if retries < MaxRetries-1 {
|
||||||
|
select {
|
||||||
|
case <-fd.ctx.Done():
|
||||||
|
errorChan <- fmt.Errorf("task %d cancelled during retry", task.taskID)
|
||||||
|
return
|
||||||
|
case <-time.After(RetryDelay):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChan <- fmt.Errorf("task %d failed after %d attempts: %v", task.taskID, MaxRetries, task.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *DownloadTask) error {
|
||||||
|
select {
|
||||||
|
case <-fd.ctx.Done():
|
||||||
|
return fmt.Errorf("download cancelled")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(fd.ctx, "GET", fd.Url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create request failed: %w", err)
|
||||||
|
}
|
||||||
|
fd.setHeaders(request)
|
||||||
|
|
||||||
|
if fd.IsMultiPart {
|
||||||
|
rangeStart := task.rangeStart + task.downloadedSize
|
||||||
|
rangeHeader := fmt.Sprintf("bytes=%d-%d", rangeStart, task.rangeEnd)
|
||||||
|
request.Header.Set("Range", rangeHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := fd.buildClient()
|
||||||
|
resp, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("send request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if fd.IsMultiPart && resp.StatusCode != http.StatusPartialContent {
|
||||||
|
return fmt.Errorf("server does not support range requests, status: %d", resp.StatusCode)
|
||||||
|
} else if !fd.IsMultiPart && resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-fd.ctx.Done():
|
||||||
|
return fmt.Errorf("download cancelled")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := resp.Body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
writeSize := int64(n)
|
||||||
|
offset := task.rangeStart + task.downloadedSize
|
||||||
|
_, writeErr := fd.File.WriteAt(buf[:writeSize], offset)
|
||||||
|
if writeErr != nil {
|
||||||
|
return fmt.Errorf("write file failed at offset %d: %w", offset, writeErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.downloadedSize += writeSize
|
||||||
|
progressChan <- ProgressChan{taskID: task.taskID, bytes: writeSize}
|
||||||
|
|
||||||
|
if fd.TotalSize > 0 && task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read response failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) verifyDownload() error {
|
||||||
|
for _, task := range fd.DownloadTaskList {
|
||||||
|
if !task.isCompleted {
|
||||||
|
return fmt.Errorf("task %d not completed", task.taskID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fd.TotalSize > 0 {
|
||||||
|
_, err := fd.File.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get file info failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) Start() error {
|
||||||
|
if err := fd.init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fd.createDownloadTasks()
|
||||||
|
|
||||||
|
err := fd.startDownload()
|
||||||
|
|
||||||
|
if fd.File != nil {
|
||||||
|
fd.File.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FileDownloader) Cancel() {
|
||||||
|
if fd.cancelFunc != nil {
|
||||||
|
fd.cancelFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
if fd.File != nil {
|
||||||
|
fd.File.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if fd.FileName != "" {
|
||||||
|
_ = os.Remove(fd.FileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
408
core/http.go
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type respData map[string]interface{}
|
||||||
|
|
||||||
|
type ResponseData struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpServer struct{}
|
||||||
|
|
||||||
|
func initHttpServer() *HttpServer {
|
||||||
|
if httpServerOnce == nil {
|
||||||
|
httpServerOnce = &HttpServer{}
|
||||||
|
}
|
||||||
|
return httpServerOnce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) run() {
|
||||||
|
listener, err := net.Listen("tcp", globalConfig.Host+":"+globalConfig.Port)
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Err(err)
|
||||||
|
log.Fatalf("Service cannot start: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Service started, listening http://" + globalConfig.Host + ":" + globalConfig.Port)
|
||||||
|
if err1 := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Host == "127.0.0.1:"+globalConfig.Port && HandleApi(w, r) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
proxyOnce.Proxy.ServeHTTP(w, r) // 代理
|
||||||
|
}
|
||||||
|
})); err1 != nil {
|
||||||
|
globalLogger.Err(err1)
|
||||||
|
fmt.Printf("Service startup exception: %v", err1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) downCert(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/x-x509-ca-data")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment;filename=res-downloader-public.crt")
|
||||||
|
w.Header().Set("Content-Transfer-Encoding", "binary")
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(appOnce.PublicCrt)))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
io.Copy(w, io.NopCloser(bytes.NewReader(appOnce.PublicCrt)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) preview(w http.ResponseWriter, r *http.Request) {
|
||||||
|
realURL := r.URL.Query().Get("url")
|
||||||
|
if realURL == "" {
|
||||||
|
http.Error(w, "Missing 'url' parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
realURL, _ = url.QueryUnescape(realURL)
|
||||||
|
parsedURL, err := url.Parse(realURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to fetch the resource", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rangeHeader := r.Header.Get("Range"); rangeHeader != "" {
|
||||||
|
request.Header.Set("Range", rangeHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to fetch the resource", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
if strings.ToLower(k) == "access-control-allow-origin" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, vv := range v {
|
||||||
|
w.Header().Add(k, vv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
|
||||||
|
_, err = io.Copy(w, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to serve the resource", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) send(t string, data interface{}) {
|
||||||
|
jsonData, err := json.Marshal(map[string]interface{}{
|
||||||
|
"type": t,
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error converting map to JSON:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runtime.EventsEmit(appOnce.ctx, "event", string(jsonData))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) writeJson(w http.ResponseWriter, data *ResponseData) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
err := json.NewEncoder(w).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) error(w http.ResponseWriter, args ...interface{}) {
|
||||||
|
message := "ok"
|
||||||
|
var data interface{}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
message = args[0].(string)
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
data = args[1]
|
||||||
|
}
|
||||||
|
h.writeJson(w, h.buildResp(0, message, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) success(w http.ResponseWriter, args ...interface{}) {
|
||||||
|
message := "ok"
|
||||||
|
var data interface{}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
data = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
message = args[1].(string)
|
||||||
|
}
|
||||||
|
h.writeJson(w, h.buildResp(1, message, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) buildResp(code int, message string, data interface{}) *ResponseData {
|
||||||
|
return &ResponseData{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) openDirectoryDialog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
folder, err := runtime.OpenDirectoryDialog(appOnce.ctx, runtime.OpenDialogOptions{
|
||||||
|
DefaultDirectory: "",
|
||||||
|
Title: "Select a folder",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.success(w, respData{
|
||||||
|
"folder": folder,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) openFileDialog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filePath, err := runtime.OpenFileDialog(appOnce.ctx, runtime.OpenDialogOptions{
|
||||||
|
Filters: []runtime.FileFilter{
|
||||||
|
{
|
||||||
|
DisplayName: "Videos (*.mov;*.mp4)",
|
||||||
|
Pattern: "*.mp4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Title: "Select a file",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.success(w, respData{
|
||||||
|
"file": filePath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data struct {
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
|
if err == nil && data.FilePath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = shared.OpenFolder(data.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Err(err)
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.success(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) install(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if appOnce.isInstall() {
|
||||||
|
h.success(w, respData{
|
||||||
|
"isPass": systemOnce.Password == "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := appOnce.installCert()
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error()+"\n"+out, respData{
|
||||||
|
"isPass": systemOnce.Password == "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.success(w, respData{
|
||||||
|
"isPass": systemOnce.Password == "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) setSystemPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
IsCache bool `json:"isCache"`
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
systemOnce.SetPassword(data.Password, data.IsCache)
|
||||||
|
h.success(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) openSystemProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := appOnce.OpenSystemProxy()
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error(), respData{
|
||||||
|
"value": appOnce.IsProxy,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.success(w, respData{
|
||||||
|
"value": appOnce.IsProxy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := appOnce.UnsetSystemProxy()
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error(), respData{
|
||||||
|
"value": appOnce.IsProxy,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.success(w, respData{
|
||||||
|
"value": appOnce.IsProxy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) isProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.success(w, respData{
|
||||||
|
"value": appOnce.IsProxy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) appInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.success(w, appOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) getConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.success(w, globalConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) setConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data Config
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
globalConfig.setConfig(data)
|
||||||
|
h.success(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) setType(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
|
if err == nil {
|
||||||
|
if data.Type != "" {
|
||||||
|
resourceOnce.setResType(strings.Split(data.Type, ","))
|
||||||
|
} else {
|
||||||
|
resourceOnce.setResType([]string{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.success(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resourceOnce.clear()
|
||||||
|
h.success(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data struct {
|
||||||
|
Sign []string `json:"sign"`
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
|
if err == nil && len(data.Sign) > 0 {
|
||||||
|
for _, v := range data.Sign {
|
||||||
|
resourceOnce.delete(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.success(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data struct {
|
||||||
|
shared.MediaInfo
|
||||||
|
DecodeStr string `json:"decodeStr"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resourceOnce.download(data.MediaInfo, data.DecodeStr)
|
||||||
|
h.success(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data struct {
|
||||||
|
shared.MediaInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := resourceOnce.cancel(data.Id)
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.success(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data struct {
|
||||||
|
shared.MediaInfo
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
DecodeStr string `json:"decodeStr"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
savePath, err := resourceOnce.wxFileDecode(data.MediaInfo, data.Filename, data.DecodeStr)
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.success(w, respData{
|
||||||
|
"save_path": savePath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) batchExport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileName := filepath.Join(globalConfig.SaveDirectory, "res-downloader-"+shared.GetCurrentDateTimeFormatted()+".txt")
|
||||||
|
err := os.WriteFile(fileName, []byte(data.Content), 0644)
|
||||||
|
if err != nil {
|
||||||
|
h.error(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = shared.OpenFolder(fileName)
|
||||||
|
h.success(w, respData{
|
||||||
|
"file_name": fileName,
|
||||||
|
})
|
||||||
|
}
|
||||||
68
core/logger.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
zerolog.Logger
|
||||||
|
logFile *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func initLogger() *Logger {
|
||||||
|
if globalLogger == nil {
|
||||||
|
globalLogger = NewLogger(!shared.IsDevelopment(), filepath.Join(appOnce.UserDir, "logs", "app.log"))
|
||||||
|
}
|
||||||
|
return globalLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Close() {
|
||||||
|
_ = l.logFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Err(err error) {
|
||||||
|
l.Error().Stack().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Esg(err error, format string, v ...interface{}) {
|
||||||
|
l.Error().Stack().Err(err).Msgf(fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger create a new logger
|
||||||
|
func NewLogger(logFile bool, logPath string) *Logger {
|
||||||
|
var out io.Writer
|
||||||
|
if logFile {
|
||||||
|
// log to file
|
||||||
|
logDir := filepath.Dir(logPath)
|
||||||
|
if err := shared.CreateDirIfNotExist(logDir); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
logfile *os.File
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
logfile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
out = logfile
|
||||||
|
} else {
|
||||||
|
out = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := &Logger{}
|
||||||
|
if logFile {
|
||||||
|
logger.logFile = out.(*os.File)
|
||||||
|
}
|
||||||
|
logger.Logger = zerolog.New(zerolog.ConsoleWriter{
|
||||||
|
NoColor: true,
|
||||||
|
Out: out,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
|
}).With().Timestamp().Logger()
|
||||||
|
return logger
|
||||||
|
}
|
||||||
73
core/middleware.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Middleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if HandleApi(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleApi(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/api") {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
if r.URL.Path != "/api/preview" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/install":
|
||||||
|
httpServerOnce.install(w, r)
|
||||||
|
case "/api/set-system-password":
|
||||||
|
httpServerOnce.setSystemPassword(w, r)
|
||||||
|
case "/api/preview":
|
||||||
|
httpServerOnce.preview(w, r)
|
||||||
|
case "/api/proxy-open":
|
||||||
|
httpServerOnce.openSystemProxy(w, r)
|
||||||
|
case "/api/proxy-unset":
|
||||||
|
httpServerOnce.unsetSystemProxy(w, r)
|
||||||
|
case "/api/open-directory":
|
||||||
|
httpServerOnce.openDirectoryDialog(w, r)
|
||||||
|
case "/api/open-file":
|
||||||
|
httpServerOnce.openFileDialog(w, r)
|
||||||
|
case "/api/open-folder":
|
||||||
|
httpServerOnce.openFolder(w, r)
|
||||||
|
case "/api/is-proxy":
|
||||||
|
httpServerOnce.isProxy(w, r)
|
||||||
|
case "/api/app-info":
|
||||||
|
httpServerOnce.appInfo(w, r)
|
||||||
|
case "/api/set-config":
|
||||||
|
httpServerOnce.setConfig(w, r)
|
||||||
|
case "/api/get-config":
|
||||||
|
httpServerOnce.getConfig(w, r)
|
||||||
|
case "/api/set-type":
|
||||||
|
httpServerOnce.setType(w, r)
|
||||||
|
case "/api/clear":
|
||||||
|
httpServerOnce.clear(w, r)
|
||||||
|
case "/api/delete":
|
||||||
|
httpServerOnce.delete(w, r)
|
||||||
|
case "/api/download":
|
||||||
|
httpServerOnce.download(w, r)
|
||||||
|
case "/api/cancel":
|
||||||
|
httpServerOnce.cancel(w, r)
|
||||||
|
case "/api/wx-file-decode":
|
||||||
|
httpServerOnce.wxFileDecode(w, r)
|
||||||
|
case "/api/batch-export":
|
||||||
|
httpServerOnce.batchExport(w, r)
|
||||||
|
case "/api/cert":
|
||||||
|
httpServerOnce.downCert(w, r)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
87
core/plugins/plugin.default.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/elazarl/goproxy"
|
||||||
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultPlugin struct {
|
||||||
|
bridge *shared.Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DefaultPlugin) SetBridge(bridge *shared.Bridge) {
|
||||||
|
p.bridge = bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DefaultPlugin) Domains() []string {
|
||||||
|
return []string{"default"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DefaultPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DefaultPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||||
|
if resp == nil || resp.Request == nil || (resp.StatusCode != 200 && resp.StatusCode != 206 && resp.StatusCode != 304) {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
classify, suffix := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
|
||||||
|
if classify == "" {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
rawUrl := resp.Request.URL.String()
|
||||||
|
isAll, _ := p.bridge.GetResType("all")
|
||||||
|
isClassify, _ := p.bridge.GetResType(classify)
|
||||||
|
|
||||||
|
if suffix == "default" {
|
||||||
|
ext := filepath.Ext(filepath.Base(strings.Split(strings.Split(rawUrl, "?")[0], "#")[0]))
|
||||||
|
if ext != "" {
|
||||||
|
suffix = ext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urlSign := shared.Md5(rawUrl)
|
||||||
|
if ok := p.bridge.MediaIsMarked(urlSign); !ok && (isAll || isClassify) {
|
||||||
|
value, _ := strconv.ParseFloat(resp.Header.Get("content-length"), 64)
|
||||||
|
id, err := gonanoid.New()
|
||||||
|
if err != nil {
|
||||||
|
id = urlSign
|
||||||
|
}
|
||||||
|
res := shared.MediaInfo{
|
||||||
|
Id: id,
|
||||||
|
Url: rawUrl,
|
||||||
|
UrlSign: urlSign,
|
||||||
|
CoverUrl: "",
|
||||||
|
Size: value,
|
||||||
|
Domain: shared.GetTopLevelDomain(rawUrl),
|
||||||
|
Classify: classify,
|
||||||
|
Suffix: suffix,
|
||||||
|
Status: shared.DownloadStatusReady,
|
||||||
|
SavePath: "",
|
||||||
|
DecodeKey: "",
|
||||||
|
OtherData: map[string]string{},
|
||||||
|
Description: "",
|
||||||
|
ContentType: resp.Header.Get("Content-Type"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store entire request headers as JSON
|
||||||
|
if headers, err := json.Marshal(resp.Request.Header); err == nil {
|
||||||
|
res.OtherData["headers"] = string(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.bridge.MarkMedia(urlSign)
|
||||||
|
go func(res shared.MediaInfo) {
|
||||||
|
p.bridge.Send("newResources", res)
|
||||||
|
}(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
272
core/plugins/plugin.qq.com.go
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/elazarl/goproxy"
|
||||||
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var qqMediaRegex = regexp.MustCompile(`get\s*media\(\)\{`)
|
||||||
|
var qqCommentRegex = regexp.MustCompile(`async\s*finderGetCommentDetail\((\w+)\)\s*\{return(.*?)\s*}\s*async`)
|
||||||
|
|
||||||
|
type QqPlugin struct {
|
||||||
|
bridge *shared.Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) SetBridge(bridge *shared.Bridge) {
|
||||||
|
p.bridge = bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) Domains() []string {
|
||||||
|
return []string{"qq.com"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||||
|
if strings.Contains(r.Host, "qq.com") && strings.Contains(r.URL.Path, "/res-downloader/wechat") {
|
||||||
|
if p.bridge.GetConfig("WxAction").(bool) && r.URL.Query().Get("type") == "1" {
|
||||||
|
return p.handleWechatRequest(r, ctx)
|
||||||
|
} else if !p.bridge.GetConfig("WxAction").(bool) && r.URL.Query().Get("type") == "2" {
|
||||||
|
return p.handleWechatRequest(r, ctx)
|
||||||
|
} else {
|
||||||
|
return r, p.buildEmptyResponse(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||||
|
if resp.StatusCode != 200 && resp.StatusCode != 206 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
host := resp.Request.Host
|
||||||
|
Path := resp.Request.URL.Path
|
||||||
|
|
||||||
|
classify, _ := p.bridge.TypeSuffix(resp.Header.Get("Content-Type"))
|
||||||
|
if classify == "video" && strings.HasSuffix(host, "finder.video.qq.com") {
|
||||||
|
if strings.Contains(resp.Request.Header.Get("Origin"), "mp.weixin.qq.com") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(host, "channels.weixin.qq.com") &&
|
||||||
|
(strings.Contains(Path, "/web/pages/feed") || strings.Contains(Path, "/web/pages/home")) {
|
||||||
|
return p.replaceWxJsContent(resp, ".js\"", ".js?v="+p.v()+"\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(host, "res.wx.qq.com") {
|
||||||
|
respTemp := resp
|
||||||
|
is := false
|
||||||
|
if strings.HasSuffix(respTemp.Request.URL.RequestURI(), ".js?v="+p.v()) {
|
||||||
|
respTemp = p.replaceWxJsContent(respTemp, ".js\"", ".js?v="+p.v()+"\"")
|
||||||
|
is = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(Path, "web/web-finder/res/js/virtual_svg-icons-register.publish") {
|
||||||
|
body, err := io.ReadAll(respTemp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return respTemp
|
||||||
|
}
|
||||||
|
bodyStr := string(body)
|
||||||
|
newBody := qqMediaRegex.
|
||||||
|
ReplaceAllString(bodyStr, `
|
||||||
|
get media(){
|
||||||
|
if(this.objectDesc){
|
||||||
|
fetch("https://wxapp.tc.qq.com/res-downloader/wechat?type=1", {
|
||||||
|
method: "POST",
|
||||||
|
mode: "no-cors",
|
||||||
|
body: JSON.stringify(this.objectDesc),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
newBody = qqCommentRegex.
|
||||||
|
ReplaceAllString(newBody, `
|
||||||
|
async finderGetCommentDetail($1) {
|
||||||
|
var res = await$2;
|
||||||
|
if (res?.data?.object?.objectDesc) {
|
||||||
|
fetch("https://wxapp.tc.qq.com/res-downloader/wechat?type=2", {
|
||||||
|
method: "POST",
|
||||||
|
mode: "no-cors",
|
||||||
|
body: JSON.stringify(res.data.object.objectDesc),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}async
|
||||||
|
`)
|
||||||
|
newBodyBytes := []byte(newBody)
|
||||||
|
respTemp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
|
||||||
|
respTemp.ContentLength = int64(len(newBodyBytes))
|
||||||
|
respTemp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
|
||||||
|
return respTemp
|
||||||
|
}
|
||||||
|
if is {
|
||||||
|
return respTemp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return r, p.buildEmptyResponse(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
go p.handleMedia(body)
|
||||||
|
|
||||||
|
return r, p.buildEmptyResponse(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) handleMedia(body []byte) {
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaArr, ok := result["media"].([]interface{})
|
||||||
|
if !ok || len(mediaArr) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
firstMedia, ok := mediaArr[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawUrl, ok := firstMedia["url"].(string)
|
||||||
|
if !ok || rawUrl == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urlSign := shared.Md5(rawUrl)
|
||||||
|
if p.bridge.MediaIsMarked(urlSign) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := gonanoid.New()
|
||||||
|
if err != nil {
|
||||||
|
id = urlSign
|
||||||
|
}
|
||||||
|
|
||||||
|
res := shared.MediaInfo{
|
||||||
|
Id: id,
|
||||||
|
Url: rawUrl,
|
||||||
|
UrlSign: urlSign,
|
||||||
|
CoverUrl: "",
|
||||||
|
Size: 0,
|
||||||
|
Domain: shared.GetTopLevelDomain(rawUrl),
|
||||||
|
Classify: "video",
|
||||||
|
Suffix: ".mp4",
|
||||||
|
Status: shared.DownloadStatusReady,
|
||||||
|
SavePath: "",
|
||||||
|
DecodeKey: "",
|
||||||
|
OtherData: map[string]string{},
|
||||||
|
Description: "",
|
||||||
|
ContentType: "video/mp4",
|
||||||
|
}
|
||||||
|
|
||||||
|
if mediaType, ok := firstMedia["mediaType"].(float64); ok && mediaType == 9 {
|
||||||
|
res.Classify = "image"
|
||||||
|
res.Suffix = ".png"
|
||||||
|
res.ContentType = "image/png"
|
||||||
|
}
|
||||||
|
|
||||||
|
isAll, _ := p.bridge.GetResType("all")
|
||||||
|
isImage, _ := p.bridge.GetResType("image")
|
||||||
|
if res.Classify == "image" && !isImage && !isAll {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isVideo, _ := p.bridge.GetResType("video")
|
||||||
|
if res.Classify == "video" && !isVideo && !isAll {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlToken, ok := firstMedia["urlToken"].(string); ok {
|
||||||
|
res.Url += urlToken
|
||||||
|
}
|
||||||
|
|
||||||
|
switch size := firstMedia["fileSize"].(type) {
|
||||||
|
case float64:
|
||||||
|
res.Size = size
|
||||||
|
case string:
|
||||||
|
if value, err := strconv.ParseFloat(size, 64); err == nil {
|
||||||
|
res.Size = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if coverUrl, ok := firstMedia["coverUrl"].(string); ok {
|
||||||
|
res.CoverUrl = coverUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
if decodeKey, ok := firstMedia["decodeKey"].(string); ok {
|
||||||
|
res.DecodeKey = decodeKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc, ok := result["description"].(string); ok {
|
||||||
|
res.Description = desc
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec, ok := firstMedia["spec"].([]interface{}); ok {
|
||||||
|
var fileFormats []string
|
||||||
|
for _, item := range spec {
|
||||||
|
if m, ok := item.(map[string]interface{}); ok {
|
||||||
|
if format, ok := m["fileFormat"].(string); ok {
|
||||||
|
fileFormats = append(fileFormats, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.OtherData["wx_file_formats"] = strings.Join(fileFormats, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.bridge.MarkMedia(urlSign)
|
||||||
|
|
||||||
|
go func(res shared.MediaInfo) {
|
||||||
|
p.bridge.Send("newResources", res)
|
||||||
|
}(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) buildEmptyResponse(r *http.Request) *http.Response {
|
||||||
|
body := "The content does not exist"
|
||||||
|
resp := &http.Response{
|
||||||
|
Status: http.StatusText(http.StatusOK),
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Header: make(http.Header),
|
||||||
|
Body: io.NopCloser(strings.NewReader(body)),
|
||||||
|
ContentLength: int64(len(body)),
|
||||||
|
Request: r,
|
||||||
|
}
|
||||||
|
resp.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) replaceWxJsContent(resp *http.Response, old, new string) *http.Response {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
bodyString := string(body)
|
||||||
|
newBodyString := strings.ReplaceAll(bodyString, old, new)
|
||||||
|
newBodyBytes := []byte(newBodyString)
|
||||||
|
resp.Body = io.NopCloser(bytes.NewBuffer(newBodyBytes))
|
||||||
|
resp.ContentLength = int64(len(newBodyBytes))
|
||||||
|
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(newBodyBytes)))
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QqPlugin) v() string {
|
||||||
|
return p.bridge.GetVersion()
|
||||||
|
}
|
||||||
173
core/proxy.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"res-downloader/core/plugins"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/elazarl/goproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Proxy struct {
|
||||||
|
ctx context.Context
|
||||||
|
Proxy *goproxy.ProxyHttpServer
|
||||||
|
Is bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginRegistry = make(map[string]shared.Plugin)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ps := []shared.Plugin{
|
||||||
|
&plugins.QqPlugin{},
|
||||||
|
&plugins.DefaultPlugin{},
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge := &shared.Bridge{
|
||||||
|
GetVersion: func() string {
|
||||||
|
return appOnce.Version
|
||||||
|
},
|
||||||
|
GetResType: func(key string) (bool, bool) {
|
||||||
|
return resourceOnce.getResType(key)
|
||||||
|
},
|
||||||
|
TypeSuffix: func(mine string) (string, string) {
|
||||||
|
return globalConfig.typeSuffix(mine)
|
||||||
|
},
|
||||||
|
MediaIsMarked: func(key string) bool {
|
||||||
|
return resourceOnce.mediaIsMarked(key)
|
||||||
|
},
|
||||||
|
MarkMedia: func(key string) {
|
||||||
|
resourceOnce.markMedia(key)
|
||||||
|
},
|
||||||
|
GetConfig: func(key string) interface{} {
|
||||||
|
return globalConfig.getConfig(key)
|
||||||
|
},
|
||||||
|
Send: func(t string, data interface{}) {
|
||||||
|
httpServerOnce.send(t, data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range ps {
|
||||||
|
p.SetBridge(bridge)
|
||||||
|
for _, domain := range p.Domains() {
|
||||||
|
pluginRegistry[domain] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initProxy() *Proxy {
|
||||||
|
if proxyOnce == nil {
|
||||||
|
proxyOnce = &Proxy{}
|
||||||
|
proxyOnce.Startup()
|
||||||
|
}
|
||||||
|
return proxyOnce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) Startup() {
|
||||||
|
err := p.setCa()
|
||||||
|
if err != nil {
|
||||||
|
DialogErr("Failed to start proxy service:" + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Proxy = goproxy.NewProxyHttpServer()
|
||||||
|
//p.Proxy.KeepDestinationHeaders = true
|
||||||
|
//p.Proxy.Verbose = false
|
||||||
|
p.setTransport()
|
||||||
|
//p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
|
||||||
|
p.Proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||||
|
if ruleOnce.shouldMitm(host) {
|
||||||
|
return goproxy.MitmConnect, host
|
||||||
|
}
|
||||||
|
return goproxy.OkConnect, host
|
||||||
|
})
|
||||||
|
|
||||||
|
p.Proxy.OnRequest().DoFunc(p.httpRequestEvent)
|
||||||
|
p.Proxy.OnResponse().DoFunc(p.httpResponseEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) setCa() error {
|
||||||
|
ca, err := tls.X509KeyPair(appOnce.PublicCrt, appOnce.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ca.Leaf, err = x509.ParseCertificate(ca.Certificate[0]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
goproxy.GoproxyCa = ca
|
||||||
|
goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: goproxy.TLSConfigFromCA(&ca)}
|
||||||
|
goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&ca)}
|
||||||
|
goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&ca)}
|
||||||
|
goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&ca)}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) setTransport() {
|
||||||
|
transport := &http.Transport{
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
// MaxIdleConnsPerHost: 10,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
TLSHandshakeTimeout: 60 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 60 * time.Second,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Proxy.ConnectDial = nil
|
||||||
|
p.Proxy.ConnectDialWithReq = nil
|
||||||
|
|
||||||
|
if globalConfig.UpstreamProxy != "" && globalConfig.OpenProxy && !strings.Contains(globalConfig.UpstreamProxy, globalConfig.Port) {
|
||||||
|
proxyURL, err := url.Parse(globalConfig.UpstreamProxy)
|
||||||
|
if err == nil {
|
||||||
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
p.Proxy.ConnectDial = p.Proxy.NewConnectDialToProxy(globalConfig.UpstreamProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Proxy.Tr = transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) matchPlugin(host string) shared.Plugin {
|
||||||
|
domain := shared.GetTopLevelDomain(host)
|
||||||
|
if plugin, ok := pluginRegistry[domain]; ok {
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) httpRequestEvent(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||||
|
plugin := p.matchPlugin(r.Host)
|
||||||
|
if plugin != nil {
|
||||||
|
newReq, newResp := plugin.OnRequest(r, ctx)
|
||||||
|
if newResp != nil {
|
||||||
|
return newReq, newResp
|
||||||
|
}
|
||||||
|
|
||||||
|
if newReq != nil {
|
||||||
|
return newReq, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pluginRegistry["default"].OnRequest(r, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) httpResponseEvent(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||||
|
if resp == nil || resp.Request == nil {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin := p.matchPlugin(resp.Request.Host)
|
||||||
|
if plugin != nil {
|
||||||
|
newResp := plugin.OnResponse(resp, ctx)
|
||||||
|
if newResp != nil {
|
||||||
|
return newResp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginRegistry["default"].OnResponse(resp, ctx)
|
||||||
|
}
|
||||||
283
core/resource.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WxFileDecodeResult struct {
|
||||||
|
SavePath string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resource struct {
|
||||||
|
mediaMark sync.Map
|
||||||
|
tasks sync.Map
|
||||||
|
resType map[string]bool
|
||||||
|
resTypeMux sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func initResource() *Resource {
|
||||||
|
if resourceOnce == nil {
|
||||||
|
resourceOnce = &Resource{}
|
||||||
|
resourceOnce.resType = resourceOnce.buildResType(globalConfig.MimeMap)
|
||||||
|
}
|
||||||
|
return resourceOnce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) buildResType(mime map[string]MimeInfo) map[string]bool {
|
||||||
|
t := map[string]bool{
|
||||||
|
"all": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range mime {
|
||||||
|
if _, ok := t[item.Type]; !ok {
|
||||||
|
t[item.Type] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) mediaIsMarked(key string) bool {
|
||||||
|
_, loaded := r.mediaMark.Load(key)
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) markMedia(key string) {
|
||||||
|
r.mediaMark.Store(key, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) getResType(key string) (bool, bool) {
|
||||||
|
r.resTypeMux.RLock()
|
||||||
|
value, ok := r.resType[key]
|
||||||
|
r.resTypeMux.RUnlock()
|
||||||
|
return value, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) setResType(n []string) {
|
||||||
|
r.resTypeMux.Lock()
|
||||||
|
for key := range r.resType {
|
||||||
|
r.resType[key] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range n {
|
||||||
|
if _, ok := r.resType[value]; ok {
|
||||||
|
r.resType[value] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.resTypeMux.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) clear() {
|
||||||
|
r.mediaMark.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) delete(sign string) {
|
||||||
|
r.mediaMark.Delete(sign)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) cancel(id string) error {
|
||||||
|
if d, ok := r.tasks.Load(id); ok {
|
||||||
|
d.(*FileDownloader).Cancel()
|
||||||
|
r.tasks.Delete(id) // 可选:取消后清理
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("task not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) download(mediaInfo shared.MediaInfo, decodeStr string) {
|
||||||
|
if globalConfig.SaveDirectory == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func(mediaInfo shared.MediaInfo) {
|
||||||
|
rawUrl := mediaInfo.Url
|
||||||
|
fileName := shared.Md5(rawUrl)
|
||||||
|
|
||||||
|
if v := shared.GetFileNameFromURL(rawUrl); v != "" {
|
||||||
|
fileName = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if mediaInfo.Description != "" {
|
||||||
|
fileName = regexp.MustCompile(`[^\w\p{Han}]`).ReplaceAllString(mediaInfo.Description, "")
|
||||||
|
fileLen := globalConfig.FilenameLen
|
||||||
|
if fileLen <= 0 {
|
||||||
|
fileLen = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
runes := []rune(fileName)
|
||||||
|
if len(runes) > fileLen {
|
||||||
|
fileName = string(runes[:fileLen])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalConfig.FilenameTime {
|
||||||
|
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName+"_"+shared.GetCurrentDateTimeFormatted())
|
||||||
|
} else {
|
||||||
|
mediaInfo.SavePath = filepath.Join(globalConfig.SaveDirectory, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(mediaInfo.SavePath, mediaInfo.Suffix) {
|
||||||
|
mediaInfo.SavePath = mediaInfo.SavePath + mediaInfo.Suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(rawUrl, "qq.com") {
|
||||||
|
if globalConfig.Quality == 1 &&
|
||||||
|
strings.Contains(rawUrl, "encfilekey=") &&
|
||||||
|
strings.Contains(rawUrl, "token=") {
|
||||||
|
parseUrl, err := url.Parse(rawUrl)
|
||||||
|
queryParams := parseUrl.Query()
|
||||||
|
if err == nil && queryParams.Has("encfilekey") && queryParams.Has("token") {
|
||||||
|
rawUrl = parseUrl.Scheme + "://" + parseUrl.Host + "/" + parseUrl.Path +
|
||||||
|
"?encfilekey=" + queryParams.Get("encfilekey") +
|
||||||
|
"&token=" + queryParams.Get("token")
|
||||||
|
}
|
||||||
|
} else if globalConfig.Quality > 1 && mediaInfo.OtherData["wx_file_formats"] != "" {
|
||||||
|
format := strings.Split(mediaInfo.OtherData["wx_file_formats"], "#")
|
||||||
|
qualityMap := []string{
|
||||||
|
format[0],
|
||||||
|
format[len(format)/2],
|
||||||
|
format[len(format)-1],
|
||||||
|
}
|
||||||
|
rawUrl += "&X-snsvideoflag=" + qualityMap[globalConfig.Quality-2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers, _ := r.parseHeaders(mediaInfo)
|
||||||
|
|
||||||
|
downloader := NewFileDownloader(rawUrl, mediaInfo.SavePath, globalConfig.TaskNumber, headers)
|
||||||
|
downloader.progressCallback = func(totalDownloaded, totalSize float64, taskID int, taskProgress float64) {
|
||||||
|
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning)
|
||||||
|
}
|
||||||
|
r.tasks.Store(mediaInfo.Id, downloader)
|
||||||
|
err := downloader.Start()
|
||||||
|
mediaInfo.SavePath = downloader.FileName
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "cancelled") {
|
||||||
|
r.progressEventsEmit(mediaInfo, err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if decodeStr != "" {
|
||||||
|
r.progressEventsEmit(mediaInfo, "decrypting in progress", shared.DownloadStatusRunning)
|
||||||
|
if err := r.decodeWxFile(mediaInfo.SavePath, decodeStr); err != nil {
|
||||||
|
r.progressEventsEmit(mediaInfo, "decryption error: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.progressEventsEmit(mediaInfo, "complete", shared.DownloadStatusDone)
|
||||||
|
}(mediaInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) parseHeaders(mediaInfo shared.MediaInfo) (map[string]string, error) {
|
||||||
|
headers := make(map[string]string)
|
||||||
|
|
||||||
|
if hh, ok := mediaInfo.OtherData["headers"]; ok {
|
||||||
|
var tempHeaders map[string][]string
|
||||||
|
if err := json.Unmarshal([]byte(hh), &tempHeaders); err != nil {
|
||||||
|
return headers, fmt.Errorf("parse headers JSON err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, values := range tempHeaders {
|
||||||
|
if len(values) > 0 {
|
||||||
|
headers[key] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) wxFileDecode(mediaInfo shared.MediaInfo, fileName, decodeStr string) (string, error) {
|
||||||
|
sourceFile, err := os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
mediaInfo.SavePath = strings.ReplaceAll(fileName, ".mp4", "_decrypt.mp4")
|
||||||
|
|
||||||
|
destinationFile, err := os.Create(mediaInfo.SavePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer destinationFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destinationFile, sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = r.decodeWxFile(mediaInfo.SavePath, decodeStr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return mediaInfo.SavePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) progressEventsEmit(mediaInfo shared.MediaInfo, args ...string) {
|
||||||
|
Status := shared.DownloadStatusError
|
||||||
|
Message := "ok"
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
Message = args[0]
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
Status = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
httpServerOnce.send("downloadProgress", map[string]interface{}{
|
||||||
|
"Id": mediaInfo.Id,
|
||||||
|
"Status": Status,
|
||||||
|
"SavePath": mediaInfo.SavePath,
|
||||||
|
"Message": Message,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) decodeWxFile(fileName, decodeStr string) error {
|
||||||
|
decodedBytes, err := base64.StdEncoding.DecodeString(decodeStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := os.OpenFile(fileName, os.O_RDWR, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
byteCount := len(decodedBytes)
|
||||||
|
fileBytes := make([]byte, byteCount)
|
||||||
|
n, err := file.Read(fileBytes)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < byteCount {
|
||||||
|
byteCount = n
|
||||||
|
}
|
||||||
|
|
||||||
|
xorResult := make([]byte, byteCount)
|
||||||
|
for i := 0; i < byteCount; i++ {
|
||||||
|
xorResult[i] = decodedBytes[i] ^ fileBytes[i]
|
||||||
|
}
|
||||||
|
_, err = file.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = file.Write(xorResult)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
126
core/rule.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
raw string
|
||||||
|
isNeg bool // 是否否定规则(以 ! 开头)
|
||||||
|
isWildcard bool // 是否为 *.domain 形式
|
||||||
|
isAll bool
|
||||||
|
domain string // 域名部分,不含 "*."
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleSet struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
rules []Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func initRule() *RuleSet {
|
||||||
|
if ruleOnce == nil {
|
||||||
|
ruleOnce = &RuleSet{}
|
||||||
|
err := ruleOnce.Load(globalConfig.Rule)
|
||||||
|
if err != nil {
|
||||||
|
globalLogger.Esg(err, "init rule failed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ruleOnce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuleSet) Load(rs string) error {
|
||||||
|
reader := strings.NewReader(rs)
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|
||||||
|
var rules []Rule
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isNeg := false
|
||||||
|
if strings.HasPrefix(line, "!") {
|
||||||
|
isNeg = true
|
||||||
|
line = strings.TrimSpace(line[1:])
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if line == "*" {
|
||||||
|
rules = append(rules, Rule{
|
||||||
|
raw: "*",
|
||||||
|
isAll: true,
|
||||||
|
isNeg: isNeg,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isWildcard := false
|
||||||
|
domain := line
|
||||||
|
if strings.HasPrefix(line, "*.") {
|
||||||
|
isWildcard = true
|
||||||
|
domain = line[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, Rule{
|
||||||
|
raw: line,
|
||||||
|
isNeg: isNeg,
|
||||||
|
isWildcard: isWildcard,
|
||||||
|
domain: strings.ToLower(domain),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
r.rules = rules
|
||||||
|
r.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldMitm: 根据当前规则集判断是否对 host 做 MITM
|
||||||
|
// host 可能带端口(example.com:443),函数会只匹配 hostname 部分
|
||||||
|
// 返回 true => MITM(解密),false => 透传
|
||||||
|
func (r *RuleSet) shouldMitm(host string) bool {
|
||||||
|
h := host
|
||||||
|
if strings.HasPrefix(h, "[") {
|
||||||
|
if hostSplitIdx := strings.LastIndex(h, "]"); hostSplitIdx != -1 {
|
||||||
|
h = h[:hostSplitIdx+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hp, _, err := net.SplitHostPort(host); err == nil {
|
||||||
|
h = hp
|
||||||
|
}
|
||||||
|
h = strings.ToLower(strings.Trim(h, "[]"))
|
||||||
|
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
action := false
|
||||||
|
for _, rule := range r.rules {
|
||||||
|
if rule.isAll {
|
||||||
|
action = !rule.isNeg
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.isWildcard {
|
||||||
|
if h == rule.domain || strings.HasSuffix(h, "."+rule.domain) {
|
||||||
|
action = !rule.isNeg
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if h == rule.domain {
|
||||||
|
action = !rule.isNeg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return action
|
||||||
|
}
|
||||||
18
core/shared/base.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
type MediaInfo struct {
|
||||||
|
Id string
|
||||||
|
Url string
|
||||||
|
UrlSign string
|
||||||
|
CoverUrl string
|
||||||
|
Size float64
|
||||||
|
Domain string
|
||||||
|
Classify string
|
||||||
|
Suffix string
|
||||||
|
SavePath string
|
||||||
|
Status string
|
||||||
|
DecodeKey string
|
||||||
|
Description string
|
||||||
|
ContentType string
|
||||||
|
OtherData map[string]string
|
||||||
|
}
|
||||||
9
core/shared/const.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
const (
|
||||||
|
DownloadStatusReady string = "ready" // task create but not start
|
||||||
|
DownloadStatusRunning string = "running"
|
||||||
|
DownloadStatusError string = "error"
|
||||||
|
DownloadStatusDone string = "done"
|
||||||
|
DownloadStatusHandle string = "handle"
|
||||||
|
)
|
||||||
23
core/shared/plugin.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/elazarl/goproxy"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bridge struct {
|
||||||
|
GetVersion func() string
|
||||||
|
GetResType func(key string) (bool, bool)
|
||||||
|
TypeSuffix func(mime string) (string, string)
|
||||||
|
MediaIsMarked func(key string) bool
|
||||||
|
MarkMedia func(key string)
|
||||||
|
GetConfig func(key string) interface{}
|
||||||
|
Send func(t string, data interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plugin interface {
|
||||||
|
SetBridge(*Bridge)
|
||||||
|
Domains() []string
|
||||||
|
OnRequest(*http.Request, *goproxy.ProxyCtx) (*http.Request, *http.Response)
|
||||||
|
OnResponse(*http.Response, *goproxy.ProxyCtx) *http.Response
|
||||||
|
}
|
||||||
163
core/shared/utils.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
sysRuntime "runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Md5(data string) string {
|
||||||
|
hashNew := md5.New()
|
||||||
|
hashNew.Write([]byte(data))
|
||||||
|
hash := hashNew.Sum(nil)
|
||||||
|
return hex.EncodeToString(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatSize(size float64) string {
|
||||||
|
if size > 1048576 {
|
||||||
|
return fmt.Sprintf("%.2fMB", float64(size)/1048576)
|
||||||
|
}
|
||||||
|
if size > 1024 {
|
||||||
|
return fmt.Sprintf("%.2fKB", float64(size)/1024)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.0fb", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTopLevelDomain(rawURL string) string {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err == nil && u.Host != "" {
|
||||||
|
rawURL = u.Host
|
||||||
|
}
|
||||||
|
domain, err := publicsuffix.EffectiveTLDPlusOne(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return rawURL
|
||||||
|
}
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileExist(file string) bool {
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateDirIfNotExist(dir string) error {
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
return os.MkdirAll(dir, 0750)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsDevelopment() bool {
|
||||||
|
return os.Getenv("APP_ENV") == "development"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileNameFromURL(rawUrl string) string {
|
||||||
|
parsedURL, err := url.Parse(rawUrl)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := path.Base(parsedURL.Path)
|
||||||
|
if fileName == "" || fileName == "/" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded, err := url.QueryUnescape(fileName); err == nil {
|
||||||
|
fileName = decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
re := regexp.MustCompile(`[<>:"/\\|?*]`)
|
||||||
|
fileName = re.ReplaceAllString(fileName, "_")
|
||||||
|
|
||||||
|
fileName = strings.TrimRightFunc(fileName, func(r rune) bool {
|
||||||
|
return r == '.' || r == ' '
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxFileNameLen = 255
|
||||||
|
runes := []rune(fileName)
|
||||||
|
if len(runes) > maxFileNameLen {
|
||||||
|
ext := path.Ext(fileName)
|
||||||
|
name := strings.TrimSuffix(fileName, ext)
|
||||||
|
|
||||||
|
runes = []rune(name)
|
||||||
|
if len(runes) > maxFileNameLen-len(ext) {
|
||||||
|
runes = runes[:maxFileNameLen-len(ext)]
|
||||||
|
}
|
||||||
|
name = string(runes)
|
||||||
|
fileName = name + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCurrentDateTimeFormatted() string {
|
||||||
|
now := time.Now()
|
||||||
|
return fmt.Sprintf("%04d%02d%02d%02d%02d%02d",
|
||||||
|
now.Year(),
|
||||||
|
now.Month(),
|
||||||
|
now.Day(),
|
||||||
|
now.Hour(),
|
||||||
|
now.Minute(),
|
||||||
|
now.Second())
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUniqueFileName(filePath string) string {
|
||||||
|
if !FileExist(filePath) {
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(filePath)
|
||||||
|
baseName := strings.TrimSuffix(filePath, ext)
|
||||||
|
count := 1
|
||||||
|
|
||||||
|
for {
|
||||||
|
newFileName := fmt.Sprintf("%s(%d)%s", baseName, count, ext)
|
||||||
|
if !FileExist(newFileName) {
|
||||||
|
return newFileName
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenFolder(filePath string) error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
|
switch sysRuntime.GOOS {
|
||||||
|
case "darwin":
|
||||||
|
cmd = exec.Command("open", "-R", filePath)
|
||||||
|
case "windows":
|
||||||
|
cmd = exec.Command("explorer", "/select,", filePath)
|
||||||
|
case "linux":
|
||||||
|
cmd = exec.Command("nautilus", filePath)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
cmd = exec.Command("thunar", filePath)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
cmd = exec.Command("dolphin", filePath)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
cmd = exec.Command("pcmanfm", filePath)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("unsupported platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
41
core/storage.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"res-downloader/core/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
fileName string
|
||||||
|
def []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorage(filename string, def []byte) *Storage {
|
||||||
|
return &Storage{
|
||||||
|
fileName: path.Join(appOnce.UserDir, filename),
|
||||||
|
def: def,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Storage) Load() ([]byte, error) {
|
||||||
|
if !shared.FileExist(l.fileName) {
|
||||||
|
err := os.WriteFile(l.fileName, l.def, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return l.def, nil
|
||||||
|
}
|
||||||
|
d, err := os.ReadFile(l.fileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Storage) Store(data []byte) error {
|
||||||
|
if err := os.WriteFile(l.fileName, data, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
83
core/system.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SystemSetup struct {
|
||||||
|
CertFile string
|
||||||
|
CacheFile string
|
||||||
|
Password string
|
||||||
|
aesCipher *AESCipher
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSystem() *SystemSetup {
|
||||||
|
if systemOnce == nil {
|
||||||
|
systemOnce = &SystemSetup{
|
||||||
|
aesCipher: NewAESCipher("resd48w2d7er95627d447c490a8f02ff"),
|
||||||
|
CertFile: filepath.Join(appOnce.UserDir, "cert.crt"),
|
||||||
|
CacheFile: filepath.Join(appOnce.UserDir, "pass.cache"),
|
||||||
|
}
|
||||||
|
systemOnce.checkPasswordFile()
|
||||||
|
}
|
||||||
|
return systemOnce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) initCert() ([]byte, error) {
|
||||||
|
content, err := os.ReadFile(s.CertFile)
|
||||||
|
if err == nil {
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.WriteFile(s.CertFile, appOnce.PublicCrt, 0750)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return appOnce.PublicCrt, nil
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) SetPassword(password string, isCache bool) {
|
||||||
|
s.Password = password
|
||||||
|
if isCache {
|
||||||
|
encrypted, err := s.aesCipher.Encrypt(password)
|
||||||
|
if err == nil {
|
||||||
|
err1 := os.WriteFile(s.CacheFile, []byte(encrypted), 0750)
|
||||||
|
if err1 != nil {
|
||||||
|
fmt.Println("Failed to write password: ", err1.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Failed to Encrypt password: ", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) checkPasswordFile() {
|
||||||
|
fileInfo, err := os.Stat(s.CacheFile)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastModified := fileInfo.ModTime()
|
||||||
|
oneMonthAgo := time.Now().AddDate(0, -1, 0)
|
||||||
|
if lastModified.Before(oneMonthAgo) {
|
||||||
|
os.Remove(s.CacheFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(s.CacheFile)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := s.aesCipher.Decrypt(string(content))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Password = password
|
||||||
|
}
|
||||||
129
core/system_darwin.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *SystemSetup) runCommand(args []string) ([]byte, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return nil, fmt.Errorf("no command provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if s.Password != "" {
|
||||||
|
cmd = exec.Command("sudo", append([]string{"-S"}, args...)...)
|
||||||
|
cmd.Stdin = bytes.NewReader([]byte(s.Password + "\n"))
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(args[0], args[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) getNetworkServices() ([]string, error) {
|
||||||
|
output, err := s.runCommand([]string{"networksetup", "-listallnetworkservices"})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute command: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
services := strings.Split(string(output), "\n")
|
||||||
|
var activeServices []string
|
||||||
|
for _, service := range services {
|
||||||
|
service = strings.TrimSpace(service)
|
||||||
|
if service == "" || strings.Contains(service, "*") || strings.Contains(service, "Serial Port") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
infoOutput, err := s.runCommand([]string{"networksetup", "-getinfo", service})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to get info for service %s: %v\n", service, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(string(infoOutput), "IP address:") {
|
||||||
|
activeServices = append(activeServices, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(activeServices) == 0 {
|
||||||
|
return nil, fmt.Errorf("no active network services found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeServices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) setProxy() error {
|
||||||
|
services, err := s.getNetworkServices()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isSuccess := false
|
||||||
|
var errs strings.Builder
|
||||||
|
for _, serviceName := range services {
|
||||||
|
commands := [][]string{
|
||||||
|
{"networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port},
|
||||||
|
{"networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port},
|
||||||
|
}
|
||||||
|
for _, cmd := range commands {
|
||||||
|
if output, err := s.runCommand(cmd); err != nil {
|
||||||
|
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
|
||||||
|
} else {
|
||||||
|
isSuccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to set proxy for any active network service, errs:%s", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) unsetProxy() error {
|
||||||
|
services, err := s.getNetworkServices()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isSuccess := false
|
||||||
|
var errs strings.Builder
|
||||||
|
for _, serviceName := range services {
|
||||||
|
commands := [][]string{
|
||||||
|
{"networksetup", "-setwebproxystate", serviceName, "off"},
|
||||||
|
{"networksetup", "-setsecurewebproxystate", serviceName, "off"},
|
||||||
|
}
|
||||||
|
for _, cmd := range commands {
|
||||||
|
if output, err := s.runCommand(cmd); err != nil {
|
||||||
|
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
|
||||||
|
} else {
|
||||||
|
isSuccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to unset proxy for any active network service, errs:%s", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) installCert() (string, error) {
|
||||||
|
_, err := s.initCert()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
output, err := s.runCommand([]string{"security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile})
|
||||||
|
if err != nil {
|
||||||
|
return string(output), err
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
142
core/system_linux.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *SystemSetup) getLinuxDistro() (string, error) {
|
||||||
|
data, err := os.ReadFile("/etc/os-release")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
if strings.HasPrefix(line, "ID=") {
|
||||||
|
return strings.Trim(strings.TrimPrefix(line, "ID="), "\""), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("could not determine linux distribution")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) runCommand(args []string, sudo bool) ([]byte, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return nil, fmt.Errorf("no command provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if s.Password != "" && sudo {
|
||||||
|
cmd = exec.Command("sudo", append([]string{"-S"}, args...)...)
|
||||||
|
cmd.Stdin = bytes.NewReader([]byte(s.Password + "\n"))
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(args[0], args[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) setProxy() error {
|
||||||
|
commands := [][]string{
|
||||||
|
{"gsettings", "set", "org.gnome.system.proxy", "mode", "manual"},
|
||||||
|
{"gsettings", "set", "org.gnome.system.proxy.http", "host", "127.0.0.1"},
|
||||||
|
{"gsettings", "set", "org.gnome.system.proxy.http", "port", globalConfig.Port},
|
||||||
|
{"gsettings", "set", "org.gnome.system.proxy.https", "host", "127.0.0.1"},
|
||||||
|
{"gsettings", "set", "org.gnome.system.proxy.https", "port", globalConfig.Port},
|
||||||
|
}
|
||||||
|
|
||||||
|
isSuccess := false
|
||||||
|
var errs strings.Builder
|
||||||
|
|
||||||
|
for _, cmd := range commands {
|
||||||
|
if output, err := s.runCommand(cmd, false); err != nil {
|
||||||
|
errs.WriteString(fmt.Sprintf("cmd: %v\noutput: %s\nerr: %s\n", cmd, output, err))
|
||||||
|
} else {
|
||||||
|
isSuccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to set proxy:\n%s", errs.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) unsetProxy() error {
|
||||||
|
cmd := []string{"gsettings", "set", "org.gnome.system.proxy", "mode", "none"}
|
||||||
|
output, err := s.runCommand(cmd, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unset proxy: %s\noutput: %s", err.Error(), string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) installCert() (string, error) {
|
||||||
|
_, err := s.initCert()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
distro, err := s.getLinuxDistro()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("detect distro failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certName := appOnce.AppName + ".crt"
|
||||||
|
var certPath string
|
||||||
|
var updateCmd = []string{"update-ca-certificates"}
|
||||||
|
|
||||||
|
switch distro {
|
||||||
|
case "deepin":
|
||||||
|
certDir := "/usr/share/ca-certificates/" + appOnce.AppName
|
||||||
|
certPath = certDir + "/" + certName
|
||||||
|
s.runCommand([]string{"mkdir", "-p", certDir}, true)
|
||||||
|
case "arch":
|
||||||
|
certPath = "/usr/share/ca-certificates/trust-source/" + certName
|
||||||
|
updateCmd = []string{"update-ca-trust"}
|
||||||
|
default:
|
||||||
|
certPath = "/usr/local/share/ca-certificates/" + certName
|
||||||
|
}
|
||||||
|
|
||||||
|
var outs, errs strings.Builder
|
||||||
|
isSuccess := false
|
||||||
|
|
||||||
|
if output, err := s.runCommand([]string{"cp", "-f", s.CertFile, certPath}, true); err != nil {
|
||||||
|
errs.WriteString(fmt.Sprintf("copy cert failed: %s\n%s\n", err.Error(), output))
|
||||||
|
} else {
|
||||||
|
isSuccess = true
|
||||||
|
outs.Write(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if distro == "deepin" {
|
||||||
|
confPath := "/etc/ca-certificates.conf"
|
||||||
|
checkCmd := []string{"grep", "-qxF", certName, confPath}
|
||||||
|
if _, err := s.runCommand(checkCmd, true); err != nil {
|
||||||
|
echoCmd := []string{"bash", "-c", fmt.Sprintf("echo '%s/%s' >> %s", appOnce.AppName, certName, confPath)}
|
||||||
|
if output, err := s.runCommand(echoCmd, true); err != nil {
|
||||||
|
errs.WriteString(fmt.Sprintf("append conf failed: %s\n%s\n", err.Error(), output))
|
||||||
|
} else {
|
||||||
|
isSuccess = true
|
||||||
|
outs.Write(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if output, err := s.runCommand(updateCmd, true); err != nil {
|
||||||
|
errs.WriteString(fmt.Sprintf("update failed: %s\n%s\n", err.Error(), output))
|
||||||
|
} else {
|
||||||
|
isSuccess = true
|
||||||
|
outs.Write(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSuccess {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return outs.String(), fmt.Errorf("certificate installation failed:\n%s", errs.String())
|
||||||
|
}
|
||||||
83
core/system_windows.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *SystemSetup) setProxy() error {
|
||||||
|
key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer key.Close()
|
||||||
|
|
||||||
|
err = key.SetStringValue("ProxyServer", "127.0.0.1:"+globalConfig.Port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = key.SetDWordValue("ProxyEnable", 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) unsetProxy() error {
|
||||||
|
key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer key.Close()
|
||||||
|
err = key.SetDWordValue("ProxyEnable", 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemSetup) installCert() (string, error) {
|
||||||
|
certData, err := s.initCert()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("installCert1:" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(certData)
|
||||||
|
if block == nil {
|
||||||
|
return "", errors.New("Failed to parse certificate PEM" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("installCert3:" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
rootStorePtr, err := windows.UTF16PtrFromString("ROOT")
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("installCert4:" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := windows.CertOpenStore(windows.CERT_STORE_PROV_SYSTEM, 0, 0, windows.CERT_SYSTEM_STORE_LOCAL_MACHINE, uintptr(unsafe.Pointer(rootStorePtr)))
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("installCert5:" + err.Error())
|
||||||
|
}
|
||||||
|
defer windows.CertCloseStore(store, 0)
|
||||||
|
|
||||||
|
certContext, err := windows.CertCreateCertificateContext(windows.X509_ASN_ENCODING|windows.PKCS_7_ASN_ENCODING, &cert.Raw[0], uint32(len(cert.Raw)))
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("installCert6:" + err.Error())
|
||||||
|
}
|
||||||
|
defer windows.CertFreeCertificateContext(certContext)
|
||||||
|
err = windows.CertAddCertificateContextToStore(store, certContext, windows.CERT_STORE_ADD_REPLACE_EXISTING, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("installCert7:" + err.Error())
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
14
core/utils.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DialogErr(message string) {
|
||||||
|
_, _ = runtime.MessageDialog(appOnce.ctx, runtime.MessageDialogOptions{
|
||||||
|
Type: runtime.ErrorDialog,
|
||||||
|
Title: "Error",
|
||||||
|
Message: message,
|
||||||
|
DefaultButton: "Cancel",
|
||||||
|
})
|
||||||
|
}
|
||||||
0
docs/.nojekyll
Normal file
12
docs/_coverpage.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/putyy/res-downloader"><img src="images/logo.png" width="120"/></a>
|
||||||
|
<h1><strong>res-downloader</strong></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> 全新技术栈,更新、更小、更快、更稳
|
||||||
|
|
||||||
|
### 简单、高效、轻便 (仅 ~10M)
|
||||||
|
|
||||||
|
|
||||||
|
[开始使用 Let Go](/readme.md)
|
||||||
|
[下载](/getting-started.md)
|
||||||
4
docs/_navbar.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
* [论坛](https://s.gowas.cn/d/4089)
|
||||||
|
* [反馈](https://github.com/putyy/res-downloader/issues)
|
||||||
|
* [日志](https://github.com/putyy/res-downloader/releases)
|
||||||
|
* [WX群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
|
||||||
6
docs/_sidebar.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
* [简介](readme.md)
|
||||||
|
* [快速开始](getting-started.md)
|
||||||
|
* [安装指南](installation.md)
|
||||||
|
* [功能演示](examples.md)
|
||||||
|
* [更多说明](more.md)
|
||||||
|
* [常见问题](troubleshooting.md)
|
||||||
19
docs/examples.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## 开启代理
|
||||||
|
- 安装完成后开启代理 (最新版本为“开启抓取”),如图:
|
||||||
|

|
||||||
|
|
||||||
|
## 拦截资源
|
||||||
|
### 视频号
|
||||||
|
- 打开视频号即可看到本软件中拦截到的资源
|
||||||
|

|
||||||
|
|
||||||
|
### 网页资源
|
||||||
|
- 浏览器打开或者其他软件内置浏览器打开的网页
|
||||||
|
- 这里演示打开百度这个网站:https://www.baidu.com/
|
||||||
|

|
||||||
|
|
||||||
|
### 小程序、公众号、抖音、小红书、qq音乐、酷狗等应用内资源获取方式都差不多!
|
||||||
|
|
||||||
|
## 下载资源到本地
|
||||||
|
- 选择你想下载的视频 点击下载即可
|
||||||
|

|
||||||
BIN
docs/favicon.ico
Normal file
|
After Width: | Height: | Size: 264 KiB |
18
docs/getting-started.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## 软件下载
|
||||||
|
🆕 [github下载](https://github.com/putyy/res-downloader/releases)
|
||||||
|
🆕 [蓝奏云下载 密码:9vs5](https://wwjv.lanzoum.com/b04wgtfyb)
|
||||||
|
|
||||||
|
!> Win7用户请使用2.3.0版本
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
- 安装时一定要同意安装证书文件、一定要允许网络访问
|
||||||
|
- 打开本软件(win系统首次使用管理员打开-鼠标右键选择管理员打开)
|
||||||
|
- 打开后,左上角点击 “启动代理”
|
||||||
|
- 打开要捕获的源, 如:视频号、网页、小程序等等
|
||||||
|
- 返回软件首页即可看到资源列
|
||||||
|
|
||||||
|
!> windows安装,先关闭所有安全管家之类的软件,安装完成后首次使用需右键管理员打开
|
||||||
|
|
||||||
|
!> Mac如果无法拦截 请关闭防火墙
|
||||||
|
|
||||||
|

|
||||||
BIN
docs/images/config.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/images/examples-1.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/images/examples-2.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
docs/images/examples-3.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/images/examples-4.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
docs/images/installation-mac-1.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
BIN
docs/images/more-1.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
docs/images/more-2.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
docs/images/more-3.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/images/more-4.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/images/show.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
50
docs/index.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>res-downloader</title>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="keywords" content="res-downloader,视频号下载,抖音下载,快手下载,小红书下载,万能下载器,爱享素材,爱享素材下载器">
|
||||||
|
<meta name="description" content="res-downloader是一款集网络资源嗅探 + 高速下载功能于一体的软件,高颜值、高性能和多样化,提供个人用户下载自己上传到各大平台的网络资源功能!">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script>
|
||||||
|
window.$docsify = {
|
||||||
|
name: 'res-downloader',
|
||||||
|
repo: 'https://github.com/putyy/res-downloader',
|
||||||
|
// 侧边栏支持,默认加载的是项目根目录下的_sidebar.md文件
|
||||||
|
loadSidebar: true,
|
||||||
|
// 导航栏支持,默认加载的是项目根目录下的_navbar.md文件
|
||||||
|
loadNavbar: true,
|
||||||
|
// 封面支持,默认加载的是项目根目录下的_coverpage.md文件
|
||||||
|
coverpage: true,
|
||||||
|
// 最大支持渲染的标题层级
|
||||||
|
maxLevel: 5,
|
||||||
|
// 自定义侧边栏后默认不会再生成目录,设置生成目录的最大层级(建议配置为2-4)
|
||||||
|
subMaxLevel: 4,
|
||||||
|
// 小屏设备下合并导航栏到侧边栏
|
||||||
|
mergeNavbar: true,
|
||||||
|
basePath: '/',
|
||||||
|
homepage: 'readme.md',
|
||||||
|
search: {
|
||||||
|
maxAge: 86400000,// 过期时间,单位毫秒,默认一天
|
||||||
|
paths: 'auto',// 注意:仅适用于 paths: 'auto' 模式
|
||||||
|
// 支持本地化
|
||||||
|
placeholder: '搜索',
|
||||||
|
noData: '找不到结果',
|
||||||
|
depth: 4,
|
||||||
|
hideOtherSidebarContent: false,
|
||||||
|
namespace: 'Docsify-Guide',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/emoji.min.js"></script>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/zoom-image.min.js"></script>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
docs/installation.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## 下载安装文件
|
||||||
|
- windows下载.exe结尾的,根据自己的系统架构下载合适的安装文件,通常下载带有“win_amd64.exe”或“x64-installer.exe”结尾的文件
|
||||||
|
- Mac下载.dmg结尾即可
|
||||||
|
- Linux根据系统类型下载对应的执行文件或安装文件
|
||||||
|
|
||||||
|
## Windows安装过程
|
||||||
|
- 双击下载好的exe 正常安装即可,首次打开记得右键管理员运行
|
||||||
|
|
||||||
|
## Mac安装过程
|
||||||
|
- 双击下载好的dmg文件,将res-downloader拖入应用即可,如图:
|
||||||
|

|
||||||
|
|
||||||
|
## Linux安装过程(自行替换掉对应的安装文件目录)
|
||||||
|
- ubuntu安装deb文件
|
||||||
|
> sudo apt install res-downloader_3.0.2_linux_x64.deb
|
||||||
|
|
||||||
|
- 执行文件运行方式
|
||||||
|
> chmod +x ./res-downloader_3.0.2_linux_x64
|
||||||
|
> sudo ./res-downloader_3.0.2_linux_x64
|
||||||
|
|
||||||
|
|
||||||
20
docs/more.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
## 清空列表、类型筛选
|
||||||
|
- 当资源列表过大时,无法快速找到需要的资源,这时可以先清空列表再去刷新需要的资源页面
|
||||||
|
- 资源列表过多,可以快速根据需要的资源类型进行筛选
|
||||||
|

|
||||||
|
|
||||||
|
## 拦截想要的资源类型、批量下载
|
||||||
|
- 比如只需要视频时就选择视频类型,可以多选
|
||||||
|

|
||||||
|
|
||||||
|
## 批量导出、批量导入使用场景
|
||||||
|
- 导出resd格式数据,将txt文件发送到另外的电脑,打开文件复制内容,使用批量导入导入到新电脑
|
||||||
|
|
||||||
|
## 复制链接、视频解密
|
||||||
|
- 复制链接可用于第三方软件进行下载,下载完成后对该视频解密,点击“视频解密”选择用其他软件下载完成后的视频文件进行解密
|
||||||
|

|
||||||
|
|
||||||
|
## 设置说明
|
||||||
|
!> 修改完成后记得点保存
|
||||||
|
- 几乎每项配置在软件中都有说明(鼠标悬浮在?处即可查看),此处就不进行多余讲解
|
||||||
|

|
||||||
63
docs/readme.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://github.com/putyy/res-downloader"><img src="images/logo.png" width="120"/></a>
|
||||||
|
<h1>res-downloader</h1>
|
||||||
|
<h4>📖 中文 | <a href="https://github.com/putyy/res-downloader/blob/master/README-EN.md">English</a></h4>
|
||||||
|
|
||||||
|
[](https://github.com/putyy/res-downloader/stargazers)
|
||||||
|
[](https://github.com/putyy/res-downloader/fork)
|
||||||
|
[](https://github.com/putyy/res-downloader/releases)
|
||||||
|

|
||||||
|
[](https://github.com/putyy/res-downloader/blob/master/LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎉 爱享素材下载器
|
||||||
|
|
||||||
|
> 一款基于 Go + [Wails](https://github.com/wailsapp/wails) 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。
|
||||||
|
|
||||||
|
## ✨ 功能特色
|
||||||
|
|
||||||
|
- 🚀 **简单易用**:操作简单,界面清晰美观
|
||||||
|
- 🖥️ **多平台支持**:Windows / macOS / Linux
|
||||||
|
- 🌐 **多资源类型支持**:视频 / 音频 / 图片 / m3u8 / 直播流等
|
||||||
|
- 📱 **平台兼容广泛**:支持微信视频号、小程序、抖音、快手、小红书、酷狗音乐、QQ音乐等
|
||||||
|
- 🌍 **代理抓包**:支持设置代理获取受限网络下的资源
|
||||||
|
|
||||||
|
## 📚 文档 & 版本
|
||||||
|
|
||||||
|
- 📘 [在线文档](https://res.putyy.com/)
|
||||||
|
- 💬 [加入交流群](https://www.putyy.com/app/admin/upload/img/20250418/6801d9554dc7.webp)
|
||||||
|
- 🧩 [最新版](https://github.com/putyy/res-downloader/releases) | [Mini版 使用默认浏览器展示UI](https://github.com/putyy/resd-mini) | [Electron旧版 支持Win7](https://github.com/putyy/res-downloader/tree/old)
|
||||||
|
> *群满时可加微信 `AmorousWorld`,请备注“来源”*
|
||||||
|
|
||||||
|
## 🧩 下载地址
|
||||||
|
|
||||||
|
- 🆕 [GitHub 下载](https://github.com/putyy/res-downloader/releases)
|
||||||
|
- 🆕 [蓝奏云下载(密码:9vs5)](https://wwjv.lanzoum.com/b04wgtfyb)
|
||||||
|
- ⚠️ *Win7 用户请下载 `2.3.0` 版本*
|
||||||
|
|
||||||
|
|
||||||
|
## 🖼️ 预览
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🧠 更多问题
|
||||||
|
|
||||||
|
- [GitHub Issues](https://github.com/putyy/res-downloader/issues)
|
||||||
|
- [爱享论坛讨论帖](https://s.gowas.cn/d/4089)
|
||||||
|
|
||||||
|
## 💡 实现原理 & 初衷
|
||||||
|
|
||||||
|
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 免责声明
|
||||||
|
|
||||||
|
> 本软件仅供学习与研究用途,禁止用于任何商业或违法用途。
|
||||||
|
如因此产生的任何法律责任,概与作者无关!
|
||||||
78
docs/troubleshooting.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
## 视频号拦截了一大堆 找不到想要的
|
||||||
|
> 设置里面关闭全量拦截,将视频转发好友后打开
|
||||||
|
|
||||||
|
## 某某网址拦截不了?
|
||||||
|
> 本软件并非万能的,所以有一些应用拦截不了很正常,实现原理 & 初衷如下,
|
||||||
|
```
|
||||||
|
本工具通过代理方式实现网络抓包,并筛选可用资源。与 Fiddler、Charles、浏览器 DevTools 原理类似,但对资源进行了更友好的筛选、展示和处理,大幅度降低了使用门槛,更适合大众用户使用。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 软件打不开了?之前可以打开
|
||||||
|
> 删除对应目录, 然后重启
|
||||||
|
```
|
||||||
|
## Mac终端执行
|
||||||
|
rm -rf /Users/$(whoami)/Library/Preferences/res-downloader
|
||||||
|
|
||||||
|
## Windows手动删除以下目录,Administrator为用户名 通常如下:
|
||||||
|
C:\Users\Administrator\AppData\Roaming\res-downloader
|
||||||
|
|
||||||
|
## Linux手动删除以下目录
|
||||||
|
/home/user/.config/res-downloader/home/user/.config/res-downloader
|
||||||
|
```
|
||||||
|
|
||||||
|
## 某应用只支持手机打开 如何拦截?
|
||||||
|
> 这里需要注意的是 应用使用http协议通讯才能拦截,且安卓7.0以上系统不再信任用户CA证书 所以没法拦截,解决方案自行查找,
|
||||||
|
```
|
||||||
|
1. 将手机和电脑处于同一个网络
|
||||||
|
2. 在手机端安装res-downloader的证书
|
||||||
|
3. 将手机网络代理设置为res-downloader的代理
|
||||||
|
4. 正常使用
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mac 提示“已损坏,无法打开”, 打开命令行执行如下命令:
|
||||||
|
> sudo xattr -d com.apple.quarantine /Applications/res-downloader.app
|
||||||
|
|
||||||
|
## 打开本软件,无法正常拦截获取
|
||||||
|
> 检查系统证书是否安装
|
||||||
|
> 关闭网络防火墙
|
||||||
|
> 系统代理是否正确设置(代理地址:127.0.0.1 端口:8899)
|
||||||
|
|
||||||
|
## 关闭软件后无法正常上网
|
||||||
|
> 手动关闭系统代理设置
|
||||||
|
|
||||||
|
## 链接不是私密链接
|
||||||
|
> 通常是证书未正确安装,最新版证书下载:软件左下角 ?点击后有下载地址
|
||||||
|
> 根据自己系统进行安装证书操作(不懂的自行百度),手动安装需安装到受信任的根证书
|
||||||
|
|
||||||
|
- Mac手动安装证书(V3+版本支持),打开终端复制以下命令 粘贴到终端回车 按照提示输入密码,完成后再打开软件:
|
||||||
|
```shell
|
||||||
|
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /Users/$(whoami)/Library/Preferences/res-downloader/cert.crt && touch /Users/$(whoami)/Library/Preferences/res-downloader/install.lock && echo "安装完成"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 拦截不到小程序中的资源
|
||||||
|
清理微信缓存,删除小程序后,重新打开
|
||||||
|
> 1.设置->存储空间->缓存
|
||||||
|
> 2.删除小程序相关缓存目录(自行搜索)
|
||||||
|
|
||||||
|
## 只拦截打开的视频号视频
|
||||||
|
关闭全量拦截,打开视频号视频详情,通常分享好友后打开的页面属于详情页
|
||||||
|
|
||||||
|
## 拦截视频号账号视频
|
||||||
|
打开对应作者个人主页,浏览即可
|
||||||
|
|
||||||
|
## 下载慢、大视频下载失败
|
||||||
|
推荐使用如下工具加速下载,视频号可以下载完成后再到对应视频操作项选择 “视频解密” 按钮
|
||||||
|
> [Neat Download Manager](https://www.neatdownloadmanager.com/index.php/en/)、[Motrix](https://motrix.app/download)等软件进行下载
|
||||||
|
|
||||||
|
## 直播流: 预览和录制:
|
||||||
|
> [使用obs进行预览和录制 使用教程自行百度, 点击下载obs]( https://obsproject.com/)
|
||||||
|
|
||||||
|
## m3u8: 预览和下载:
|
||||||
|
> [在线下载](https://m3u8-down.gowas.cn/)、[在线预览](https://m3u8play.com/)
|
||||||
|
|
||||||
|
## 安装证书后还会提示安装
|
||||||
|
使用命令行打开本软件,查看 “lockfile:” 这串字符后面的锁文件路径,然后创建该文件即可
|
||||||
|
例如 mac系统下终端执行如下命令即可创建
|
||||||
|
> touch /Users/你的用户名/Library/Preferences/res-downloader/install.lock
|
||||||
|
|
||||||
|
## 更多问题 请前往github进行[反馈](https://github.com/putyy/res-downloader/issues)
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* @see https://www.electron.build/configuration/configuration
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
|
|
||||||
"appId": "com.putyy.ResDownloader",
|
|
||||||
"asar": true,
|
|
||||||
"directories": {
|
|
||||||
"output": "release/${version}"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist-electron",
|
|
||||||
"dist",
|
|
||||||
"electron/res/**/*"
|
|
||||||
],
|
|
||||||
"mac": {
|
|
||||||
"icon": "electron/res/icon/icons/mac/icon.icns",
|
|
||||||
"artifactName": "${productName}_${version}.${ext}",
|
|
||||||
"target": [
|
|
||||||
"dmg"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"icon": "electron/res/icon/icons/win/icon.ico",
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "nsis",
|
|
||||||
"arch": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"artifactName": "${productName}_${version}.${ext}"
|
|
||||||
},
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"perMachine": false,
|
|
||||||
"allowElevation": true,
|
|
||||||
"allowToChangeInstallationDirectory": true,
|
|
||||||
"deleteAppDataOnUninstall": false
|
|
||||||
},
|
|
||||||
"extraResources": [
|
|
||||||
"electron/res"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
11
electron/electron-env.d.ts
vendored
@@ -1,11 +0,0 @@
|
|||||||
/// <reference types="vite-plugin-electron/electron-env" />
|
|
||||||
|
|
||||||
declare namespace NodeJS {
|
|
||||||
interface ProcessEnv {
|
|
||||||
VSCODE_DEBUG?: 'true'
|
|
||||||
DIST_ELECTRON: string
|
|
||||||
DIST: string
|
|
||||||
/** /dist/ or /public/ */
|
|
||||||
VITE_PUBLIC: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import CONFIG from './const'
|
|
||||||
import {mkdirp} from 'mkdirp'
|
|
||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import {clipboard, dialog} from 'electron'
|
|
||||||
import spawn from 'cross-spawn'
|
|
||||||
|
|
||||||
export function checkCertInstalled() {
|
|
||||||
return fs.existsSync(CONFIG.INSTALL_CERT_FLAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installCert(checkInstalled = true) {
|
|
||||||
if (checkInstalled && checkCertInstalled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mkdirp.sync(path.dirname(CONFIG.INSTALL_CERT_FLAG))
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
clipboard.writeText(
|
|
||||||
`echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CONFIG.CERT_PUBLIC_PATH}" && touch ${CONFIG.INSTALL_CERT_FLAG} && echo "安装完成"`,
|
|
||||||
)
|
|
||||||
dialog.showMessageBoxSync({
|
|
||||||
type: 'info',
|
|
||||||
message: `命令已复制到剪贴板,粘贴命令到终端并运行以安装并信任证书`,
|
|
||||||
});
|
|
||||||
|
|
||||||
reject()
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return new Promise((resolve: any, reject) => {
|
|
||||||
const result = spawn.sync(CONFIG.WIN_CERT_INSTALL_HELPER, [
|
|
||||||
'-c',
|
|
||||||
'-add',
|
|
||||||
CONFIG.CERT_PUBLIC_PATH,
|
|
||||||
'-s',
|
|
||||||
'root',
|
|
||||||
]);
|
|
||||||
if (result.stdout.toString().indexOf('Succeeded') > -1) {
|
|
||||||
fs.writeFileSync(CONFIG.INSTALL_CERT_FLAG, '')
|
|
||||||
resolve()
|
|
||||||
} else {
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import isDev from 'electron-is-dev'
|
|
||||||
import os from 'os'
|
|
||||||
import {app} from 'electron'
|
|
||||||
|
|
||||||
const APP_PATH = app.getAppPath();
|
|
||||||
// 对于一些 shell 去执行的文件,asar 目录下无法使用。配合 extraResources
|
|
||||||
const EXECUTABLE_PATH = path.join(
|
|
||||||
APP_PATH.indexOf('app.asar') > -1
|
|
||||||
? APP_PATH.substring(0, APP_PATH.indexOf('app.asar'))
|
|
||||||
: APP_PATH,
|
|
||||||
'electron/res',
|
|
||||||
)
|
|
||||||
|
|
||||||
const HOME_PATH = path.join(os.homedir(), '.res-downloader@putyy')
|
|
||||||
|
|
||||||
export default {
|
|
||||||
IS_DEV: isDev,
|
|
||||||
EXECUTABLE_PATH,
|
|
||||||
HOME_PATH,
|
|
||||||
CERT_PRIVATE_PATH: path.join(EXECUTABLE_PATH, './keys/private.pem'),
|
|
||||||
CERT_PUBLIC_PATH: path.join(EXECUTABLE_PATH, './keys/public.pem'),
|
|
||||||
INSTALL_CERT_FLAG: path.join(HOME_PATH, './installed.lock'),
|
|
||||||
WIN_CERT_INSTALL_HELPER: path.join(EXECUTABLE_PATH, './w_c.exe'),
|
|
||||||
APP_CN_NAME: '爱享素材下载器',
|
|
||||||
APP_EN_NAME: 'ResDownloader',
|
|
||||||
REGEDIT_VBS_PATH: path.join(EXECUTABLE_PATH, './regedit-vbs'),
|
|
||||||
OPEN_SSL_BIN_PATH: path.join(EXECUTABLE_PATH, './openssl/openssl.exe'),
|
|
||||||
OPEN_SSL_CNF_PATH: path.join(EXECUTABLE_PATH, './openssl/openssl.cnf'),
|
|
||||||
};
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import {app, BrowserWindow, shell, ipcMain, Menu} from 'electron'
|
|
||||||
import {release} from 'node:os'
|
|
||||||
import {join} from 'node:path'
|
|
||||||
import CONFIG from './const'
|
|
||||||
import initIPC, {setWin} from './ipc'
|
|
||||||
|
|
||||||
// The built directory structure
|
|
||||||
//
|
|
||||||
// ├─┬ dist-electron
|
|
||||||
// │ ├─┬ main
|
|
||||||
// │ │ └── index.js > Electron-Main
|
|
||||||
// │ └─┬ preload
|
|
||||||
// │ └── index.js > Preload-Scripts
|
|
||||||
// ├─┬ dist
|
|
||||||
// │ └── index.html > Electron-Renderer
|
|
||||||
//
|
|
||||||
process.env.DIST_ELECTRON = join(__dirname, '..')
|
|
||||||
process.env.DIST = join(process.env.DIST_ELECTRON, '../dist')
|
|
||||||
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
|
|
||||||
? join(process.env.DIST_ELECTRON, '../public')
|
|
||||||
: process.env.DIST
|
|
||||||
|
|
||||||
// Disable GPU Acceleration for Windows 7
|
|
||||||
if (release().startsWith('6.1')) app.disableHardwareAcceleration()
|
|
||||||
|
|
||||||
// Set application name for Windows 10+ notifications
|
|
||||||
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
|
||||||
|
|
||||||
if (!app.requestSingleInstanceLock()) {
|
|
||||||
app.quit()
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove electron security warnings
|
|
||||||
// This warning only shows in development mode
|
|
||||||
// Read more on https://www.electronjs.org/docs/latest/tutorial/security
|
|
||||||
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
|
|
||||||
|
|
||||||
app.commandLine.appendSwitch('--no-proxy-server')
|
|
||||||
process.on('uncaughtException', () => {
|
|
||||||
});
|
|
||||||
process.on('unhandledRejection', () => {
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
|
||||||
let previewWin: BrowserWindow | null = null
|
|
||||||
|
|
||||||
// Here, you can also use other preload
|
|
||||||
const preload = join(__dirname, '../preload/index.js')
|
|
||||||
const url = process.env.VITE_DEV_SERVER_URL
|
|
||||||
const indexHtml = join(process.env.DIST, 'index.html')
|
|
||||||
|
|
||||||
// app.whenReady().then(createWindow)
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
mainWindow = null
|
|
||||||
if (process.platform !== 'darwin') app.quit()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('second-instance', () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
// Focus on the main window if the user tried to open another
|
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
const allWindows = BrowserWindow.getAllWindows()
|
|
||||||
if (allWindows.length) {
|
|
||||||
allWindows[0].focus()
|
|
||||||
} else {
|
|
||||||
createWindow()
|
|
||||||
createPreviewWindow(mainWindow)
|
|
||||||
setWin(mainWindow, previewWin)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// New window example arg: new windows url
|
|
||||||
ipcMain.handle('open-win', (_, arg) => {
|
|
||||||
const childWindow = new BrowserWindow({
|
|
||||||
webPreferences: {
|
|
||||||
preload,
|
|
||||||
nodeIntegration: true,
|
|
||||||
contextIsolation: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (process.env.VITE_DEV_SERVER_URL) {
|
|
||||||
childWindow.loadURL(`${url}#${arg}`)
|
|
||||||
} else {
|
|
||||||
childWindow.loadFile(indexHtml, {hash: arg})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function createWindow() {
|
|
||||||
Menu.setApplicationMenu(null);
|
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
title: 'Main window',
|
|
||||||
icon: join(process.env.VITE_PUBLIC, 'favicon.ico'),
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
webPreferences: {
|
|
||||||
preload,
|
|
||||||
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
|
|
||||||
// Consider using contextBridge.exposeInMainWorld
|
|
||||||
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
|
|
||||||
nodeIntegration: true,
|
|
||||||
contextIsolation: false,
|
|
||||||
webSecurity: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298
|
|
||||||
mainWindow.loadURL(url).then(r => {
|
|
||||||
})
|
|
||||||
// Open devTool if the app is not packaged
|
|
||||||
// mainWindow.webContents.openDevTools()
|
|
||||||
} else {
|
|
||||||
mainWindow.loadFile(indexHtml).then(r => {
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG.IS_DEV && mainWindow.webContents.openDevTools()
|
|
||||||
|
|
||||||
// Test actively push message to the Electron-Renderer
|
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
|
||||||
mainWindow?.webContents.send('main-process-message', new Date().toLocaleString())
|
|
||||||
})
|
|
||||||
|
|
||||||
// Make all links open with the browser, not with the application
|
|
||||||
mainWindow.webContents.setWindowOpenHandler(({url}) => {
|
|
||||||
if (url.startsWith('https:')) shell.openExternal(url)
|
|
||||||
return {action: 'deny'}
|
|
||||||
})
|
|
||||||
// win.webContents.on('will-navigate', (event, url) => { }) #344
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPreviewWindow(parent: BrowserWindow) {
|
|
||||||
previewWin = new BrowserWindow({
|
|
||||||
parent: parent,
|
|
||||||
width: 600,
|
|
||||||
height: 400,
|
|
||||||
show: false,
|
|
||||||
// paintWhenInitiallyHidden: false,
|
|
||||||
webPreferences: {
|
|
||||||
webSecurity: false,
|
|
||||||
nodeIntegration: true,
|
|
||||||
contextIsolation: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// previewWin.hide()
|
|
||||||
previewWin.setTitle("预览")
|
|
||||||
|
|
||||||
previewWin.webContents.session.on('will-download', (event, item, webContents) => {
|
|
||||||
// console.log("取消下载")
|
|
||||||
item.cancel()
|
|
||||||
})
|
|
||||||
|
|
||||||
previewWin.on("page-title-updated", (event) => {
|
|
||||||
// 阻止该事件
|
|
||||||
event.preventDefault()
|
|
||||||
})
|
|
||||||
|
|
||||||
previewWin.on("close", (event) => {
|
|
||||||
// 不关闭窗口
|
|
||||||
event.preventDefault()
|
|
||||||
// 影藏窗口
|
|
||||||
previewWin.hide()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
initIPC()
|
|
||||||
createWindow()
|
|
||||||
createPreviewWindow(mainWindow)
|
|
||||||
setWin(mainWindow, previewWin)
|
|
||||||
})
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import {ipcMain, dialog, BrowserWindow, app, shell} from 'electron'
|
|
||||||
import {startServer} from './proxyServer'
|
|
||||||
import {installCert, checkCertInstalled} from './cert'
|
|
||||||
import {downloadFile, decodeWxFile} from './utils'
|
|
||||||
// @ts-ignore
|
|
||||||
import {hexMD5} from '../../src/common/md5'
|
|
||||||
import fs from "fs"
|
|
||||||
import CryptoJS from 'crypto-js'
|
|
||||||
import {closeProxy, setProxy} from "./setProxy"
|
|
||||||
import log from "electron-log"
|
|
||||||
import {floor} from "lodash";
|
|
||||||
|
|
||||||
let getMac = require("getmac").default
|
|
||||||
let win: BrowserWindow
|
|
||||||
let previewWin: BrowserWindow
|
|
||||||
let isStartProxy = false
|
|
||||||
let isOpenProxy = false
|
|
||||||
|
|
||||||
let aesKey = "as5d45as4d6qe6wqfar6gt4749q6y7w6h34v64tv7t37ty5qwtv6t6qv"
|
|
||||||
|
|
||||||
const suffix = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case "video/mp4":
|
|
||||||
return ".mp4";
|
|
||||||
case "image/png":
|
|
||||||
return ".png";
|
|
||||||
case "image/webp":
|
|
||||||
return ".webp";
|
|
||||||
case "image/svg+xml":
|
|
||||||
return ".svg";
|
|
||||||
case "image/gif":
|
|
||||||
return ".gif";
|
|
||||||
case "audio/mpeg":
|
|
||||||
return ".mp3";
|
|
||||||
case "application/vnd.apple.mpegurl":
|
|
||||||
return ".m3u8";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function initIPC() {
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_app_is_init', async (event, arg) => {
|
|
||||||
// 初始化应用 安装证书相关
|
|
||||||
return checkCertInstalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_init_app', (event, arg) => {
|
|
||||||
// 开始 初始化应用 安装证书相关
|
|
||||||
// console.log('invoke_init_app')
|
|
||||||
return installCert(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_start_proxy', async (event, arg) => {
|
|
||||||
// 启动代理服务
|
|
||||||
if (isStartProxy) {
|
|
||||||
if (isOpenProxy === false) {
|
|
||||||
isOpenProxy = true
|
|
||||||
setProxy('127.0.0.1', 8899)
|
|
||||||
.then(() => {
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isStartProxy = true
|
|
||||||
isOpenProxy = true
|
|
||||||
return startServer({
|
|
||||||
win: win,
|
|
||||||
setProxyErrorCallback: err => {
|
|
||||||
isStartProxy = false
|
|
||||||
isOpenProxy = false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_close_proxy', (event, arg) => {
|
|
||||||
// 关闭代理
|
|
||||||
try {
|
|
||||||
isOpenProxy = false
|
|
||||||
return closeProxy()
|
|
||||||
} catch (error) {
|
|
||||||
log.log("--------------closeProxy error--------------", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_select_down_dir', async (event, arg) => {
|
|
||||||
// 选择下载位置
|
|
||||||
const result = dialog.showOpenDialogSync({title: '保存', properties: ['openDirectory']})
|
|
||||||
if (!result?.[0]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return result?.[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_select_wx_file', async (event, {index, data}) => {
|
|
||||||
// 选择下载位置
|
|
||||||
const result = dialog.showOpenDialogSync({title: '保存', properties: ['openFile']})
|
|
||||||
if (!result?.[0]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return decodeWxFile(result?.[0], data.decode_key, result?.[0].replace(".mp4", "_解密.mp4"))
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_file_exists', async (event, {save_path, url}) => {
|
|
||||||
let url_sign = hexMD5(url)
|
|
||||||
let res = fs.existsSync(`${save_path}/${url_sign}.mp4`)
|
|
||||||
return {is_file: res, fileName: `${save_path}/${url_sign}.mp4`}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_down_file', async (event, {index, data, save_path, high}) => {
|
|
||||||
let down_url = data.down_url
|
|
||||||
if (high && data.high_url) {
|
|
||||||
down_url = data.high_url
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!down_url) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let url_sign = hexMD5(down_url)
|
|
||||||
let save_path_file = `${save_path}/${url_sign}` + suffix(data.type)
|
|
||||||
if (fs.existsSync(save_path_file)) {
|
|
||||||
return {fullFileName: save_path_file, totalLen: ""}
|
|
||||||
}
|
|
||||||
// 开始下载
|
|
||||||
return downloadFile(
|
|
||||||
down_url,
|
|
||||||
data.decode_key,
|
|
||||||
save_path_file,
|
|
||||||
(res) => {
|
|
||||||
win?.webContents.send('on_down_file_schedule', {schedule: floor(res * 100)})
|
|
||||||
}
|
|
||||||
).catch(err => {
|
|
||||||
// console.log('invoke_down_file:err', err)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_get_mac', async (event) => {
|
|
||||||
let mac = getMac()
|
|
||||||
if (mac === "") {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return CryptoJS.AES.encrypt(mac, CryptoJS.enc.Hex.parse(aesKey), {
|
|
||||||
mode: CryptoJS.mode.ECB,
|
|
||||||
padding: CryptoJS.pad.Pkcs7
|
|
||||||
}).ciphertext.toString()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_resources_preview', async (event, {url}) => {
|
|
||||||
if (!url) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
previewWin.loadURL(url).then(r => {
|
|
||||||
return
|
|
||||||
}).catch(res => {
|
|
||||||
})
|
|
||||||
previewWin.show()
|
|
||||||
return
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_open_default_browser', (event, {url}) => {
|
|
||||||
shell.openExternal(url).then(r => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('invoke_open_file_dir', (event, {save_path}) => {
|
|
||||||
shell.showItemInFolder(save_path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setWin(w, p) {
|
|
||||||
win = w
|
|
||||||
previewWin = p
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import log from 'electron-log'
|
|
||||||
import CONFIG from './const'
|
|
||||||
import {closeProxy, setProxy} from './setProxy'
|
|
||||||
import {app} from "electron"
|
|
||||||
import * as urlTool from "url"
|
|
||||||
import {toSize} from "./utils"
|
|
||||||
// @ts-ignore
|
|
||||||
import {hexMD5} from '../../src/common/md5'
|
|
||||||
|
|
||||||
const hoXy = require('hoxy')
|
|
||||||
|
|
||||||
const port = 8899
|
|
||||||
|
|
||||||
let videoList = {}
|
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
process.env.OPENSSL_BIN = CONFIG.OPEN_SSL_BIN_PATH
|
|
||||||
process.env.OPENSSL_CONF = CONFIG.OPEN_SSL_CNF_PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
// setTimeout to allow working in macOS
|
|
||||||
// in windows: H5ExtTransfer:ok
|
|
||||||
// in macOS: finderH5ExtTransfer:ok
|
|
||||||
|
|
||||||
const injection_script1 = `
|
|
||||||
setTimeout(() => {
|
|
||||||
let receiver_url = "https://res-downloader.666666.com";
|
|
||||||
|
|
||||||
function send_response_if_is_video(response) {
|
|
||||||
if (response == undefined) return;
|
|
||||||
if (!response["err_msg"].includes("H5ExtTransfer:ok")) return;
|
|
||||||
let value = JSON.parse(response["jsapi_resp"]["resp_json"]);
|
|
||||||
if (value["object"] == undefined || value["object"]["object_desc"] == undefined || value["object"]["object_desc"]["media"].length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let media = value["object"]["object_desc"]["media"][0];
|
|
||||||
let description = value["object"]["object_desc"]["description"].trim();
|
|
||||||
let video_data = {
|
|
||||||
"decode_key": media["decode_key"],
|
|
||||||
"url": media["url"]+media["url_token"],
|
|
||||||
"size": media["file_size"],
|
|
||||||
"description": description,
|
|
||||||
"uploader": value["object"]["nickname"]
|
|
||||||
};
|
|
||||||
fetch(receiver_url, {
|
|
||||||
method: "POST",
|
|
||||||
mode: "no-cors",
|
|
||||||
body: JSON.stringify(video_data),
|
|
||||||
}).then((resp) => {
|
|
||||||
// alert(\`video data for \${video_data["description"]} sent!\`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapper(name,origin) {
|
|
||||||
return function() {
|
|
||||||
let cmdName = arguments[0];
|
|
||||||
if (arguments.length == 3) {
|
|
||||||
let original_callback = arguments[2];
|
|
||||||
arguments[2] = async function () {
|
|
||||||
if (arguments.length == 1) {
|
|
||||||
send_response_if_is_video(arguments[0]);
|
|
||||||
}
|
|
||||||
return await original_callback.apply(this, arguments);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
let result = origin.apply(this,arguments);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.WeixinJSBridge.invoke = wrapper("WeixinJSBridge.invoke", window.WeixinJSBridge.invoke);
|
|
||||||
window.wvds = true;
|
|
||||||
}, 200);`;
|
|
||||||
|
|
||||||
export async function startServer({
|
|
||||||
win,
|
|
||||||
setProxyErrorCallback = f => f,
|
|
||||||
}) {
|
|
||||||
return new Promise(async (resolve: any, reject) => {
|
|
||||||
const proxy = hoXy.createServer({
|
|
||||||
certAuthority: {
|
|
||||||
key: fs.readFileSync(CONFIG.CERT_PRIVATE_PATH),
|
|
||||||
cert: fs.readFileSync(CONFIG.CERT_PUBLIC_PATH),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.listen(port, () => {
|
|
||||||
setProxy('127.0.0.1', port)
|
|
||||||
.then(() => {
|
|
||||||
// log.log("--------------setProxy success--------------")
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// log.log("--------------setProxy error--------------")
|
|
||||||
// setProxyErrorCallback(data);
|
|
||||||
setProxyErrorCallback({});
|
|
||||||
reject('设置代理失败');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on('error', err => {
|
|
||||||
log.log("--------------proxy err--------------", err)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
proxy.intercept(
|
|
||||||
{
|
|
||||||
phase: 'request',
|
|
||||||
hostname: 'res-downloader.666666.com',
|
|
||||||
as: 'json',
|
|
||||||
},
|
|
||||||
(req, res) => {
|
|
||||||
// console.log('req.json: ', req.json)
|
|
||||||
res.string = 'ok'
|
|
||||||
res.statusCode = 200
|
|
||||||
let url_sign: string = hexMD5(req.json.url)
|
|
||||||
let urlInfo = urlTool.parse(req.json.url, true)
|
|
||||||
win?.webContents?.send?.('on_get_queue', {
|
|
||||||
url_sign: url_sign,
|
|
||||||
url: req.json.url,
|
|
||||||
down_url: req.json.url,
|
|
||||||
high_url: '',
|
|
||||||
platform: urlInfo.hostname,
|
|
||||||
size: toSize(req.json.size ?? 0),
|
|
||||||
type: "video/mp4",
|
|
||||||
type_str: 'video',
|
|
||||||
progress_bar: '',
|
|
||||||
save_path: '',
|
|
||||||
downing: false,
|
|
||||||
decode_key: req.json.decode_key,
|
|
||||||
description: req.json.description,
|
|
||||||
uploader: '',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
proxy.intercept(
|
|
||||||
{
|
|
||||||
phase: 'response',
|
|
||||||
hostname: 'channels.weixin.qq.com',
|
|
||||||
as: 'string',
|
|
||||||
},
|
|
||||||
async (req, res) => {
|
|
||||||
// console.log('inject[channels.weixin.qq.com] req.url:', req.url);
|
|
||||||
if (req.url.includes('/web/pages/feed') || req.url.includes('/web/pages/home')) {
|
|
||||||
res.string = res.string.replace('</body>', '\n<script>' + injection_script1 + '</script>\n</body>');
|
|
||||||
res.statusCode = 200;
|
|
||||||
// console.log('inject[channels.weixin.qq.com]:', req.url, res.string.length);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
proxy.intercept(
|
|
||||||
{
|
|
||||||
phase: 'response',
|
|
||||||
},
|
|
||||||
async (req, res) => {
|
|
||||||
// 拦截响应
|
|
||||||
let ctype = res?._data?.headers?.['content-type']
|
|
||||||
let url_sign: string = hexMD5(req.fullUrl())
|
|
||||||
let res_url = req.fullUrl()
|
|
||||||
let urlInfo = urlTool.parse(res_url, true)
|
|
||||||
switch (ctype) {
|
|
||||||
case "video/mp4":
|
|
||||||
if (videoList.hasOwnProperty(url_sign) === false) {
|
|
||||||
videoList[url_sign] = req.fullUrl()
|
|
||||||
let high_url = ''
|
|
||||||
let down_url = res_url
|
|
||||||
// console.log('down_url', down_url)
|
|
||||||
win?.webContents?.send?.('on_get_queue', {
|
|
||||||
url_sign: url_sign,
|
|
||||||
url: down_url,
|
|
||||||
down_url: down_url,
|
|
||||||
high_url: high_url,
|
|
||||||
platform: urlInfo.hostname,
|
|
||||||
size: toSize(res?._data?.headers?.['content-length'] ?? 0),
|
|
||||||
type: ctype,
|
|
||||||
type_str: 'video',
|
|
||||||
progress_bar: '',
|
|
||||||
save_path: '',
|
|
||||||
downing: false,
|
|
||||||
decode_key: '',
|
|
||||||
description: '',
|
|
||||||
uploader: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "image/png":
|
|
||||||
case "image/webp":
|
|
||||||
case "image/svg+xml":
|
|
||||||
case "image/gif":
|
|
||||||
win?.webContents?.send?.('on_get_queue', {
|
|
||||||
url_sign: url_sign,
|
|
||||||
url: res_url,
|
|
||||||
down_url: res_url,
|
|
||||||
high_url: '',
|
|
||||||
platform: urlInfo.hostname,
|
|
||||||
size: toSize(res?._data?.headers?.['content-length'] ?? 0),
|
|
||||||
type: ctype,
|
|
||||||
type_str: 'image',
|
|
||||||
progress_bar: '',
|
|
||||||
save_path: '',
|
|
||||||
downing: false,
|
|
||||||
decode_key: '',
|
|
||||||
description: '',
|
|
||||||
uploader: '',
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
case "audio/mpeg":
|
|
||||||
win?.webContents?.send?.('on_get_queue', {
|
|
||||||
url_sign: url_sign,
|
|
||||||
url: res_url,
|
|
||||||
down_url: res_url,
|
|
||||||
high_url: '',
|
|
||||||
platform: urlInfo.hostname,
|
|
||||||
size: toSize(res?._data?.headers?.['content-length'] ?? 0),
|
|
||||||
type: ctype,
|
|
||||||
type_str: 'audio',
|
|
||||||
progress_bar: '',
|
|
||||||
save_path: '',
|
|
||||||
downing: false,
|
|
||||||
decode_key: '',
|
|
||||||
description: '',
|
|
||||||
uploader: '',
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
case "application/vnd.apple.mpegurl":
|
|
||||||
win.webContents?.send?.('on_get_queue', {
|
|
||||||
url_sign: url_sign,
|
|
||||||
url: res_url,
|
|
||||||
down_url: res_url,
|
|
||||||
high_url: '',
|
|
||||||
platform: urlInfo.hostname,
|
|
||||||
size: toSize(res?._data?.headers?.['content-length'] ?? 0),
|
|
||||||
type: ctype,
|
|
||||||
type_str: 'm3u8',
|
|
||||||
progress_bar: '',
|
|
||||||
save_path: '',
|
|
||||||
downing: false,
|
|
||||||
decode_key: '',
|
|
||||||
description: '',
|
|
||||||
uploader: '',
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('before-quit', async e => {
|
|
||||||
e.preventDefault()
|
|
||||||
try {
|
|
||||||
await closeProxy()
|
|
||||||
log.log("--------------closeProxy success--------------")
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
app.exit()
|
|
||||||
})
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import {exec} from 'child_process'
|
|
||||||
// @ts-ignore
|
|
||||||
import regedit from 'regedit'
|
|
||||||
import CONFIG from './const'
|
|
||||||
|
|
||||||
regedit.setExternalVBSLocation(CONFIG.REGEDIT_VBS_PATH)
|
|
||||||
|
|
||||||
export async function setProxy(host, port) {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
const networks = await getMacAvailableNetworks()
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
if (networks.length === 0) {
|
|
||||||
throw 'no network'
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
// @ts-ignore
|
|
||||||
networks.map(network => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
exec(`networksetup -setsecurewebproxy "${network}" ${host} ${port}`, error => {
|
|
||||||
if (error) {
|
|
||||||
reject(null)
|
|
||||||
} else {
|
|
||||||
resolve(network)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const valuesToPut = {
|
|
||||||
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings': {
|
|
||||||
ProxyServer: {
|
|
||||||
value: `${host}:${port}`,
|
|
||||||
type: 'REG_SZ',
|
|
||||||
},
|
|
||||||
ProxyEnable: {
|
|
||||||
value: 1,
|
|
||||||
type: 'REG_DWORD',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// @ts-ignore
|
|
||||||
return regedit.promisified.putValue(valuesToPut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function closeProxy() {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
const networks = await getMacAvailableNetworks()
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
if (networks.length === 0) {
|
|
||||||
throw 'no network'
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
// @ts-ignore
|
|
||||||
networks.map(network => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
exec(`networksetup -setsecurewebproxystate "${network}" off`, error => {
|
|
||||||
if (error) {
|
|
||||||
reject(null)
|
|
||||||
} else {
|
|
||||||
resolve(network)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const valuesToPut = {
|
|
||||||
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings': {
|
|
||||||
ProxyEnable: {
|
|
||||||
value: 0,
|
|
||||||
type: 'REG_DWORD',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// @ts-ignore
|
|
||||||
return regedit.promisified.putValue(valuesToPut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMacAvailableNetworks() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
exec('networksetup -listallnetworkservices', (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error)
|
|
||||||
} else {
|
|
||||||
Promise.all(
|
|
||||||
stdout
|
|
||||||
.toString()
|
|
||||||
.split('\n')
|
|
||||||
.map(network => {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
exec(
|
|
||||||
`networksetup getinfo "${network}" | grep "^IP address:\\s\\d"`,
|
|
||||||
(error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
resolve(null)
|
|
||||||
} else {
|
|
||||||
resolve(stdout ? network : null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
).then(networks => {
|
|
||||||
resolve(networks.filter(Boolean))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import {Transform } from 'stream'
|
|
||||||
import {getDecryptionArray} from '../wxjs/decrypt'
|
|
||||||
|
|
||||||
const axios = require('axios')
|
|
||||||
function xorTransform(decryptionArray) {
|
|
||||||
let processedBytes = 0;
|
|
||||||
return new Transform({
|
|
||||||
transform(chunk, encoding, callback) {
|
|
||||||
if (processedBytes < decryptionArray.length) {
|
|
||||||
let remaining = Math.min(decryptionArray.length - processedBytes, chunk.length);
|
|
||||||
for (let i = 0; i < remaining; i++) {
|
|
||||||
chunk[i] = chunk[i] ^ decryptionArray[processedBytes + i];
|
|
||||||
}
|
|
||||||
processedBytes += remaining;
|
|
||||||
}
|
|
||||||
this.push(chunk);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadFile(url, decodeKey, fullFileName, progressCallback) {
|
|
||||||
let xorStream = null
|
|
||||||
if (decodeKey) {
|
|
||||||
xorStream = xorTransform(getDecryptionArray(decodeKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
return axios.get(url, {
|
|
||||||
responseType: 'stream',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36',
|
|
||||||
},
|
|
||||||
}).then(({data, headers}) => {
|
|
||||||
let currentLen = 0
|
|
||||||
const totalLen = headers['content-length']
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
data.on('data', ({length}) => {
|
|
||||||
currentLen += length
|
|
||||||
// @ts-ignore
|
|
||||||
progressCallback?.(currentLen / totalLen)
|
|
||||||
});
|
|
||||||
|
|
||||||
data.on('error', err => reject(err))
|
|
||||||
if (xorStream) {
|
|
||||||
data.pipe(xorStream).pipe(
|
|
||||||
fs.createWriteStream(fullFileName).on('finish', () => {
|
|
||||||
resolve({
|
|
||||||
fullFileName,
|
|
||||||
totalLen,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
data.pipe(
|
|
||||||
fs.createWriteStream(fullFileName).on('finish', () => {
|
|
||||||
resolve({
|
|
||||||
fullFileName,
|
|
||||||
totalLen,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeWxFile(fileName, decodeKey, fullFileName) {
|
|
||||||
let xorStream = xorTransform(getDecryptionArray(decodeKey));
|
|
||||||
let data = fs.createReadStream(fileName);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
data.on('error', err => reject(err));
|
|
||||||
data.pipe(xorStream).pipe(
|
|
||||||
fs.createWriteStream(fullFileName).on('finish', () => {
|
|
||||||
resolve({
|
|
||||||
fullFileName,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toSize(size: number) {
|
|
||||||
if (size > 1048576) {
|
|
||||||
return (size / 1048576).toFixed(2) + "MB"
|
|
||||||
}
|
|
||||||
if (size > 1024) {
|
|
||||||
return (size / 1024).toFixed(2) + "KB"
|
|
||||||
}
|
|
||||||
return size + 'b'
|
|
||||||
}
|
|
||||||
|
|
||||||
export {downloadFile, toSize, decodeWxFile}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (condition.includes(document.readyState)) {
|
|
||||||
resolve(true)
|
|
||||||
} else {
|
|
||||||
document.addEventListener('readystatechange', () => {
|
|
||||||
if (condition.includes(document.readyState)) {
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const safeDOM = {
|
|
||||||
append(parent: HTMLElement, child: HTMLElement) {
|
|
||||||
if (!Array.from(parent.children).find(e => e === child)) {
|
|
||||||
return parent.appendChild(child)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
remove(parent: HTMLElement, child: HTMLElement) {
|
|
||||||
if (Array.from(parent.children).find(e => e === child)) {
|
|
||||||
return parent.removeChild(child)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://tobiasahlin.com/spinkit
|
|
||||||
* https://connoratherton.com/loaders
|
|
||||||
* https://projects.lukehaas.me/css-loaders
|
|
||||||
* https://matejkustec.github.io/SpinThatShit
|
|
||||||
*/
|
|
||||||
function useLoading() {
|
|
||||||
const className = `loaders-css__square-spin`
|
|
||||||
const styleContent = `
|
|
||||||
@keyframes square-spin {
|
|
||||||
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
|
|
||||||
50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
|
|
||||||
75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
|
|
||||||
100% { transform: perspective(100px) rotateX(0) rotateY(0); }
|
|
||||||
}
|
|
||||||
.${className} > div {
|
|
||||||
animation-fill-mode: both;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
background: #fff;
|
|
||||||
animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
|
|
||||||
}
|
|
||||||
.app-loading-wrap {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #282c34;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const oStyle = document.createElement('style')
|
|
||||||
const oDiv = document.createElement('div')
|
|
||||||
|
|
||||||
oStyle.id = 'app-loading-style'
|
|
||||||
oStyle.innerHTML = styleContent
|
|
||||||
oDiv.className = 'app-loading-wrap'
|
|
||||||
oDiv.innerHTML = `<div class="${className}"><div></div></div>`
|
|
||||||
|
|
||||||
return {
|
|
||||||
appendLoading() {
|
|
||||||
safeDOM.append(document.head, oStyle)
|
|
||||||
safeDOM.append(document.body, oDiv)
|
|
||||||
},
|
|
||||||
removeLoading() {
|
|
||||||
safeDOM.remove(document.head, oStyle)
|
|
||||||
safeDOM.remove(document.body, oDiv)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
const { appendLoading, removeLoading } = useLoading()
|
|
||||||
domReady().then(appendLoading)
|
|
||||||
|
|
||||||
window.onmessage = (ev) => {
|
|
||||||
ev.data.payload === 'removeLoading' && removeLoading()
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(removeLoading, 4999)
|
|
||||||
|
Before Width: | Height: | Size: 199 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>爱享素材下载器</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEogIBAAKCAQEAsmAqn3hYd/YZcrfgqM1Q6xgHI50EBckbOkfCqTWS1yVFZjLF
|
|
||||||
bMehWb9xGFZJD21A5sxl4xelIWblhety+YTVa/mn2CEJh3je069oeULfXdzhhHyf
|
|
||||||
/ci0IloJhvX+2RJ+176uTKKcWhuOtNVs5VeFoHDoUcISnTqkaVyWeeLfafgrOW7w
|
|
||||||
N8ip128nuBx19ylIygb/DELmjKRRCSpx2vOw2JErTM8L5r0f4eWdqiwBOwu0NHWy
|
|
||||||
Svh9YG8B31UPga4I8FbFhybOP9cQNQPafOSfjwuZoi5CAtyJbwT7KyII9iMD74bZ
|
|
||||||
1mTx2xokmQ2TeiCSKSF8Mx9/8Gq+95mzvvIbRwIDAQABAoIBAHNt++caj9WBclJk
|
|
||||||
X4Oc6eJYuDX5o+LCk1YRngy12IJVYiWScWPFg8p6MouXOsw63Sb92mksofWNirYw
|
|
||||||
+UQzC5FGC7G3H12FgFzoQ+lEtxscluuPYlFukfMw5L1rbzG14FNo145MJHXDI4Qu
|
|
||||||
ILwA+T4sEorl1fndOwvbmJzjjcQaeRNz7/R9e6QTOlZ2+IEMKnHSBXXGJbDj6mPN
|
|
||||||
+f1/ec6nVENdxazgRCi0xfinyft4Ipst93Eb+wGcpk+J43aF+0leWQCdl6Y9U1Lz
|
|
||||||
zpv5H5XOQdwpX+dpuioRp73zwPwIialq+hTUN28Bn9U1jW2tjxUl/vgIpjy1s94a
|
|
||||||
UipRwSECgYEA57vYB+wGnxQxY9IPpr9H/y3HciIwCnuOEsWBzjYe8sIqBif2tEpO
|
|
||||||
OgDZZMQY7+JJrDQbDRs442TuRjKhJ5hiW+MyoiFWaYkBBoNVM8RBTkIjHfrh+uB2
|
|
||||||
XT15FbEyyxo3n9QY610ZJFRnW4Uf5V0osjOqqUgQRrVXvamk6NQH6FkCgYEAxQ3v
|
|
||||||
jFYPL3EkZe1br6X0RM42ykGv5Di5Q6NnjpSPcyn9a2obA1cZuCd5S1lhrkuZGsdI
|
|
||||||
iFapeL+7vpts9gu9/ii9y+CgEKplOMmm0ZrChBKAcXMZvdDKV3y5SmTMZPas4X5i
|
|
||||||
hqNqatx9/J93sMYWc0CuoosDEJYKtSz8GE+1rJ8CgYAmp5rdl21zU7b5Y6zgr7+e
|
|
||||||
vVArpbBFz15fmzqP309CR0kjRb9NS6fI3SNmP5+5RBHt+7MXeJcAt3FXnFJtfGnL
|
|
||||||
0hY8HTuA1y2onHe17uLF3xpkgdj4NEEKRJrSF4DViEYHDyYo/JqZCMtE5OvxIp0L
|
|
||||||
PLsXCcJNSSqdpJKxk8zN4QKBgEuoxSAh7uStUWddUkXHt1kvwDO6MtmyuddxhxJk
|
|
||||||
kguKxMWYUNTgfXyKk3TN1caBOkDg4UWP2LQHEgPmU1jJO2K5q9362hpsAj9ilY2H
|
|
||||||
GUZygCSPKAQMhZQ/zDj3KM9fMxPFXfkKB5MOI8V6SQ9zjy0jWaoJK90TbvsPUZ/Y
|
|
||||||
Aw5LAoGADifZwCHPiXhTfJjOom2uBgXmL03yTXcCw4EDIX3ZR0sP6ACPQq4T4jxZ
|
|
||||||
UJLXLjOb2pzCq0c5+k0cG6ahYINq4tGOo+vQ9fDvhKg0nlf1FrzxSd7S12o+un2q
|
|
||||||
+U+dBllYIDlRMgMhXu9CxFDjUsCwPRmsBvmVZiH4XSs6QVnfn90=
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEogIBAAKCAQEAsmAqn3hYd/YZcrfgqM1Q6xgHI50EBckbOkfCqTWS1yVFZjLF
|
|
||||||
bMehWb9xGFZJD21A5sxl4xelIWblhety+YTVa/mn2CEJh3je069oeULfXdzhhHyf
|
|
||||||
/ci0IloJhvX+2RJ+176uTKKcWhuOtNVs5VeFoHDoUcISnTqkaVyWeeLfafgrOW7w
|
|
||||||
N8ip128nuBx19ylIygb/DELmjKRRCSpx2vOw2JErTM8L5r0f4eWdqiwBOwu0NHWy
|
|
||||||
Svh9YG8B31UPga4I8FbFhybOP9cQNQPafOSfjwuZoi5CAtyJbwT7KyII9iMD74bZ
|
|
||||||
1mTx2xokmQ2TeiCSKSF8Mx9/8Gq+95mzvvIbRwIDAQABAoIBAHNt++caj9WBclJk
|
|
||||||
X4Oc6eJYuDX5o+LCk1YRngy12IJVYiWScWPFg8p6MouXOsw63Sb92mksofWNirYw
|
|
||||||
+UQzC5FGC7G3H12FgFzoQ+lEtxscluuPYlFukfMw5L1rbzG14FNo145MJHXDI4Qu
|
|
||||||
ILwA+T4sEorl1fndOwvbmJzjjcQaeRNz7/R9e6QTOlZ2+IEMKnHSBXXGJbDj6mPN
|
|
||||||
+f1/ec6nVENdxazgRCi0xfinyft4Ipst93Eb+wGcpk+J43aF+0leWQCdl6Y9U1Lz
|
|
||||||
zpv5H5XOQdwpX+dpuioRp73zwPwIialq+hTUN28Bn9U1jW2tjxUl/vgIpjy1s94a
|
|
||||||
UipRwSECgYEA57vYB+wGnxQxY9IPpr9H/y3HciIwCnuOEsWBzjYe8sIqBif2tEpO
|
|
||||||
OgDZZMQY7+JJrDQbDRs442TuRjKhJ5hiW+MyoiFWaYkBBoNVM8RBTkIjHfrh+uB2
|
|
||||||
XT15FbEyyxo3n9QY610ZJFRnW4Uf5V0osjOqqUgQRrVXvamk6NQH6FkCgYEAxQ3v
|
|
||||||
jFYPL3EkZe1br6X0RM42ykGv5Di5Q6NnjpSPcyn9a2obA1cZuCd5S1lhrkuZGsdI
|
|
||||||
iFapeL+7vpts9gu9/ii9y+CgEKplOMmm0ZrChBKAcXMZvdDKV3y5SmTMZPas4X5i
|
|
||||||
hqNqatx9/J93sMYWc0CuoosDEJYKtSz8GE+1rJ8CgYAmp5rdl21zU7b5Y6zgr7+e
|
|
||||||
vVArpbBFz15fmzqP309CR0kjRb9NS6fI3SNmP5+5RBHt+7MXeJcAt3FXnFJtfGnL
|
|
||||||
0hY8HTuA1y2onHe17uLF3xpkgdj4NEEKRJrSF4DViEYHDyYo/JqZCMtE5OvxIp0L
|
|
||||||
PLsXCcJNSSqdpJKxk8zN4QKBgEuoxSAh7uStUWddUkXHt1kvwDO6MtmyuddxhxJk
|
|
||||||
kguKxMWYUNTgfXyKk3TN1caBOkDg4UWP2LQHEgPmU1jJO2K5q9362hpsAj9ilY2H
|
|
||||||
GUZygCSPKAQMhZQ/zDj3KM9fMxPFXfkKB5MOI8V6SQ9zjy0jWaoJK90TbvsPUZ/Y
|
|
||||||
Aw5LAoGADifZwCHPiXhTfJjOom2uBgXmL03yTXcCw4EDIX3ZR0sP6ACPQq4T4jxZ
|
|
||||||
UJLXLjOb2pzCq0c5+k0cG6ahYINq4tGOo+vQ9fDvhKg0nlf1FrzxSd7S12o+un2q
|
|
||||||
+U+dBllYIDlRMgMhXu9CxFDjUsCwPRmsBvmVZiH4XSs6QVnfn90=
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICuDCCAaACCQC7PQmrxgWOlTANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJs
|
|
||||||
ZWNlcGluLTIwMjItMDUtMTkwIBcNMjIwNTE5MTI1NjA0WhgPMzAyMTA5MTkxMjU2
|
|
||||||
MDRaMB0xGzAZBgNVBAMMEmxlY2VwaW4tMjAyMi0wNS0xOTCCASIwDQYJKoZIhvcN
|
|
||||||
AQEBBQADggEPADCCAQoCggEBALJgKp94WHf2GXK34KjNUOsYByOdBAXJGzpHwqk1
|
|
||||||
ktclRWYyxWzHoVm/cRhWSQ9tQObMZeMXpSFm5YXrcvmE1Wv5p9ghCYd43tOvaHlC
|
|
||||||
313c4YR8n/3ItCJaCYb1/tkSfte+rkyinFobjrTVbOVXhaBw6FHCEp06pGlclnni
|
|
||||||
32n4Kzlu8DfIqddvJ7gcdfcpSMoG/wxC5oykUQkqcdrzsNiRK0zPC+a9H+Hlnaos
|
|
||||||
ATsLtDR1skr4fWBvAd9VD4GuCPBWxYcmzj/XEDUD2nzkn48LmaIuQgLciW8E+ysi
|
|
||||||
CPYjA++G2dZk8dsaJJkNk3ogkikhfDMff/BqvveZs77yG0cCAwEAATANBgkqhkiG
|
|
||||||
9w0BAQsFAAOCAQEADymHk+wLJAdv3p+4hHo57VLaBtwVYXc5oRUbUzgMYTTtPWIs
|
|
||||||
xuILEqXftMspt6PzdEt0V1WeCWNyypsAbur/CKpAOoVjBDPIo09TiYnYIn9xt5wQ
|
|
||||||
AmR5kVEZheuazcvzW3C9NAY1T6QDmxNvFCiCXRbtklOg2HqFDZX+pkj8CylQ9TDk
|
|
||||||
rroUg17b/FD1ds1uyPXzucEWfxqkOaujvsCnzrbFs9luB5VfM+QzLU+l9QRN9Tmj
|
|
||||||
z7CpGuP6vKvhXJLUjXkZ0q5JyL5wEAe6Ttbu+c/8HhPFKQsW6q/lQSDo0v0LGDrd
|
|
||||||
ikjWXhSrVjd8+qTTVgia/UNqv/wi+bkWnVdRzQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||