.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Teaching-Material-Download-Manager
6 |
7 |
8 | 优雅地下载电子教材
9 |
10 |
11 | for Ver.1.1.0_202308281300
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 源代码: GitHub 仓库 | Gitee 仓库
26 |
27 |
28 | 下载地址: GitHub release | Gitee release | 123网盘
29 |
30 |
31 | 如果你的设备不受支持, 可以查看下载原理自行获取教材下载链接!
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ## 食用方法🍕
40 |
41 | ### 想开盖即食 ?
42 |
43 | 从 [Github release](https://github.com/RicardoJackMC/Teaching-Material-Download-Manager/releases) 或 [Gitee release](https://gitee.com/RicardoJackMC/Teaching-Material-Download-Manager/releases) 页面选择最新版本, 点击`Teaching-Material-Download-Manager.zip`, 或者前往 [123网盘](https://www.123pan.com/s/Y59qVv-uuubd.html) 选择最新版本的文件夹, 下载`Teaching-Material-Download-Manager.zip`, 下载完成后解压, 双击`main.exe`即可使用. 具体操作可以去B站看[演示视频](https://www.bilibili.com/video/BV1xH4y1Q7aM/)
44 |
45 | ### 想食用源码 ?
46 |
47 | 下载完源码后解压, (有需要的记得先激活虚拟环境) 在 cmd 中用`cd`转到`main.py`所在的目录, 然后运行
48 |
49 | ```
50 | pip install -r requirements.txt
51 | ```
52 |
53 | 然后, ENJOY YOURSELF ! ! !
54 |
55 | > **Note**
56 | > 本软件的开发环境如下
57 | >
58 | > Python 3.9.13
59 | >
60 | > PyQt5 5.15.9
61 | >
62 | > requests 2.31.0
63 | >
64 | > PyQt-Fluent-Widgets 1.1.9
65 |
66 | ## 敏感行为🛡️
67 |
68 | 本软件的某些行为可能会被杀毒软件识别为危险行为, 下表列出了程序的敏感行为
69 |
70 | | 行为 | 触发方式 | 具体描述 |
71 | | ------------------------------------------------------------ | :----------------------------------------------------------- | ------------------------------------------------------------ |
72 | | 对与 main.exe 同个目录下的的 config.json 进行读取与写入 | 当软件第一次启动时, 或当用户更改任意设置使自动触发 | 位于与 main.exe 同一目录下的 config.json 为本软件的配置文件, 上面记载了用户对软件的设置, 例如: 是否开启队列, 是否开启自动下载等. |
73 | | 对与 main.exe 同个目录下的的 URL.json 进行读取 | 当软件启动时 | 与 main.exe 同个目录下的的 URL.json 记载了多个智慧教育网站的 api (网址), 用于生成最终的下载链接 |
74 | | 将 JSON 文件保存至用户指定的位置 | 当用户点击“导出下载日志按钮时”在用户选定保存位置后触发 | 被保存的 JSON 文件为软件的下载日志, 上面记载了下载时的信息, 例如: 未能获取要保存的教材 pdf 文件标题的原因, 下载失败文件的链接等. 用户可以自行决定是否生成 JSON 文件, JSON 文件的保存位置, 以及是否将 JSON 文件自行发送给开发者 |
75 | | 与多个智慧教育平台 api (网址)通讯 | 当处理任意任务时, 自动触发, 使用到 api (网址)详见[下载原理](#pt1-下载原理) | 软件需要与多个网站通讯才可获得 ID_B, 教材 pdf 文件的标题, 教材下载链接及下载教材, 具体通讯的网站可查看[下载原理](#pt1-下载原理) |
76 | | 与 https://api.github.com/repos/RicardoJackMC/Teaching-Material-Download-Manager/releases/latest 和 https://gitee.com/api/v5/repos/RicardoJackMC/Teaching-Material-Download-Manager/releases/latest 通讯 | 当用户点击“检查更新”时触发 | 这两个链接为 GitHub 和 Gitee 的 api, 软件通过此 api 获取版本号判断是否有新版本 |
77 | | 读取注册表: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize 下 AppsUseLightTheme 的值 | 每次当软件启动时自动触发 | 判断软件是否应启用暗黑模式使其与系统应用的外观同步 |
78 | | 读取注册表: HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM 下 AccentColor 的值 | 每次当软件启动时自动触发 | 设置软件的主题色使其与系统应用同步 |
79 |
80 | ## 软件原理📒
81 |
82 | ### Pt.1 下载原理
83 |
84 | #### · 基础
85 |
86 | 研究发现教材详情页网址的组成如下
87 |
88 | ```url
89 | https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&contentId={id}&catalogType=tchMaterial&subCatalog=tchMaterial
90 | ```
91 |
92 | 我们把 {id} 的值记为 ID_A
93 |
94 | 可以发现使用 ID_A 的值替换下列网址中的 {ID_A} 后形成的新网址即为 pdf 教材教材的下载链接:
95 |
96 | | 使用ID_A的下载链接 |
97 | | ------------------------------------------------------------ |
98 | | https://r1-ndr.ykt.cbern.com.cn/edu_product/esp/assets_document/{ID_A}.pkg/pdf.pdf |
99 | | https://r2-ndr.ykt.cbern.com.cn/edu_product/esp/assets_document/{ID_A}.pkg/pdf.pdf |
100 | | https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets_document/{ID_A}.pkg/pdf.pdf |
101 | | https://c1.ykt.cbern.com.cn/edu_product/esp/assets_document/{ID_A}.pkg/pdf.pdf |
102 | | https://r1-ndr.ykt.cbern.com.cn/edu_product/esp/assets/{ID_A}.pkg/pdf.pdf |
103 | | https://r2-ndr.ykt.cbern.com.cn/edu_product/esp/assets/{ID_A}.pkg/pdf.pdf |
104 | | https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets/{ID_A}.pkg/pdf.pdf |
105 |
106 | 例如:
107 |
108 | ```url
109 | https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&contentId=b8e9a3fe-dae7-49c0-86cb-d146f883fd8e&catalogType=tchMaterial&subCatalog=tchMaterial
110 | ```
111 |
112 | 其中 ID_A 的值为:
113 |
114 | ```ID_A
115 | b8e9a3fe-dae7-49c0-86cb-d146f883fd8e
116 | ```
117 |
118 | 则该教材有以下下载链接:
119 |
120 | | 示例下载链接 |
121 | | ------------------------------------------------------------ |
122 | | https://r1-ndr.ykt.cbern.com.cn/edu_product/esp/assets_document/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.pkg/pdf.pdf |
123 | | https://r2-ndr.ykt.cbern.com.cn/edu_product/esp/assets_document/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.pkg/pdf.pdf |
124 | | https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets_document/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.pkg/pdf.pdf |
125 | | https://c1.ykt.cbern.com.cn/edu_product/esp/assets_document/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.pkg/pdf.pdf |
126 | | * https://r1-ndr.ykt.cbern.com.cn/edu_product/esp/assets/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.pkg/pdf.pdf |
127 | | * https://r2-ndr.ykt.cbern.com.cn/edu_product/esp/assets/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.pkg/pdf.pdf |
128 | | * https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.pkg/pdf.pdf |
129 |
130 | 至此, 该教材已有 4 个下载链接.
131 |
132 | > **Note**
133 | > 在上述例子中, 前面带星号的下载链接已经不可用, 但是在下载 https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&contentId=2600a906-afca-43bc-9070-5d962b92c85c&catalogType=tchMaterial&subCatalog=tchMaterial 时, 前面不带星号的下载链接不可用, 但是带星号的下载链接可以使用, 固本软件仍保留带星号的下载链接
134 |
135 | #### · 高阶
136 |
137 | 使用 ID_A 的值替换下列网址中的 {ID_A} 后形成的新网址为 JSON 文件:
138 |
139 | | JSON文件链接 |
140 | | ------------------------------------------------------------ |
141 | | https://s-file-3.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/{ID_A}.json |
142 | | https://s-file-2.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/{ID_A}.json |
143 | | https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/{ID_A}.json |
144 |
145 | 此时, 可以在此 JSON 中获得教材的标题, 版本和 ID_B, 其中, 如果将 JSON 文件保存为一个名为 json_data 的 Python 字典, 则可以通过:
146 |
147 | ```python
148 | json_data["title"] # 获取教材标题
149 | json_data['tag_list'][2]['tag_name'] # 获取教材版本
150 | json_data['custom_properties']['thumbnails'][0] # 获取教材的图片, 我们把它记为url_B, 我们会用url_B获取ID_B
151 | ```
152 |
153 | 其中, url_B 组成通常如下:
154 |
155 | ```url
156 | https://r3-ndr.ykt.cbern.com.cn/edu_product/65/document/{id}/image/1.jpg
157 | ```
158 |
159 | 我们把上述网址 {id} 的值记为 ID_B
160 |
161 | > **Note**
162 | > 这仅仅只是其中的一种获取 ID_B 的方法, 据开发者所知还有其他更稳定的方法可以获取 ID_B
163 |
164 | 可以发现使用 ID_B 的值替换下列网址中的 {ID_B} 后形成的新网址即为 pdf 教材教材的下载链接:
165 |
166 | | 使用 ID_B 的下载链接 |
167 | | ------------------------------------------------------------ |
168 | | https://v1.ykt.cbern.com.cn/65/document/{ID_B}/pdf.pdf |
169 | | https://v2.ykt.cbern.com.cn/65/document/{ID_B}/pdf.pdf |
170 | | https://v3.ykt.cbern.com.cn/65/document/{ID_B}/pdf.pdf |
171 | | https://r1-ndr.ykt.cbern.com.cn/edu_product/65/document/{ID_B}/pdf.pdf |
172 | | https://r2-ndr.ykt.cbern.com.cn/edu_product/65/document/{ID_B}/pdf.pdf |
173 | | https://r3-ndr.ykt.cbern.com.cn/edu_product/65/document/{ID_B}/pdf.pdf |
174 |
175 | > **Warning**
176 | > 通过 JSON 获取的 ID_B, 教材标题, 教材版本出错的可能性较大, 不同的教材对应的 JSON 文件基本都具有差异, 尽管差异很小, 也会造成程序无法正确获取 ID_B, 教材标题, 教材版本.
177 |
178 | 仍然以[基础](#-基础)中使用的教材为例子, 可从以下链接获取 JSON 文件
179 |
180 | | JSON文件链接 |
181 | | ------------------------------------------------------------ |
182 | | https://s-file-3.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.json |
183 | | https://s-file-2.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.json |
184 | | https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/b8e9a3fe-dae7-49c0-86cb-d146f883fd8e.json |
185 |
186 | 讲 JSON 文件保存为名为 json_data 的 Python 字典, 则
187 |
188 | ```py
189 | print(json_data["title"])
190 | # 输出 普通高中教科书·语文必修 上册
191 | print(json_data['tag_list'][2]['tag_name'])
192 | # 输出 统编版
193 | print(json_data['custom_properties']['thumbnails'][0])
194 | # 输出 https://r1-ndr.ykt.cbern.com.cn/edu_product/65/document/7a69755810bb492c9e44f94a213b7e5e/image/1.jpg
195 | ```
196 |
197 | 通过 url_B 获取 ID_B 为
198 |
199 | ```ID_B
200 | 7a69755810bb492c9e44f94a213b7e5e
201 | ```
202 |
203 | 则可获得以下下载链接:
204 |
205 | | 示例下载链接 |
206 | | ------------------------------------------------------------ |
207 | | https://v1.ykt.cbern.com.cn/65/document/7a69755810bb492c9e44f94a213b7e5e/pdf.pdf |
208 | | https://v2.ykt.cbern.com.cn/65/document/7a69755810bb492c9e44f94a213b7e5e/pdf.pdf |
209 | | https://v3.ykt.cbern.com.cn/65/document/7a69755810bb492c9e44f94a213b7e5e/pdf.pdf |
210 | | https://r1-ndr.ykt.cbern.com.cn/edu_product/65/document/7a69755810bb492c9e44f94a213b7e5e/pdf.pdf |
211 | | https://r2-ndr.ykt.cbern.com.cn/edu_product/65/document/7a69755810bb492c9e44f94a213b7e5e/pdf.pdf |
212 | | https://r3-ndr.ykt.cbern.com.cn/edu_product/65/document/7a69755810bb492c9e44f94a213b7e5e/pdf.pdf |
213 |
214 | 综上所述, 一个教材目前最多有 13 个下载链接.
215 |
216 | #### · 总结
217 |
218 | 一个教材最多有 13 个下载链接, 若您的设备不支持本软件, 建议使用[基础](#-基础)中的操作自行获取下载链接, 同时, 建议使用程序自动化完成[高阶](#-高阶)中的操作获取下载链接. 如果发现下载链接失效了或发现新的下载链接, 请发邮件到 ricardojackmc@gmail.com 告诉作者, 谢谢啦 !
219 |
220 | ### Pt.2 运行原理
221 |
222 | 本软件使用多进程 (multiprocessing) + 多线程 (QThread) 的方式运行
223 |
224 | 主进程 UI_Process 负责 UI 界面的刷新, 另有下载进程 Manager_Process 专门负责下载功能
225 |
226 | 使用 multiprocessing 中的 Queue 实现进程间的通讯, 其中 queue 负责向 UI_Process 传递下载进度, 下载状态等基本信息, queue_admin 负责向 UI_Process 传递结束指令, 下载链接, queue_command 负责向 Manager_Process 传递下载的配置项以及关闭指令
227 |
228 | 主进程下又有子线程 normal_info, admin_info 专门监听 queue 和 queue_admin, 另外还有子线程 update_func 专门负责检查软件更新.
229 |
230 | ## 软件前瞻 (前方大型画饼现场)🍪
231 |
232 | 此外, 作者正在研究 智慧教育平台的课程的下载方法, 以及国家智慧教育读书平台, Library Genesis, Sci-Hub, Z-Library 等网站的下载方法.
233 |
234 | 可能会在下个版本支持 智慧教育平台课程 和 国家智慧教育读书平台 的下载.
235 |
236 | (要开学啦, 开学后作者不一定更新, 可能要寒假才可以更新😥)
237 |
238 | ## 许可证🏛️
239 |
240 | Teaching-Material-Download-Manager 使用 [GPLv3](https://github.com/RicardoJackMC/Teaching-Material-Download-Manager/blob/main/LICENSE) 许可证.
241 |
242 | Copyright 2023 by RicardoJackMC
243 |
244 | > **Note**
245 | > 如果您是在阅读[软件原理](#软件原理)或软件源码后自行编写程序然后分发, 则您的程序可以不必使用使用 [GPLv3](https://github.com/RicardoJackMC/Teaching-Material-Download-Manager/blob/main/LICENSE) 许可证.
246 |
247 | ## 坚决反对日本排放福岛核污染水!!!☢️
248 |
249 | > **Warning**
250 | > 本条目不涉及歧视或引战, 更与政治无关 ! ! ! 本条与整个人类的存亡有关 ! ! !
251 |
252 | 首先, 对于 国内外舆论迫使日本政府停止排放福岛核污染水 这一可能事件, 本人持消极态度
253 |
254 | 并且, 对于 我们普通人通过各种努力与斗争迫使日本政府停止排放福岛核污染水 这一可能事件, 本人同样持消极态度
255 |
256 | **但是, 不代表本人会消极应对此事!!!**
257 |
258 | **普通人的力量太小, 不能改变什么, 但是, 群众的力量是强大的**
259 |
260 | **我们也许不能改变日本排放福岛核污染水这一事实, 但是我们可以通过自己的行动, 让自己的子孙, 或者说, 人类的后代记住这件事!!!使他们不至于消失得这么不明不白!!!**
261 |
262 | 当然, 最好的结果是 迫使日本政府停止排放福岛核污染水 (尽管可能性很小)
263 |
264 | **但是即使可能性很小, 我们也要去尝试, 去斗争!!!**
265 |
266 | **本人这样做不是为了挑起任何冲突与矛盾, 也不想看到任何歧视与暴力的发生, 本人始终坚信, 日本人民, 甚至是日本政府内部, 仍然有人知道这件事的危害, 仍然有人在试图制止这一切的继续!!!**
267 |
268 | **本人的诉求十分简单:**
269 |
270 | **1. 日本政府停止继续通过排海的方式处理福岛核污染水, 使用更负责任的方式处理福岛核污染水 (本人清楚的知道实现这一条的概率很低)**
271 |
272 | **2. 若第一条未能实现, 本人希望全人类都能记住, 在公元2023年8月24日, 日本正式启动福岛核污染水排海 (本人将致力于实现本条诉求)**
273 |
274 | 最后祝愿各位顺利可以成功通关地球OL😶🌫️!!!
275 |
--------------------------------------------------------------------------------
/UI.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Copyright 2023 by RicardoJackMC
5 | Teaching Material Download Manager 使用 GPLv3 许可证
6 | 本文件是 Teaching Material Download Manager 的一部分
7 | 请自行前往
8 | https://github.com/RicardoJackMC/Teaching-Material-Download-Manager
9 | 或
10 | https://gitee.com/RicardoJackMC/Teaching-Material-Download-Manager
11 | 根据版本号校验本文件MD5
12 | """
13 |
14 | # Form implementation generated from reading ui file 'UI.ui'
15 | #
16 | # Created by: PyQt5 UI code generator 5.15.9
17 | #
18 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is
19 | # run again. Do not edit this file unless you know what you are doing.
20 |
21 | import json
22 | import os
23 | import sys
24 | import time
25 | import webbrowser
26 | import winreg
27 | from functools import partial
28 | import requests
29 | import resources_rc
30 |
31 | from PyQt5 import QtCore, QtGui, QtWidgets
32 | from PyQt5.QtCore import Qt, QThread, pyqtSignal, QCoreApplication, QPoint, QEvent
33 | from PyQt5.QtGui import QColor, QPainter, QIcon, QPixmap
34 | from PyQt5.QtWidgets import QFileDialog, QTableWidgetItem, QListWidgetItem, QWidget, QPushButton, \
35 | QVBoxLayout, QHBoxLayout, QLabel
36 | from qfluentwidgets import BodyLabel, ComboBox, HyperlinkButton, IndeterminateProgressRing, LineEdit, ListWidget, \
37 | ToolButton, ProgressRing, PushButton, SegmentedWidget, SpinBox, SwitchButton, TableWidget, TextEdit, FluentIcon, \
38 | Dialog, InfoBarIcon, Flyout, InfoBarPosition, InfoBar, setThemeColor, RoundMenu, Action, MenuAnimationType, \
39 | TransparentToolButton, CardWidget, IconWidget, CaptionLabel, SubtitleLabel, StateToolTip
40 |
41 | DISPLAY_MODE = 0
42 |
43 |
44 | class RoundWindow(QWidget):
45 |
46 | def __init__(self, parent=None):
47 | super(RoundWindow, self).__init__(parent)
48 | self.border_width = 8
49 |
50 | def paintEvent(self, event):
51 | global DISPLAY_MODE
52 | pat = QPainter(self)
53 | pat.setRenderHint(pat.Antialiasing)
54 | if DISPLAY_MODE == 1:
55 | pat.setBrush(QColor(243, 243, 243, 255))
56 | else:
57 | pat.setBrush(QColor(32, 32, 32, 255))
58 | pat.setPen(Qt.transparent)
59 |
60 | rect = self.rect()
61 | rect.setLeft(9)
62 | rect.setTop(9)
63 | rect.setWidth(rect.width() - 9)
64 | rect.setHeight(rect.height() - 9)
65 | pat.drawRoundedRect(rect, 15, 15)
66 |
67 |
68 | class update_func(QThread):
69 | return_result = pyqtSignal(str)
70 |
71 | def __init__(self):
72 | super().__init__()
73 | self.version = 'Ver.1.1.0_202308281300'
74 | self.api = None
75 | self.result = 'False'
76 |
77 | def run(self):
78 | try:
79 | response = requests.get(self.api)
80 | data = response.json()
81 | latest_version = data['tag_name']
82 | if self.version != latest_version:
83 | self.result = 'True'
84 | else:
85 | self.result = 'False'
86 | except:
87 | self.result = 'Error'
88 | self.return_result.emit(self.result)
89 |
90 |
91 | class normal_info(QThread):
92 | # 负责与下载进程通讯的线程
93 | get_info_signal = pyqtSignal() # 从主线程获取信息, 以及通知主线程完成下载所需的设置
94 | update_state_signal = pyqtSignal(str) # 更新下载状态, 即基本信息里的state
95 | update_progress_signal = pyqtSignal(int) # 更新下载进度, 即基本信息里的那个圈圈
96 | update_download_data_signal = pyqtSignal(dict) # 更新下载日志
97 | update_basic_info = pyqtSignal(dict) # 更新基本信息
98 |
99 | def __init__(self):
100 | super().__init__()
101 | self.pre_KEY = []
102 | self.Manager_start = False # 一个旗标变量, 目的是达到自动下载的效果
103 | self.setting_finish = False # 同样是旗标变量, 确保从主线程获取完整的信息以及确保主进程已完成下载所需的设置
104 | self.command = {} # 发送给下载进程的下载参数
105 | self.queue = None
106 | self.pre_progress = None
107 |
108 | def run(self):
109 | self.filter()
110 |
111 | def producer(self, type_info, info):
112 | data_info = {type_info: info}
113 | self.queue.put(data_info)
114 |
115 | def filter(self):
116 | # 用于区分来自下载进程的命令
117 | # 这是为了应对Queue的特性, 防止重复执行
118 | while True:
119 | print('queue running')
120 | try:
121 | info = self.queue.get()
122 | if 'KEY' in info:
123 | if info['KEY'] not in self.pre_KEY:
124 | self.pre_KEY.append(info['KEY'])
125 | if 'state' in info:
126 | self.update_state_signal.emit(info['state'])
127 | if 'progress' in info:
128 | if info['progress'] != self.pre_progress:
129 | self.update_progress_signal.emit(info['progress'])
130 | self.pre_progress = info['progress']
131 | if 'download_data' in info:
132 | self.update_download_data_signal.emit(info['download_data'])
133 | if 'basic_info' in info:
134 | self.update_basic_info.emit(info['basic_info'])
135 | except:
136 | pass
137 |
138 |
139 | class admin_info(QThread):
140 | finish_signal = pyqtSignal(str) # 完成下载
141 | return_download_url = pyqtSignal(dict) # 把pdf的下载链接更新到文本框
142 |
143 | def __init__(self):
144 | super().__init__()
145 | self.Manager_start = False
146 | self.queue_admin = None
147 | self.finish = False
148 | self.pre_KEY = []
149 |
150 | def run(self):
151 | while True:
152 | print('admin running')
153 | info_admin = self.queue_admin.get()
154 | if 'KEY' in info_admin:
155 | if info_admin['KEY'] not in self.pre_KEY:
156 | if 'result' in info_admin:
157 | self.finish_signal.emit(info_admin['result'])
158 | self.finish = True
159 | if 'download_url' in info_admin:
160 | self.return_download_url.emit(info_admin['download_url'])
161 |
162 |
163 | class AppCard(CardWidget):
164 | """ App card """
165 |
166 | def __init__(self, icon, title, content, state=None, parent=None):
167 |
168 | super().__init__(parent)
169 | self.parent_ = parent
170 | self.update_api = [{'text': '通过GitHub检查更新',
171 | 'url': 'https://api.github.com/repos/RicardoJackMC/Teaching-Material-Download-Manager/releases/latest'},
172 | {'text': '通过Gitee检查更新 (推荐)',
173 | 'url': 'https://gitee.com/api/v5/repos/RicardoJackMC/Teaching-Material-Download-Manager/releases/latest'}
174 | ]
175 | self.feedback_url = [{'text': '前往GitHub上提出Issue',
176 | 'url': 'https://github.com/RicardoJackMC/Teaching-Material-Download-Manager/issues'},
177 | {'text': '前往Gitee提出Issue (推荐)',
178 | 'url': 'https://gitee.com/RicardoJackMC/Teaching-Material-Download-Manager/issues'},
179 | {'text': '通过邮件暴击作者 (推荐)',
180 | 'url': 'mailto:ricardojackmc@gmail.com'}
181 | ]
182 | self.repos_url = [
183 | {'text': '前往GitHub仓库',
184 | 'url': 'https://github.com/RicardoJackMC/Teaching-Material-Download-Manager'},
185 | {'text': '前往Gitee仓库 (推荐)',
186 | 'url': 'https://gitee.com/RicardoJackMC/Teaching-Material-Download-Manager'}
187 | ]
188 | self.dictionary = {'more_update': self.update_api,
189 | 'more_code': self.repos_url,
190 | 'more_feedback': self.feedback_url}
191 | self.iconWidget = IconWidget(icon)
192 | self.titleLabel = BodyLabel(title, self)
193 | self.contentLabel = CaptionLabel(content, self)
194 | self.tranparentToolButton = TransparentToolButton(FluentIcon.MORE, self)
195 |
196 | self.hBoxLayout = QHBoxLayout(self)
197 | self.vBoxLayout = QVBoxLayout()
198 |
199 | self.setFixedHeight(73)
200 | self.iconWidget.setFixedSize(20, 20)
201 | self.contentLabel.setTextColor("#606060", "#d2d2d2")
202 | self.tranparentToolButton.setFixedWidth(32)
203 | self.hBoxLayout.addWidget(self.tranparentToolButton, 0, Qt.AlignRight)
204 |
205 | self.hBoxLayout.setContentsMargins(20, 11, 11, 11)
206 | self.hBoxLayout.setSpacing(15)
207 | self.hBoxLayout.addWidget(self.iconWidget)
208 |
209 | self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
210 | self.vBoxLayout.setSpacing(0)
211 | self.vBoxLayout.addWidget(self.titleLabel, 0, Qt.AlignVCenter)
212 | self.vBoxLayout.addWidget(self.contentLabel, 0, Qt.AlignVCenter)
213 | self.vBoxLayout.setAlignment(Qt.AlignVCenter)
214 | self.hBoxLayout.addLayout(self.vBoxLayout)
215 |
216 | self.hBoxLayout.addStretch(1)
217 | self.hBoxLayout.addWidget(self.tranparentToolButton, 0, Qt.AlignRight)
218 |
219 | self.tranparentToolButton.clicked.connect(partial(self.button_signal, state))
220 |
221 | self.update_func = update_func()
222 | self.update_func.return_result.connect(self.update_slot)
223 |
224 | def button_signal(self, state):
225 | if state in self.dictionary:
226 | self.more_menu(state)
227 | else:
228 | self.open_url(state)
229 |
230 | def open_url(self, url):
231 | if 'latest' in url:
232 | self.update_soft(url)
233 | else:
234 | webbrowser.open_new(url)
235 |
236 | def update_soft(self, api):
237 | self.update_func.api = api
238 | self.parent_.state_tool_tip_func()
239 | self.update_func.start()
240 |
241 | def update_slot(self, result):
242 | self.parent_.state_tool_tip_func()
243 | self.parent_.analyze_result(result)
244 |
245 | def more_menu(self, state):
246 | menu = RoundMenu(parent=self)
247 | for item in self.dictionary[state]:
248 | action = Action(FluentIcon.SHARE, item['text'], self)
249 | action.triggered.connect(partial(self.open_url, item['url']))
250 | menu.addAction(action)
251 | x = (self.tranparentToolButton.width() - menu.sizeHint().width()) // 2 + 10
252 | pos = self.tranparentToolButton.mapToGlobal(QPoint(x, self.tranparentToolButton.height()))
253 | menu.exec(pos)
254 |
255 |
256 | class AboutWindow(RoundWindow, QWidget):
257 |
258 | def __init__(self, parent=None):
259 | super().__init__(parent)
260 | self.stateTooltip = None
261 |
262 | def setupUi(self, QWidget):
263 | self.setObjectName("About")
264 | self.setWindowTitle('About')
265 | self.setWindowIcon(QIcon(':/recourse/logo.ico'))
266 | self.resize(1010, 714)
267 | self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
268 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
269 |
270 | self.centralwidget = QtWidgets.QWidget(self)
271 |
272 | QtCore.QMetaObject.connectSlotsByName(self)
273 |
274 | ICON = QLabel(self.centralwidget)
275 | ICON.setGeometry(QtCore.QRect(405, 40, 200, 200))
276 | pixmap = QPixmap(':/recourse/logo.png')
277 | ICON.setPixmap(pixmap)
278 | ICON.setScaledContents(True)
279 |
280 | title = SubtitleLabel('Teaching Material Download Manager', self.centralwidget)
281 | title.setGeometry(QtCore.QRect(330, 230, 350, 30))
282 |
283 | copyright_label = BodyLabel('Copyright 2023 by RicardoJackMC', self.centralwidget)
284 | copyright_label.setGeometry(QtCore.QRect(397, 260, 211, 20))
285 |
286 | version = CaptionLabel('Ver.1.1.0_202308281300', self.centralwidget)
287 | version.setGeometry(QtCore.QRect(439, 284, 127, 10))
288 |
289 | tip = CaptionLabel('请前往本项目GitHub仓库获取更新或根据版本号校验MD5', self.centralwidget)
290 | tip.setGeometry(QtCore.QRect(348, 295, 310, 20))
291 |
292 | fight = BodyLabel('!! 坚决反对日本排放福岛核污染水 !!', self.centralwidget)
293 | fight.setGeometry(QtCore.QRect(395, 18, 220, 20))
294 |
295 | Subtitle_1 = BodyLabel('关于本软件', self.centralwidget)
296 | Subtitle_1.setGeometry(QtCore.QRect(41, 344, 460, 30))
297 |
298 | card_about_update = AppCard(FluentIcon.CERTIFICATE, "检查更新",
299 | "点击右侧“...”检查更新",
300 | 'more_update', self)
301 | card_about_update.setGeometry(QtCore.QRect(40, 374, 460, 80))
302 |
303 | card_about_gpl = AppCard(FluentIcon.CERTIFICATE, "本软件使用 GPLv3 许可证",
304 | "点击右侧“...”阅读GPLv3许可证原文",
305 | 'https://www.gnu.org/licenses/gpl-3.0.html#license-text', self.centralwidget)
306 | card_about_gpl.setGeometry(QtCore.QRect(40, 449, 460, 80))
307 |
308 | card_source_code = AppCard(FluentIcon.CODE, "获取源代码",
309 | "点击右侧“...”访问本软件仓库",
310 | 'more_code',
311 | self.centralwidget)
312 | card_source_code.setGeometry(QtCore.QRect(40, 524, 460, 80))
313 |
314 | card_feedback = AppCard(FluentIcon.FEEDBACK, "反馈",
315 | "点击右侧“...”联系作者",
316 | state='more_feedback', parent=self.centralwidget)
317 | card_feedback.setGeometry(QtCore.QRect(40, 599, 460, 80))
318 |
319 | Subtitle_2 = BodyLabel('支持', self.centralwidget)
320 | Subtitle_2.setGeometry(QtCore.QRect(510, 344, 460, 30))
321 |
322 | card_website = AppCard(FluentIcon.HOME_FILL, "惰猫の小窝",
323 | "点击右侧“...”前往作者的个人网站",
324 | 'https://ricardojackmc.github.io/', self.centralwidget)
325 | card_website.setGeometry(QtCore.QRect(510, 374, 460, 80))
326 |
327 | card_qfluentwidgets = AppCard(':/recourse/qfluentwidgets_logo.png', "PyQt-Fluent-Widgets",
328 | '本软件界面基于此项目开发, 点击右侧“...”前往该项目GitHub仓库',
329 | 'https://github.com/zhiyiYo/PyQt-Fluent-Widgets', self.centralwidget)
330 | card_qfluentwidgets.setGeometry(QtCore.QRect(510, 449, 460, 30))
331 |
332 | card_icon = AppCard(FluentIcon.GITHUB, "fluentui-system-icons",
333 | "本软件图标基于此项目制作, 点击右侧“...”前往该项目GitHub仓库",
334 | "https://github.com/microsoft/fluentui-system-icons", self.centralwidget)
335 | card_icon.setGeometry(QtCore.QRect(510, 524, 460, 30))
336 |
337 | pushButton_close = QPushButton(self.centralwidget)
338 | pushButton_close.setGeometry(QtCore.QRect(20, 20, 15, 15))
339 | pushButton_close.setMinimumSize(QtCore.QSize(15, 15))
340 | pushButton_close.setMaximumSize(QtCore.QSize(15, 15))
341 | pushButton_close.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
342 | pushButton_close.setFocusPolicy(QtCore.Qt.NoFocus)
343 | pushButton_close.setStyleSheet(
344 | "QPushButton{background:#F76677;border-radius:7px;}\n"
345 | "QPushButton:hover{background:red;}")
346 | pushButton_close.setText("")
347 | pushButton_close.setObjectName("pushButton_close")
348 |
349 | pushButton_maximize = QPushButton(self.centralwidget)
350 | pushButton_maximize.setGeometry(QtCore.QRect(40, 20, 15, 15))
351 | pushButton_maximize.setMinimumSize(QtCore.QSize(15, 15))
352 | pushButton_maximize.setMaximumSize(QtCore.QSize(15, 15))
353 | pushButton_maximize.setFocusPolicy(QtCore.Qt.NoFocus)
354 | pushButton_maximize.setStyleSheet(
355 | "QPushButton{background:#F7D674;border-radius:7px;}\n"
356 | "QPushButton:hover{background:yellow;}")
357 | pushButton_maximize.setText("")
358 | pushButton_maximize.setObjectName("pushButton_maximize")
359 | pushButton_maximize.setEnabled(False)
360 |
361 | pushButton_minimize = QPushButton(self.centralwidget)
362 | pushButton_minimize.setGeometry(QtCore.QRect(60, 20, 15, 15))
363 | pushButton_minimize.setMinimumSize(QtCore.QSize(15, 15))
364 | pushButton_minimize.setMaximumSize(QtCore.QSize(15, 15))
365 | pushButton_minimize.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
366 | pushButton_minimize.setFocusPolicy(QtCore.Qt.NoFocus)
367 | pushButton_minimize.setStyleSheet(
368 | "QPushButton{background:#6DDF6D;border-radius:7px;}\n"
369 | "QPushButton:hover{background:green;}")
370 | pushButton_minimize.setText("")
371 | pushButton_minimize.setObjectName("pushButton_minimize")
372 |
373 | pushButton_close.clicked.connect(self.close)
374 | pushButton_minimize.clicked.connect(self.showMinimized)
375 |
376 | def state_tool_tip_func(self):
377 | if self.stateTooltip:
378 | self.stateTooltip.setContent('完成!!!')
379 | self.stateTooltip.setState(True)
380 | self.stateTooltip = None
381 | else:
382 | self.stateTooltip = StateToolTip('正在检查更新', '检查更新中...', self)
383 | self.stateTooltip.move(700, 30)
384 | self.stateTooltip.show()
385 |
386 | def analyze_result(self, result):
387 | if result == 'True':
388 | title = '发现新版本!!!'
389 | content = """可以前往本项目仓库或网盘下载新版本哦!"""
390 | w = Dialog(title, content, self)
391 | w.yesButton.setText('知道辣!')
392 | w.cancelButton.setVisible(False)
393 | if w.exec():
394 | pass
395 | elif result == 'False':
396 | title = '当前版本是最新版!!!'
397 | content = """当前版本是最新版呢!如果有bug记得告诉作者哦!"""
398 | w = Dialog(title, content, self)
399 | w.yesButton.setText('知道辣!')
400 | w.cancelButton.setVisible(False)
401 | if w.exec():
402 | pass
403 | elif result == 'Error':
404 | title = '无法检查更新!!!'
405 | content = """建议检查网络设置!如果发现bug记得告诉作者哦!"""
406 | w = Dialog(title, content, self)
407 | w.yesButton.setText('知道辣!')
408 | w.cancelButton.setVisible(False)
409 | if w.exec():
410 | pass
411 |
412 | def mousePressEvent(self, event):
413 | if event.button() == Qt.LeftButton:
414 | self.mouse_flag = True
415 | self.mouse_Position = event.globalPos() - self.pos()
416 | event.accept()
417 |
418 | def mouseMoveEvent(self, QMouseEvent):
419 | if Qt.LeftButton and self.mouse_flag:
420 | self.move(QMouseEvent.globalPos() - self.mouse_Position)
421 | QMouseEvent.accept()
422 |
423 | def mouseReleaseEvent(self, QMouseEvent):
424 | self.mouse_flag = False
425 |
426 |
427 | class Ui_MainWindow(RoundWindow):
428 | def __init__(self):
429 | super().__init__()
430 | self.queue = None
431 | self.queue_admin = None
432 | self.queue_command = None
433 |
434 | def setupUi(self, MainWindow):
435 | print('setup')
436 | if os.path.isfile('.\\config.json'):
437 | with open('.\\config.json', 'r') as f:
438 | self.config = json.load(f)
439 |
440 | self.folder = self.config['folder']
441 | self.save_mode = self.config['save_mode']
442 | self.download_mode = self.config['download_mode']
443 | self.IDM_path = self.config['IDM_path']
444 | self.Aria2_url = self.config['Aria2_url']
445 | self.chunk_size = self.config['chunk_size']
446 | self.SegmentedWidget_show = self.config['SegmentedWidget_show']
447 | self.open_folder = self.config['open_folder']
448 | self.first_open = self.config['first_open']
449 |
450 | self.download_data = {}
451 | self.command = {}
452 | self.list = []
453 | self.Download_running = False
454 | self.Task_running = False
455 | self.download_url_display = "## 此项仅选择“获取下载链接”时可用"
456 | self.title = None
457 | self.list_finish = []
458 |
459 | self.setObjectName("Main")
460 | self.setWindowTitle("Teaching Material Download Manager")
461 | self.setWindowIcon(QIcon(':/recourse/logo.ico'))
462 | self.resize(1000, 540)
463 |
464 | self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
465 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
466 | theme_color = self.get_windows_theme_color()
467 | if theme_color is not None:
468 | setThemeColor(QColor(theme_color[0], theme_color[1], theme_color[2]))
469 |
470 | self.SegmentedWidget = SegmentedWidget(self)
471 | self.SegmentedWidget.setGeometry(QtCore.QRect(510, 40, 450, 35))
472 | self.SegmentedWidget.setMaximumSize(QtCore.QSize(16777215, 16777215))
473 | self.SegmentedWidget.setObjectName("SegmentedWidget")
474 |
475 | self.IndeterminateProgressRing = IndeterminateProgressRing(self)
476 | self.IndeterminateProgressRing.setGeometry(QtCore.QRect(835, 95, 125, 125))
477 | self.IndeterminateProgressRing.setMinimumSize(QtCore.QSize(125, 125))
478 | self.IndeterminateProgressRing.setMaximumSize(QtCore.QSize(125, 125))
479 | self.IndeterminateProgressRing.setProperty("value", 0)
480 | self.IndeterminateProgressRing.setTextVisible(False)
481 | self.IndeterminateProgressRing.setObjectName("IndeterminateProgressRing")
482 | self.IndeterminateProgressRing.stop()
483 |
484 | self.TableWidget_info = TableWidget(self)
485 | self.TableWidget_info.setGeometry(QtCore.QRect(510, 95, 305, 125))
486 | self.TableWidget_info.setMaximumSize(QtCore.QSize(16777215, 16777215))
487 | self.TableWidget_info.setAutoFillBackground(False)
488 | self.TableWidget_info.setLineWidth(1)
489 | self.TableWidget_info.setGridStyle(QtCore.Qt.SolidLine)
490 | self.TableWidget_info.setWordWrap(True)
491 | self.TableWidget_info.setObjectName("TableWidget_info")
492 | self.TableWidget_info.setColumnCount(2)
493 | self.TableWidget_info.setRowCount(5)
494 |
495 | item = QtWidgets.QTableWidgetItem()
496 | item.setText("title")
497 | self.TableWidget_info.setVerticalHeaderItem(0, item)
498 | item = QtWidgets.QTableWidgetItem()
499 | item.setText("ID_A")
500 | self.TableWidget_info.setVerticalHeaderItem(1, item)
501 | item = QtWidgets.QTableWidgetItem()
502 | item.setText("json_url")
503 | self.TableWidget_info.setVerticalHeaderItem(2, item)
504 | item = QtWidgets.QTableWidgetItem()
505 | item.setText("ID_B")
506 | self.TableWidget_info.setVerticalHeaderItem(3, item)
507 | item = QtWidgets.QTableWidgetItem()
508 | item.setText("state")
509 | self.TableWidget_info.setVerticalHeaderItem(4, item)
510 | item = QtWidgets.QTableWidgetItem()
511 | self.TableWidget_info.setHorizontalHeaderItem(0, item)
512 | item = QtWidgets.QTableWidgetItem()
513 | self.TableWidget_info.setHorizontalHeaderItem(1, item)
514 | item = QtWidgets.QTableWidgetItem()
515 | self.TableWidget_info.setItem(0, 0, item)
516 | item = QtWidgets.QTableWidgetItem()
517 | self.TableWidget_info.setItem(1, 0, item)
518 | item = QtWidgets.QTableWidgetItem()
519 | self.TableWidget_info.setItem(2, 0, item)
520 | item = QtWidgets.QTableWidgetItem()
521 | self.TableWidget_info.setItem(3, 0, item)
522 | item = QtWidgets.QTableWidgetItem()
523 | self.TableWidget_info.setItem(4, 0, item)
524 |
525 | self.TableWidget_info.horizontalHeader().setVisible(False)
526 | self.TableWidget_info.horizontalHeader().setDefaultSectionSize(82)
527 | self.TableWidget_info.verticalHeader().setVisible(False)
528 | self.TableWidget_info.verticalHeader().setDefaultSectionSize(25)
529 | self.TableWidget_info.verticalHeader().setMinimumSectionSize(25)
530 | self.TableWidget_info.verticalHeader().setMaximumSectionSize(25)
531 |
532 | self.TableWidget_finished = TableWidget(self)
533 | self.TableWidget_finished.setGeometry(QtCore.QRect(510, 225, 450, 238))
534 | self.TableWidget_finished.setObjectName("TableWidget_finished")
535 | self.TableWidget_finished.setColumnCount(8)
536 | self.TableWidget_finished.setRowCount(1)
537 | self.TableWidget_finished.horizontalHeader().hide()
538 | self.TableWidget_finished.verticalHeader().hide()
539 | self.TableWidget_finished.installEventFilter(self)
540 |
541 | self.ProgressRing = ProgressRing(self)
542 | self.ProgressRing.setGeometry(QtCore.QRect(835, 95, 125, 125))
543 | self.ProgressRing.setMinimumSize(QtCore.QSize(125, 125))
544 | self.ProgressRing.setMaximumSize(QtCore.QSize(125, 125))
545 | self.ProgressRing.setMaximum(100)
546 | self.ProgressRing.setTextVisible(False)
547 | self.ProgressRing.setObjectName("ProgressRing")
548 |
549 | self.HyperlinkButton_export = HyperlinkButton(self)
550 | self.HyperlinkButton_export.setGeometry(QtCore.QRect(510, 95, 215, 31))
551 | self.HyperlinkButton_export.setObjectName("HyperlinkButton_export")
552 |
553 | self.HyperlinkButton_clear = HyperlinkButton(self)
554 | self.HyperlinkButton_clear.setGeometry(QtCore.QRect(735, 95, 215, 31))
555 | self.HyperlinkButton_clear.setObjectName("HyperlinkButton_clear")
556 |
557 | self.HyperlinkButton_about = HyperlinkButton(self)
558 | self.HyperlinkButton_about.setGeometry(QtCore.QRect(510, 463, 450, 32))
559 | self.HyperlinkButton_about.setObjectName("HyperlinkButton_about")
560 |
561 | self.ListWidget_data = ListWidget(self)
562 | self.ListWidget_data.setGeometry(QtCore.QRect(510, 146, 450, 297))
563 | self.ListWidget_data.setObjectName("ListWidget_data")
564 |
565 | self.LineEdit_url = LineEdit(self)
566 | self.LineEdit_url.setGeometry(QtCore.QRect(40, 40, 450, 33))
567 | self.LineEdit_url.setObjectName("LineEdit_url")
568 |
569 | self.PushButton_add = PushButton(self)
570 | self.PushButton_add.setGeometry(QtCore.QRect(40, 285, 450, 32))
571 | self.PushButton_add.setObjectName("PushButton_add")
572 |
573 | self.ComboBox_mode = ComboBox(self)
574 | self.ComboBox_mode.setGeometry(QtCore.QRect(40, 83, 450, 32))
575 | self.ComboBox_mode.setObjectName("ComboBox_mode")
576 | for i in range(4):
577 | self.ComboBox_mode.addItem('')
578 |
579 | self.SwitchButton_open = SwitchButton(self)
580 | self.SwitchButton_open.setGeometry(QtCore.QRect(270, 210, 220, 22))
581 | self.SwitchButton_open.setMinimumSize(QtCore.QSize(220, 22))
582 | self.SwitchButton_open.setMaximumSize(QtCore.QSize(220, 33))
583 | self.SwitchButton_open.setLayoutDirection(QtCore.Qt.RightToLeft)
584 | self.SwitchButton_open.setAutoFillBackground(False)
585 | self.SwitchButton_open.setChecked(True)
586 | self.SwitchButton_open.setObjectName("SwitchButton_open")
587 |
588 | self.BodyLabel_open = BodyLabel(self)
589 | self.BodyLabel_open.setGeometry(QtCore.QRect(40, 210, 361, 22))
590 | self.BodyLabel_open.setObjectName("BodyLabel_open")
591 |
592 | self.SpinBox_size = SpinBox(self)
593 | self.SpinBox_size.setGeometry(QtCore.QRect(270, 242, 177, 33))
594 | self.SpinBox_size.setMaximum(1000000)
595 | self.SpinBox_size.setSingleStep(1)
596 | self.SpinBox_size.setProperty("value", 1024)
597 | self.SpinBox_size.setObjectName("SpinBox_size")
598 |
599 | self.BodyLabel_size = BodyLabel(self)
600 | self.BodyLabel_size.setGeometry(QtCore.QRect(40, 242, 201, 33))
601 | self.BodyLabel_size.setObjectName("BodyLabel_size")
602 |
603 | self.LineEdit_path = LineEdit(self)
604 | self.LineEdit_path.setGeometry(QtCore.QRect(40, 167, 364, 33))
605 | self.LineEdit_path.setObjectName("LineEdit_path")
606 |
607 | self.ToolButton_path_ok = ToolButton(FluentIcon.ACCEPT, self)
608 | self.ToolButton_path_ok.setGeometry(QtCore.QRect(457, 167, 33, 33))
609 | self.ToolButton_path_ok.setText("")
610 | self.ToolButton_path_ok.setObjectName("ToolButton_path_ok")
611 |
612 | self.ToolButton_path_find = ToolButton(FluentIcon.MORE, self)
613 | self.ToolButton_path_find.setGeometry(QtCore.QRect(414, 167, 33, 33))
614 | self.ToolButton_path_find.setText("")
615 | self.ToolButton_path_find.setObjectName("ToolButton_path_find")
616 |
617 | self.TextEdit = TextEdit(self)
618 | self.TextEdit.setGeometry(QtCore.QRect(510, 95, 450, 348))
619 | self.TextEdit.setObjectName("TextEdit")
620 | self.TextEdit.setMarkdown(self.download_url_display)
621 |
622 | self.TableWidget = TableWidget(self)
623 | self.TableWidget.setGeometry(QtCore.QRect(40, 327, 450, 169))
624 | self.TableWidget.setObjectName("TableWidget")
625 | self.TableWidget.setColumnCount(8)
626 | self.TableWidget.setRowCount(1)
627 | self.TableWidget.horizontalHeader().hide()
628 | self.TableWidget.verticalHeader().hide()
629 | self.TableWidget.installEventFilter(self)
630 |
631 | self.ToolButton_size = ToolButton(FluentIcon.QUESTION, self)
632 | self.ToolButton_size.setGeometry(QtCore.QRect(457, 242, 33, 33))
633 | self.ToolButton_size.setObjectName("ToolButton_size")
634 |
635 | self.ComboBox_same = ComboBox(self)
636 | self.ComboBox_same.setGeometry(QtCore.QRect(270, 125, 220, 32))
637 | self.ComboBox_same.setObjectName("ComboBox_same")
638 | for i in range(3):
639 | self.ComboBox_same.addItem('')
640 |
641 | self.BodyLabel_same = BodyLabel(self)
642 | self.BodyLabel_same.setGeometry(QtCore.QRect(40, 125, 220, 32))
643 | self.BodyLabel_same.setObjectName("BodyLabel_same")
644 |
645 | self.ToolButton_path_find_IDM = ToolButton(FluentIcon.MORE, self)
646 | self.ToolButton_path_find_IDM.setGeometry(QtCore.QRect(414, 210, 33, 33))
647 | self.ToolButton_path_find_IDM.setText("")
648 | self.ToolButton_path_find_IDM.setObjectName("ToolButton_path_find_IDM")
649 |
650 | self.LineEdit_path_IDM = LineEdit(self)
651 | self.LineEdit_path_IDM.setGeometry(QtCore.QRect(40, 210, 364, 33))
652 | self.LineEdit_path_IDM.setObjectName("LineEdit_path_IDM")
653 |
654 | self.ToolButton_path_ok_IDM = ToolButton(FluentIcon.ACCEPT, self)
655 | self.ToolButton_path_ok_IDM.setGeometry(QtCore.QRect(457, 210, 33, 33))
656 | self.ToolButton_path_ok_IDM.setText("")
657 | self.ToolButton_path_ok_IDM.setObjectName("ToolButton_path_ok_IDM")
658 |
659 | self.LineEdit_url_aria2 = LineEdit(self)
660 | self.LineEdit_url_aria2.setGeometry(QtCore.QRect(40, 210, 407, 33))
661 | self.LineEdit_url_aria2.setObjectName("LineEdit_url_aria2")
662 |
663 | self.ToolButton_url_aria2_ok = ToolButton(FluentIcon.ACCEPT, self)
664 | self.ToolButton_url_aria2_ok.setGeometry(QtCore.QRect(457, 210, 33, 33))
665 | self.ToolButton_url_aria2_ok.setText("")
666 | self.ToolButton_url_aria2_ok.setObjectName("ToolButton_url_aria2_ok")
667 |
668 | self.pushButton_close = QPushButton(self)
669 | self.pushButton_close.setGeometry(QtCore.QRect(20, 20, 15, 15))
670 | self.pushButton_close.setMinimumSize(QtCore.QSize(15, 15))
671 | self.pushButton_close.setMaximumSize(QtCore.QSize(15, 15))
672 | self.pushButton_close.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
673 | self.pushButton_close.setFocusPolicy(QtCore.Qt.NoFocus)
674 | self.pushButton_close.setStyleSheet(
675 | "QPushButton{background:#F76677;border-radius:7px;}\n"
676 | "QPushButton:hover{background:red;}")
677 | self.pushButton_close.setText("")
678 | self.pushButton_close.setObjectName("pushButton_close")
679 |
680 | self.pushButton_maximize = QPushButton(self)
681 | self.pushButton_maximize.setGeometry(QtCore.QRect(40, 20, 15, 15))
682 | self.pushButton_maximize.setMinimumSize(QtCore.QSize(15, 15))
683 | self.pushButton_maximize.setMaximumSize(QtCore.QSize(15, 15))
684 | self.pushButton_maximize.setFocusPolicy(QtCore.Qt.NoFocus)
685 | self.pushButton_maximize.setStyleSheet(
686 | "QPushButton{background:#F7D674;border-radius:7px;}\n"
687 | "QPushButton:hover{background:yellow;}")
688 | self.pushButton_maximize.setText("")
689 | self.pushButton_maximize.setObjectName("pushButton_maximize")
690 | self.pushButton_maximize.setEnabled(False)
691 |
692 | self.pushButton_minimize = QPushButton(self)
693 | self.pushButton_minimize.setGeometry(QtCore.QRect(60, 20, 15, 15))
694 | self.pushButton_minimize.setMinimumSize(QtCore.QSize(15, 15))
695 | self.pushButton_minimize.setMaximumSize(QtCore.QSize(15, 15))
696 | self.pushButton_minimize.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
697 | self.pushButton_minimize.setFocusPolicy(QtCore.Qt.NoFocus)
698 | self.pushButton_minimize.setStyleSheet(
699 | "QPushButton{background:#6DDF6D;border-radius:7px;}\n"
700 | "QPushButton:hover{background:green;}")
701 | self.pushButton_minimize.setText("")
702 | self.pushButton_minimize.setObjectName("pushButton_minimize")
703 |
704 | self.Normal_Info = normal_info()
705 | self.Normal_Info.queue = self.queue
706 |
707 | self.Normal_Info.update_state_signal.connect(self.update_state_slot)
708 | self.Normal_Info.update_progress_signal.connect(self.update_progress_slot)
709 | self.Normal_Info.update_download_data_signal.connect(self.update_download_data_slot)
710 | self.Normal_Info.update_basic_info.connect(self.update_basic_info)
711 | self.Normal_Info.start()
712 |
713 | self.Admin_Info = admin_info()
714 | self.Admin_Info.queue_admin = self.queue_admin
715 | self.Admin_Info.finish_signal.connect(self.download_finish)
716 | self.Admin_Info.return_download_url.connect(self.update_download_url)
717 | self.Admin_Info.start()
718 |
719 | self.retranslateUi()
720 |
721 | QtCore.QMetaObject.connectSlotsByName(self)
722 |
723 | self.ComboBox_mode.currentIndexChanged.connect(self.ComboBox_mode_change)
724 | self.ComboBox_same.currentIndexChanged.connect(self.ComboBox_same_change)
725 | self.LineEdit_path.textChanged.connect(self.LineEdit_path_change)
726 | self.LineEdit_path.returnPressed.connect(self.confirm_folder)
727 | self.ToolButton_path_ok.clicked.connect(self.confirm_folder)
728 | self.ToolButton_path_find.clicked.connect(self.choose_folder)
729 | self.SwitchButton_open.checkedChanged.connect(self.switch_change)
730 | self.SpinBox_size.valueChanged[int].connect(self.chunk_size_change)
731 | self.ToolButton_size.clicked.connect(self.chunk_size_dialog)
732 | self.LineEdit_path_IDM.textChanged.connect(self.LineEdit_path_IDM_change)
733 | self.LineEdit_path_IDM.returnPressed.connect(self.confirm_IDM_path)
734 | self.ToolButton_path_ok_IDM.clicked.connect(self.confirm_IDM_path)
735 | self.ToolButton_path_find_IDM.clicked.connect(self.choose_IDM_path)
736 | self.LineEdit_url_aria2.returnPressed.connect(self.LineEdit_url_aria2_change)
737 | self.ToolButton_url_aria2_ok.clicked.connect(self.LineEdit_url_aria2_change)
738 | self.HyperlinkButton_clear.clicked.connect(self.clear_download_data)
739 | self.HyperlinkButton_export.clicked.connect(self.export_download_data)
740 | self.HyperlinkButton_about.clicked.connect(self.about)
741 | self.PushButton_add.clicked.connect(self.add_command)
742 | self.LineEdit_url.returnPressed.connect(self.add_command)
743 | self.pushButton_close.clicked.connect(self.close)
744 | self.pushButton_minimize.clicked.connect(self.showMinimized)
745 | self.TableWidget.setContextMenuPolicy(Qt.CustomContextMenu)
746 | self.TableWidget.customContextMenuRequested.connect(self.show_list_menu)
747 | self.TableWidget_finished.setContextMenuPolicy(Qt.CustomContextMenu)
748 | self.TableWidget_finished.customContextMenuRequested.connect(self.show_finish_menu)
749 |
750 | self.SegmentedWidget.setCurrentItem(self.SegmentedWidget_show)
751 | if self.SegmentedWidget_show == 'basic_info':
752 | self.show_basic_info()
753 | elif self.SegmentedWidget_show == 'download_link':
754 | self.show_download_link()
755 | elif self.SegmentedWidget_show == 'download_data':
756 | self.show_download_data()
757 | self.ComboBox_mode.setCurrentIndex(self.download_mode)
758 | self.ComboBox_same.setCurrentIndex(self.save_mode)
759 | self.confirm_folder(source='start')
760 | self.SwitchButton_open.setChecked(self.open_folder)
761 | self.SpinBox_size.setValue(self.chunk_size)
762 | self.confirm_IDM_path(source='start')
763 | self.LineEdit_url_aria2_change(source='start')
764 | self.ComboBox_mode_change(self.download_mode)
765 | self.ComboBox_same_change(self.save_mode)
766 |
767 | def retranslateUi(self):
768 | self._translate = QtCore.QCoreApplication.translate
769 | self.setWindowTitle(self._translate("Teaching Material Download Manager", "Teaching Material Download Manager"))
770 | header = ['链接', '下载模式', '保存路径', '当出现相同文件时', 'chunk_size', 'IDM位置', 'Aria2地址',
771 | '完成下载后是否打开文件夹']
772 | for index, i in enumerate(header):
773 | self.TableWidget.setItem(0, index, QTableWidgetItem(i))
774 | self.TableWidget_finished.setItem(0, index, QTableWidgetItem(i))
775 | self.TableWidget.item(0, index).setFlags(self.TableWidget.item(0, index).flags() & ~Qt.ItemIsSelectable)
776 | self.TableWidget_finished.item(0, index).setFlags(
777 | self.TableWidget_finished.item(0, index).flags() & ~Qt.ItemIsSelectable)
778 | item = self.TableWidget_info.horizontalHeaderItem(0)
779 | item.setText(self._translate("MainWindow", "新建列"))
780 | item = self.TableWidget_info.horizontalHeaderItem(1)
781 | item.setText(self._translate("MainWindow", "新建列"))
782 | __sortingEnabled = self.TableWidget_info.isSortingEnabled()
783 | self.TableWidget_info.setSortingEnabled(False)
784 | item = self.TableWidget_info.item(0, 0)
785 | item.setText(self._translate("MainWindow", "title"))
786 | item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
787 | item = self.TableWidget_info.item(1, 0)
788 | item.setText(self._translate("MainWindow", "ID_A"))
789 | item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
790 | item = self.TableWidget_info.item(2, 0)
791 | item.setText(self._translate("MainWindow", "json_url"))
792 | item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
793 | item = self.TableWidget_info.item(3, 0)
794 | item.setText(self._translate("MainWindow", "ID_B"))
795 | item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
796 | item = self.TableWidget_info.item(4, 0)
797 | item.setText(self._translate("MainWindow", "state"))
798 | item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
799 | self.TableWidget_info.setSortingEnabled(__sortingEnabled)
800 | for index in range(5):
801 | self.TableWidget_info.item(index, 0).setFlags(
802 | self.TableWidget_info.item(index, 0).flags() & ~Qt.ItemIsSelectable)
803 | self.ProgressRing.setFormat(self._translate("MainWindow", ""))
804 | self.HyperlinkButton_export.setText(self._translate("MainWindow", "导出下载日志"))
805 | self.HyperlinkButton_clear.setText(self._translate("MainWindow", "清空下载日志"))
806 | self.HyperlinkButton_about.setText(self._translate("MainWindow", "关于本软件"))
807 | self.PushButton_add.setText(self._translate("MainWindow", "下载"))
808 | self.SwitchButton_open.setOnText(self._translate("MainWindow", "打开"))
809 | self.SwitchButton_open.setOffText(self._translate("MainWindow", "不打开"))
810 | self.BodyLabel_open.setText(self._translate("MainWindow", "下载完成后是否打开文件夹: "))
811 | self.BodyLabel_size.setText(self._translate("MainWindow", "设置chunk_size的大小(单位KB): "))
812 | self.BodyLabel_same.setText(self._translate("MainWindow", "当出现同名文件时:"))
813 | self.ComboBox_same.setItemText(0, self._translate("MainWindow", "覆盖"))
814 | self.ComboBox_same.setItemText(1, self._translate("MainWindow", "添加数字后缀"))
815 | self.ComboBox_same.setItemText(2, self._translate("MainWindow", "添加时间后缀"))
816 | self.ComboBox_mode.setItemText(0, self._translate("MainWindow", "使用软件内建下载"))
817 | self.ComboBox_mode.setItemText(1, self._translate("MainWindow", "调用IDM下载器下载"))
818 | self.ComboBox_mode.setItemText(2, self._translate("MainWindow", "发送到Aria2服务器下载"))
819 | self.ComboBox_mode.setItemText(3, self._translate("MainWindow", "仅获取下载链接"))
820 | self.SegmentedWidget.addItem('basic_info', '基本信息', onClick=self.show_basic_info)
821 | self.SegmentedWidget.addItem('download_link', '下载链接', onClick=self.show_download_link)
822 | self.SegmentedWidget.addItem('download_data', '下载日志', onClick=self.show_download_data)
823 | self.LineEdit_url.setPlaceholderText(self._translate("MainWindow", "输入教材的网址"))
824 | self.LineEdit_path.setPlaceholderText(
825 | self._translate("MainWindow", "在这里输入保存教材的文件夹地址,或点击...选择文件夹"))
826 | self.LineEdit_path_IDM.setPlaceholderText(
827 | self._translate("MainWindow", "在这里输入IDMan.exe的位置,或点击...选择"))
828 | self.LineEdit_url_aria2.setPlaceholderText(self._translate("MainWindow", "在这里输入Aria2服务器地址"))
829 |
830 | def mousePressEvent(self, event):
831 | if event.button() == Qt.LeftButton:
832 | self.mouse_flag = True
833 | self.mouse_Position = event.globalPos() - self.pos()
834 | event.accept()
835 |
836 | def mouseMoveEvent(self, QMouseEvent):
837 | if Qt.LeftButton and self.mouse_flag:
838 | self.move(QMouseEvent.globalPos() - self.mouse_Position)
839 | QMouseEvent.accept()
840 |
841 | def mouseReleaseEvent(self, QMouseEvent):
842 | self.mouse_flag = False
843 |
844 | def closeEvent(self, event):
845 | title = '是否退出软件'
846 | content = """请确认所有下载任务已完成, 否则可能会导致下载失败!!!"""
847 | w = Dialog(title, content, self)
848 | if w.exec():
849 | self.queue_command.put('CLOSE')
850 | event.accept()
851 | QCoreApplication.quit()
852 | sys.exit()
853 | else:
854 | event.ignore()
855 |
856 | def welcome_dialog(self):
857 | if self.first_open == 1:
858 | title = '欢迎使用 Teaching Material Download Manager'
859 | content = """请认真阅读以下内容, 如果您不同意以下任意内容, 请立即退出本软件:\n\n 1.本软件使用GPLv3许可证, 请根据GPLv3许可证正确行使您拥有的关于\n 本软件的权力以及您应履行的义务, 您可以点击“关于此软件”并在弹出
860 | 的窗口中阅读 GPLv3 的原文。\n\n 2.请尊重教材编者和作者的劳动果实, 在中华人民共和国的法律范围内\n 使用本软件及电子教材。"""
861 | w = Dialog(title, content, self)
862 | w.yesButton.setText('同意')
863 | w.cancelButton.setText('不同意')
864 | if w.exec():
865 | self.first_open = False
866 | self.save_config('first_open', self.first_open)
867 | else:
868 | self.queue_command.put('CLOSE')
869 | QCoreApplication.quit()
870 | sys.exit()
871 |
872 | def download_finish(self, state):
873 | self.Task_running = False
874 | self.IndeterminateProgressRing.stop()
875 | self.ProgressRing.setValue(0)
876 | self.ProgressRing.setTextVisible(False)
877 |
878 | self.display = self.title
879 | if self.display is None:
880 | self.display = self.list[0]['url_A']
881 | self.TableWidget_finished.setRowCount(self.TableWidget_finished.rowCount() + 1)
882 | for item in range(8):
883 | i = self.TableWidget.item(1, item).text()
884 | self.TableWidget_finished.setItem(self.TableWidget_finished.rowCount() - 1, item,
885 | QTableWidgetItem(i))
886 |
887 | self.TableWidget_finished.resizeColumnsToContents()
888 | self.TableWidget_finished.resizeRowsToContents()
889 | self.TableWidget.removeRow(1)
890 | if self.list[0]['open_folder'] and self.list[0]['download_mode'] == 0:
891 | os.startfile(self.list[0]['save_path'])
892 | self.list_finish.append(self.list[0])
893 | self.list.pop(0)
894 |
895 | for i in range(5):
896 | self.TableWidget_info.setItem(i, 1, QTableWidgetItem(''))
897 | if state == 'normal':
898 | self.successful_info(self.display)
899 | elif state == 'warning':
900 | self.warning_info(self.display)
901 | elif state == 'error':
902 | self.error_info(self.display)
903 | elif state == 'ID_A error':
904 | self.ID_A_ERROR_info(self.display)
905 |
906 | if len(self.list) != 0:
907 | self.start()
908 | else:
909 | self.Download_running = False
910 |
911 | def successful_info(self, url):
912 | InfoBar.success(
913 | title='成功处理',
914 | content="已成功处理:" + url,
915 | orient=Qt.Horizontal,
916 | isClosable=True,
917 | position=InfoBarPosition.BOTTOM_RIGHT,
918 | duration=18000,
919 | parent=self
920 | )
921 |
922 | def ID_A_ERROR_info(self, url):
923 | InfoBar.error(
924 | title='网址错误',
925 | content="无法处理:" + url + "请输入正确的网址后再试一次",
926 | orient=Qt.Horizontal,
927 | isClosable=True,
928 | position=InfoBarPosition.BOTTOM_RIGHT,
929 | duration=20000,
930 | parent=self
931 | )
932 |
933 | def error_info(self, url):
934 | InfoBar.error(
935 | title='处理失败',
936 | content="无法处理:" + url + "建议检查网络连接, 并在试一次,\n若你想支持本软件的开发, 请导出下载日志并将其发送到 ricardojackmc@gmail.com",
937 | orient=Qt.Horizontal,
938 | isClosable=True,
939 | position=InfoBarPosition.BOTTOM_RIGHT,
940 | duration=20000,
941 | parent=self
942 | )
943 |
944 | def warning_info(self, url):
945 | InfoBar.warning(
946 | title='成功处理, 但出现了一些问题',
947 | content="处理 " + url + " 时出现了一些问题,\n但是如你所见, 文件应该已经成功保存到此电脑或成功发送至指定下载器,\n若你想支持本软件的开发, 请导出下载日志并将其发送到 ricardojackmc@gmail.com",
948 | orient=Qt.Horizontal,
949 | isClosable=True,
950 | position=InfoBarPosition.BOTTOM_RIGHT,
951 | duration=20000,
952 | parent=self
953 | )
954 |
955 | def update_basic_info(self, info):
956 | if 'title' in info:
957 | self.TableWidget_info.setItem(0, 1, QTableWidgetItem(info['title']))
958 | self.title = info['title']
959 | elif 'ID_A' in info:
960 | self.TableWidget_info.setItem(1, 1, QTableWidgetItem(info['ID_A']))
961 | elif 'json_url' in info:
962 | self.TableWidget_info.setItem(2, 1, QTableWidgetItem(info['json_url']))
963 | elif 'ID_B' in info:
964 | self.TableWidget_info.setItem(3, 1, QTableWidgetItem(info['ID_B']))
965 | self.TableWidget_info.resizeColumnsToContents()
966 | self.TableWidget_info.resizeRowsToContents()
967 |
968 | def add_command(self):
969 |
970 | element = {}
971 | key = ['url_A', 'download_mode', 'save_path', 'save_mode', 'chunk_size', 'IDM_path', 'Aria2_url', 'open_folder']
972 | value = [self.LineEdit_url.text(), self.download_mode, self.folder, self.save_mode, self.chunk_size,
973 | self.IDM_path, self.Aria2_url, self.open_folder]
974 | self.TableWidget.setRowCount(self.TableWidget.rowCount() + 1)
975 | for index, item in enumerate(value):
976 | self.TableWidget.setItem(self.TableWidget.rowCount() - 1, index, QTableWidgetItem(str(item)))
977 | element[key[index]] = item
978 |
979 | self.list.append(element)
980 | self.LineEdit_url.clear()
981 | self.TableWidget.resizeColumnsToContents()
982 | self.TableWidget.resizeRowsToContents()
983 |
984 | if not self.Download_running:
985 | self.Download_running = True
986 | self.start()
987 |
988 | def start(self):
989 | self.confirm_folder(source='chosen', state='force')
990 | if self.list[0]['download_mode'] == 1:
991 | self.confirm_IDM_path(source='chosen', state='force')
992 | self.command = self.list[0]
993 | self.TableWidget.selectRow(1)
994 | self.IndeterminateProgressRing.start()
995 | self.ProgressRing.setFormat('loading...')
996 | self.ProgressRing.setTextVisible(True)
997 | self.command['key'] = time.time()
998 | data_info = {'command': self.command}
999 | self.queue_command.put(data_info)
1000 | self.Task_running = True
1001 |
1002 | def update_download_data_slot(self, data):
1003 | for key in data.keys():
1004 | if key not in self.download_data:
1005 | item = QListWidgetItem(data[key])
1006 | self.ListWidget_data.addItem(item)
1007 | self.ListWidget_data.setCurrentItem(item)
1008 | self.ListWidget_data.scrollToItem(item, QtWidgets.QAbstractItemView.PositionAtBottom)
1009 | self.download_data.update(data)
1010 |
1011 | def update_download_url(self, data):
1012 | self.download_url_display = self.download_url_display + '\n \n#### · ' + data['title'] + ' :\n' + data[
1013 | 'download_url']
1014 | self.TextEdit.setMarkdown(self.download_url_display)
1015 |
1016 | def update_progress_slot(self, value):
1017 | self.IndeterminateProgressRing.stop()
1018 | if self.Task_running:
1019 | self.ProgressRing.setValue(value)
1020 | self.ProgressRing.setFormat('已下载: ' + '%p%')
1021 |
1022 | def update_state_slot(self, info):
1023 | self.TableWidget_info.setItem(4, 1, QTableWidgetItem(info))
1024 |
1025 | def save_config(self, config_type, info):
1026 | self.config[config_type] = info
1027 | with open('config.json', 'w') as f:
1028 | json.dump(self.config, f)
1029 |
1030 | def show_basic_info(self):
1031 | self.SegmentedWidget_show = 'basic_info'
1032 | self.save_config('SegmentedWidget_show', self.SegmentedWidget_show)
1033 | self.set_basic_info(True)
1034 | self.set_download_link(False)
1035 | self.set_download_data(False)
1036 |
1037 | def show_download_link(self):
1038 | self.SegmentedWidget_show = 'download_link'
1039 | self.save_config('SegmentedWidget_show', self.SegmentedWidget_show)
1040 | self.set_basic_info(False)
1041 | self.set_download_link(True)
1042 | self.set_download_data(False)
1043 |
1044 | def show_download_data(self):
1045 | self.SegmentedWidget_show = 'download_data'
1046 | self.save_config('SegmentedWidget_show', self.SegmentedWidget_show)
1047 | self.set_basic_info(False)
1048 | self.set_download_link(False)
1049 | self.set_download_data(True)
1050 |
1051 | def set_basic_info(self, state):
1052 | self.TableWidget_info.setVisible(state)
1053 | self.TableWidget_finished.setVisible(state)
1054 | self.ProgressRing.setVisible(state)
1055 | self.IndeterminateProgressRing.setVisible(state)
1056 |
1057 | def set_download_data(self, state):
1058 | self.HyperlinkButton_export.setVisible(state)
1059 | self.HyperlinkButton_clear.setVisible(state)
1060 | self.ListWidget_data.setVisible(state)
1061 |
1062 | def set_download_link(self, state):
1063 | self.TextEdit.setVisible(state)
1064 |
1065 | def ComboBox_mode_change(self, index):
1066 | self.download_mode = index
1067 | self.save_config('download_mode', self.download_mode)
1068 | if index == 0:
1069 | self.show_basic_download()
1070 | elif index == 1:
1071 | self.show_IDM_download()
1072 | elif index == 2:
1073 | self.show_Aria2_download()
1074 | elif index == 3:
1075 | self.show_PDF_link()
1076 |
1077 | def show_basic_download(self):
1078 | self.set_bottom_setting(True)
1079 | self.set_basic_download(True)
1080 | self.set_IDM_download(False)
1081 | self.set_Aria2_download(False)
1082 |
1083 | def show_IDM_download(self):
1084 | self.set_bottom_setting(True)
1085 | self.set_basic_download(False)
1086 | self.set_IDM_download(True)
1087 | self.set_Aria2_download(False)
1088 |
1089 | def show_Aria2_download(self):
1090 | self.set_bottom_setting(True)
1091 | self.set_basic_download(False)
1092 | self.set_IDM_download(False)
1093 | self.set_Aria2_download(True)
1094 |
1095 | def show_PDF_link(self):
1096 | self.set_bottom_setting(False)
1097 | self.set_basic_download(False)
1098 | self.set_IDM_download(False)
1099 | self.set_Aria2_download(False)
1100 | self.set_PDF_link()
1101 |
1102 | def set_bottom_setting(self, state):
1103 | self.BodyLabel_same.setVisible(state)
1104 | self.ComboBox_same.setVisible(state)
1105 | self.LineEdit_path.setVisible(state)
1106 | self.ToolButton_path_ok.setVisible(state)
1107 | self.ToolButton_path_find.setVisible(state)
1108 |
1109 | def set_basic_download(self, state):
1110 | self.SwitchButton_open.setVisible(state)
1111 | self.BodyLabel_open.setVisible(state)
1112 | self.BodyLabel_size.setVisible(state)
1113 | self.SpinBox_size.setVisible(state)
1114 | self.ToolButton_size.setVisible(state)
1115 |
1116 | def set_IDM_download(self, state):
1117 | self.LineEdit_path_IDM.setVisible(state)
1118 | self.ToolButton_path_ok_IDM.setVisible(state)
1119 | self.ToolButton_path_find_IDM.setVisible(state)
1120 |
1121 | def set_Aria2_download(self, state):
1122 | self.LineEdit_url_aria2.setVisible(state)
1123 | self.ToolButton_url_aria2_ok.setVisible(state)
1124 |
1125 | def set_PDF_link(self):
1126 | self.SegmentedWidget.setCurrentItem('download_link')
1127 | self.show_download_link()
1128 |
1129 | def ComboBox_same_change(self, index):
1130 | self.save_mode = index
1131 | self.save_config('save_mode', self.save_mode)
1132 |
1133 | def confirm_folder(self, source=None, state=None):
1134 | if source == 'start':
1135 | if os.path.isdir(self.folder):
1136 | self.LineEdit_path.setText(self.folder)
1137 | self.save_config('folder', self.folder)
1138 | self.ToolButton_path_ok.setEnabled(False)
1139 | elif source == 'chosen':
1140 | if os.path.isdir(self.folder):
1141 | self.LineEdit_path.setText(self.folder)
1142 | self.save_config('folder', self.folder)
1143 | self.ToolButton_path_ok.setEnabled(False)
1144 | if state == 'force':
1145 | if not os.path.isdir(self.list[0]['save_path']):
1146 | self.list[0]['save_path'] = self.folder
1147 | else:
1148 | self.path_wrong_warning_dialog(state=state)
1149 | else:
1150 | if os.path.isdir(self.LineEdit_path.text()):
1151 | self.folder = self.LineEdit_path.text()
1152 | self.save_config('folder', self.folder)
1153 | self.ToolButton_path_ok.setEnabled(False)
1154 | else:
1155 | self.path_wrong_warning_dialog()
1156 |
1157 | def path_wrong_warning_dialog(self, state=None):
1158 | title = '当前路径不可用'
1159 | content = """点击下方OK按钮后在弹出的窗口中浏览并选择文件夹"""
1160 | w = Dialog(title, content, self)
1161 | if w.exec():
1162 | self.choose_folder(state=state)
1163 | else:
1164 | if state == 'force':
1165 | self.confirm_folder(source='chosen', state=state)
1166 |
1167 | def choose_folder(self, state=None):
1168 | chosen_fold = QtWidgets.QFileDialog.getExistingDirectory(None, "选取文件夹",
1169 | os.path.expanduser('~') + '\\document')
1170 | if chosen_fold:
1171 | if not chosen_fold.endswith('\\'):
1172 | self.folder = chosen_fold + '\\'
1173 | self.confirm_folder(source='chosen', state=state)
1174 |
1175 | def LineEdit_path_change(self):
1176 | self.ToolButton_path_ok.setEnabled(True)
1177 |
1178 | def switch_change(self):
1179 | self.open_folder = self.SwitchButton_open.isChecked()
1180 | self.save_config('open_folder', self.open_folder)
1181 |
1182 | def chunk_size_change(self, value):
1183 | self.chunk_size = value
1184 | self.save_config('chunk_size', self.chunk_size)
1185 |
1186 | def chunk_size_dialog(self):
1187 | Flyout.create(
1188 | icon=InfoBarIcon.INFORMATION,
1189 | title='什么是 chunk_size ? 如何设置 chunk_size ?',
1190 | content="软件会把PDF文件切成很多“小块”, 再下载这些小块, chunk_size 就是这些“小块”的大小.\nchunk_size 过大可能会导致内存占用增加, chunk_size 过小则会导致下载速度太慢.\n此处建议综合考虑自己电脑的内存大小, 网速和内存占用情况合理设置 chunk_size 的大小.",
1191 | target=self.ToolButton_size,
1192 | parent=self,
1193 | isClosable=True
1194 | )
1195 |
1196 | def confirm_IDM_path(self, source=None, state=None):
1197 | if source == 'start':
1198 | if os.path.isfile(self.IDM_path) and os.path.basename(self.IDM_path) == 'IDMan.exe':
1199 | self.LineEdit_path_IDM.setText(self.IDM_path)
1200 | self.ToolButton_path_ok_IDM.setEnabled(False)
1201 | elif source == 'chosen':
1202 | if os.path.isfile(self.IDM_path) and os.path.basename(self.IDM_path) == 'IDMan.exe':
1203 | self.LineEdit_path_IDM.setText(self.IDM_path)
1204 | self.ToolButton_path_ok_IDM.setEnabled(False)
1205 | if state == 'force':
1206 | if not os.path.isfile(self.list[0]['IDM_path']) and os.path.basename(
1207 | self.list[0]['IDM_path']) == 'IDMan.exe':
1208 | self.list[0]['IDM_path'] = self.IDM_path
1209 | self.save_config('IDM_path', self.IDM_path)
1210 | else:
1211 | self.IDM_path_wrong_warning_dialog(state=state)
1212 | else:
1213 | if os.path.isfile(self.IDM_path) and os.path.basename(self.IDM_path) == 'IDMan.exe':
1214 | self.LineEdit_path_IDM.setText(self.IDM_path)
1215 | self.ToolButton_path_ok_IDM.setEnabled(False)
1216 | self.save_config('IDM_path', self.IDM_path)
1217 | else:
1218 | self.IDM_path_wrong_warning_dialog()
1219 |
1220 | def IDM_path_wrong_warning_dialog(self, state=None):
1221 | title = '请配置正确的IDMan.exe的位置'
1222 | content = """点击下方OK按钮后在弹出的窗口中浏览并选择IDMan.exe"""
1223 | w = Dialog(title, content, self)
1224 | if w.exec():
1225 | self.choose_IDM_path(state=state)
1226 | else:
1227 | self.ToolButton_path_ok_IDM.setEnabled(True)
1228 | if state == 'force':
1229 | self.confirm_IDM_path(source='chosen', state=state)
1230 |
1231 | def choose_IDM_path(self, state=None):
1232 | file_name, _ = QFileDialog.getOpenFileName(self, '选取IDMan.exe', '', 'IDMan.exe (IDMan.exe)')
1233 | if file_name:
1234 | if os.path.isfile(file_name) and os.path.basename(file_name) == 'IDMan.exe':
1235 | self.IDM_path = file_name
1236 | self.confirm_IDM_path(source='chosen', state=state)
1237 |
1238 | def LineEdit_path_IDM_change(self):
1239 | self.ToolButton_path_ok_IDM.setEnabled(True)
1240 | self.IDM_path = self.LineEdit_path_IDM.text()
1241 |
1242 | def LineEdit_url_aria2_change(self, source=None):
1243 | if source == 'start':
1244 | self.LineEdit_url_aria2.setText(self.Aria2_url)
1245 | else:
1246 | self.Aria2_url = self.LineEdit_url_aria2.text()
1247 | self.save_config('Aria2_url', self.Aria2_url)
1248 |
1249 | def clear_download_data(self):
1250 | self.download_data = {}
1251 | self.ListWidget_data.clear()
1252 |
1253 | def export_download_data(self):
1254 | try:
1255 | exported_data = QtWidgets.QFileDialog.getSaveFileName(self,
1256 | "导出下载日志",
1257 | os.path.expanduser('~') + '\\document\\',
1258 | "JSON文件 (*.json)")
1259 | with open(exported_data[0], 'w') as f:
1260 | json.dump(self.download_data, f)
1261 | except:
1262 | pass
1263 |
1264 | def eventFilter(self, source, event):
1265 | if event.type() == QEvent.KeyPress:
1266 | if source == self.TableWidget:
1267 | if event.key() == Qt.Key_Delete:
1268 | self.delete_item()
1269 | return True
1270 | if source == self.TableWidget_finished:
1271 | if event.key() == Qt.Key_Delete:
1272 | self.delete_item_finished()
1273 | return True
1274 | if event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_R:
1275 | self.redo_item()
1276 | return True
1277 | return super().eventFilter(source, event)
1278 |
1279 | def redo_item(self):
1280 | selected_items = self.TableWidget_finished.selectedItems()
1281 | if selected_items:
1282 | title = '是否重新处理选中项'
1283 | content = '将选中的项重新添加至队列'
1284 | w = Dialog(title, content, self)
1285 | if w.exec():
1286 | selected_items = self.TableWidget_finished.selectedItems()
1287 | # 获取选中项所在的行号并移除行
1288 | remove = set()
1289 | for item in selected_items:
1290 | row = item.row()
1291 | remove.add(row)
1292 | # 将行从最大索引向最小索引的顺序删除,以防止索引无效
1293 | for row in sorted(remove, reverse=True):
1294 | self.TableWidget.setRowCount(self.TableWidget.rowCount() + 1)
1295 | for index in range(8):
1296 | i = self.TableWidget_finished.item(row, index).text()
1297 | self.TableWidget.setItem(self.TableWidget.rowCount() - 1, index, QTableWidgetItem(i))
1298 | self.TableWidget_finished.removeRow(row)
1299 | self.list.append(self.list_finish[row - 1])
1300 | self.list_finish.pop(row - 1)
1301 | self.TableWidget_finished.clearSelection()
1302 | if not self.Download_running:
1303 | self.start()
1304 | else:
1305 | pass
1306 |
1307 | def delete_item_finished(self):
1308 | selected_items = self.TableWidget_finished.selectedItems()
1309 | if selected_items:
1310 | title = '是否删除选中项'
1311 | content = '此操作不可逆'
1312 | w = Dialog(title, content, self)
1313 | if w.exec():
1314 | selected_items = self.TableWidget_finished.selectedItems()
1315 | # 获取选中项所在的行号并移除行
1316 | remove = set()
1317 | for item in selected_items:
1318 | row = item.row()
1319 | remove.add(row)
1320 | # 将行从最大索引向最小索引的顺序删除,以防止索引无效
1321 | for row in sorted(remove, reverse=True):
1322 | self.TableWidget_finished.removeRow(row)
1323 | self.list_finish.pop(row - 1)
1324 | self.TableWidget_finished.clearSelection()
1325 | else:
1326 | pass
1327 |
1328 | def show_finish_menu(self, pos):
1329 |
1330 | global_pos = self.TableWidget_finished.mapToGlobal(pos)
1331 | menu = RoundMenu(parent=self)
1332 | menu.setEnabled(True)
1333 | menu.setEnabled(True)
1334 | delete = Action(FluentIcon.DELETE, '删除所选项', shortcut='Delete')
1335 | delete.triggered.connect(self.delete_item_finished)
1336 | redo = Action(FluentIcon.CANCEL, '重新处理所选项', shortcut='Ctrl+R')
1337 | redo.triggered.connect(self.redo_item)
1338 | menu.addAction(delete)
1339 | menu.addAction(redo)
1340 | menu.exec(global_pos, aniType=MenuAnimationType.DROP_DOWN)
1341 |
1342 | def delete_item(self):
1343 | selected_items = self.TableWidget.selectedItems()
1344 | if selected_items:
1345 | title = '是否删除选中项'
1346 | content = '此操作不可逆, 且此操作对于正在处理的任务无效'
1347 | w = Dialog(title, content, self)
1348 | if w.exec():
1349 | selected_items = self.TableWidget.selectedItems()
1350 | # 获取选中项所在的行号并移除行
1351 | remove = set()
1352 | for item in selected_items:
1353 | row = item.row()
1354 | remove.add(row)
1355 | # 将行从最大索引向最小索引的顺序删除,以防止索引无效
1356 | for row in sorted(remove, reverse=True):
1357 | if not row == 1:
1358 | self.TableWidget.removeRow(row)
1359 | self.list.pop(row - 1)
1360 | self.TableWidget.clearSelection()
1361 | else:
1362 | pass
1363 |
1364 | def show_list_menu(self, pos):
1365 |
1366 | global_pos = self.TableWidget.mapToGlobal(pos)
1367 | menu = RoundMenu(parent=self)
1368 | delete = Action(FluentIcon.DELETE, '删除所选项')
1369 | delete.triggered.connect(self.delete_item)
1370 | menu.addAction(delete)
1371 | menu.exec(global_pos, aniType=MenuAnimationType.DROP_DOWN)
1372 |
1373 | def about(self):
1374 | self.about_window = AboutWindow()
1375 | self.about_window.setupUi(QWidget)
1376 | self.about_window.show()
1377 |
1378 | def get_windows_theme_color(self):
1379 | try:
1380 | key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\DWM")
1381 | value, type_ = winreg.QueryValueEx(key, "AccentColor")
1382 | winreg.CloseKey(key)
1383 | if type_ == winreg.REG_DWORD:
1384 | r = value % 256
1385 | g = (value >> 8) % 256
1386 | b = (value >> 16) % 256
1387 | theme_color = [r, g, b]
1388 | else:
1389 | theme_color = None
1390 | except:
1391 | theme_color = None
1392 | return theme_color
1393 |
1394 | def set_DISPLAY_MODE(self):
1395 | global DISPLAY_MODE
1396 | try:
1397 | key_path = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
1398 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key:
1399 | value_name = "AppsUseLightTheme"
1400 | value, _ = winreg.QueryValueEx(key, value_name)
1401 | DISPLAY_MODE = value
1402 | except:
1403 | DISPLAY_MODE = 1
1404 |
--------------------------------------------------------------------------------
/URL.json:
--------------------------------------------------------------------------------
1 | {"url_json_Left": ["https://s-file-3.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/", "https://s-file-2.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/", "https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/details/"], "url_PDF_current_Left": ["https://r1-ndr.ykt.cbern.com.cn/edu_product", "https://r2-ndr.ykt.cbern.com.cn/edu_product", "https://r3-ndr.ykt.cbern.com.cn/edu_product"], "url_PDF_A_Left": ["https://c1.ykt.cbern.com.cn/edu_product"], "url_PDF_B_Left": ["https://v1.ykt.cbern.com.cn", "https://v2.ykt.cbern.com.cn", "https://v3.ykt.cbern.com.cn"]}
--------------------------------------------------------------------------------
/__pycache__/DOWNLOAD_FUNC.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJackMC/Teaching-Material-Download-Manager/d7b467e8300158a13b4f0c4fbca520a315c83a1c/__pycache__/DOWNLOAD_FUNC.cpython-39.pyc
--------------------------------------------------------------------------------
/__pycache__/UI.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJackMC/Teaching-Material-Download-Manager/d7b467e8300158a13b4f0c4fbca520a315c83a1c/__pycache__/UI.cpython-39.pyc
--------------------------------------------------------------------------------
/__pycache__/resources_rc.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJackMC/Teaching-Material-Download-Manager/d7b467e8300158a13b4f0c4fbca520a315c83a1c/__pycache__/resources_rc.cpython-39.pyc
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {"download_mode": 1, "Aria2_url": "", "open_folder": true, "folder": "", "save_mode": 1, "first_open": true, "chunk_size": 1024, "IDM_path": "", "SegmentedWidget_show": "basic_info"}
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | Copyright 2023 by RicardoJackMC
5 | Teaching Material Download Manager 使用 GPLv3 许可证
6 | 本文件是 Teaching Material Download Manager 的一部分
7 | 请自行前往
8 | https://github.com/RicardoJackMC/Teaching-Material-Download-Manager
9 | 或
10 | https://gitee.com/RicardoJackMC/Teaching-Material-Download-Manager
11 | 根据版本号校验本文件MD5
12 | """
13 | import multiprocessing
14 |
15 | import UI
16 | import DOWNLOAD_FUNC
17 | from multiprocessing import Process, Queue, freeze_support
18 | import sys
19 | from PyQt5 import QtCore, QtWidgets
20 | from PyQt5.QtCore import Qt
21 | from PyQt5.QtWidgets import QWidget, QApplication
22 | from qfluentwidgets import setTheme, Theme
23 |
24 |
25 | def start_ui(queue, queue_admin, queue_command):
26 | print('ui')
27 | QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
28 | QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
29 | QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
30 | QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
31 | app = QtWidgets.QApplication(sys.argv)
32 | ui = UI.Ui_MainWindow()
33 | ui.queue = queue
34 | ui.queue_admin = queue_admin
35 | ui.queue_command = queue_command
36 |
37 | ui.set_DISPLAY_MODE()
38 | if UI.DISPLAY_MODE == 1:
39 | setTheme(Theme.LIGHT)
40 | else:
41 | setTheme(Theme.DARK)
42 |
43 | ui.setupUi(QWidget)
44 | ui.show()
45 | ui.welcome_dialog()
46 | app.exec_()
47 |
48 |
49 | def start_manager(queue, queue_admin, queue_command):
50 | print('manager')
51 | manager = DOWNLOAD_FUNC.Downloader()
52 | manager.queue = queue
53 | manager.queue_admin = queue_admin
54 | manager.queue_command = queue_command
55 | manager.run()
56 |
57 |
58 | if __name__ == '__main__':
59 | freeze_support()
60 | queue = Queue()
61 | queue_admin = Queue()
62 | queue_command = Queue()
63 | Manager_Process = Process(target=start_manager, args=(queue, queue_admin, queue_command,))
64 | UI_Process = Process(target=start_ui, args=(queue, queue_admin, queue_command,))
65 | Manager_Process.start()
66 | UI_Process.start()
67 | Manager_Process.join()
68 | UI_Process.join()
69 |
--------------------------------------------------------------------------------
/recourse/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJackMC/Teaching-Material-Download-Manager/d7b467e8300158a13b4f0c4fbca520a315c83a1c/recourse/logo.ico
--------------------------------------------------------------------------------
/recourse/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJackMC/Teaching-Material-Download-Manager/d7b467e8300158a13b4f0c4fbca520a315c83a1c/recourse/logo.png
--------------------------------------------------------------------------------
/recourse/pic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJackMC/Teaching-Material-Download-Manager/d7b467e8300158a13b4f0c4fbca520a315c83a1c/recourse/pic.png
--------------------------------------------------------------------------------
/recourse/qfluentwidgets_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJackMC/Teaching-Material-Download-Manager/d7b467e8300158a13b4f0c4fbca520a315c83a1c/recourse/qfluentwidgets_logo.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | PyQt5==5.15.9
2 | requests==2.31.0
3 | PyQt-Fluent-Widgets==1.1.9
--------------------------------------------------------------------------------
/resource.qrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJackMC/Teaching-Material-Download-Manager/d7b467e8300158a13b4f0c4fbca520a315c83a1c/resource.qrc
--------------------------------------------------------------------------------