├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── _config.yml ├── app_icon.icns ├── app_icon.ico ├── build_app.spec ├── build_exe.spec ├── lanzou ├── __init__.py ├── api │ ├── LICENSE │ ├── __init__.py │ ├── core.py │ ├── extra.py │ ├── models.py │ ├── types.py │ └── utils.py ├── browser_cookie3_n.py ├── debug.py ├── gui │ ├── __init__.py │ ├── config.py │ ├── dialogs │ │ ├── __init__.py │ │ ├── about.py │ │ ├── captcha.py │ │ ├── delete.py │ │ ├── info.py │ │ ├── login.py │ │ ├── merge_file.py │ │ ├── move.py │ │ ├── rec_folder.py │ │ ├── rename.py │ │ ├── setpwd.py │ │ ├── setting.py │ │ └── upload.py │ ├── gui.py │ ├── models.py │ ├── others.py │ ├── qss.py │ ├── src.py │ ├── todo.md │ ├── ui.py │ └── workers │ │ ├── __init__.py │ │ ├── desc.py │ │ ├── down.py │ │ ├── folders.py │ │ ├── login.py │ │ ├── manager.py │ │ ├── more.py │ │ ├── pwd.py │ │ ├── recovery.py │ │ ├── refresh.py │ │ ├── rename.py │ │ ├── rm.py │ │ ├── share.py │ │ ├── update.py │ │ └── upload.py └── login_assister.py ├── main.py ├── requirements.txt ├── setup.py └── version_info.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # other 132 | config.ini 133 | config.pkl 134 | config.pickle 135 | .directory 136 | .idea/ 137 | *.log 138 | 139 | src/ 140 | .config 141 | *.bak 142 | login_assister.exe 143 | 144 | # MacOS 145 | .DS_Store 146 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "lanzou-gui", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "python.linting.pylintArgs": [ 4 | "--logging-format-style=new ", 5 | "--extension-pkg-whitelist=PyQt6", 6 | "--max-line-length=120", 7 | "--disable", 8 | "no-member, arguments-differ, unused-argument, attribute-defined-outside-init, broad-except, missing-docstring, invalid-name, no-self-use, too-few-public-methods, too-many-ancestors, too-many-instance-attributes, too-many-arguments, line-too-long, protected-access" 9 | ], 10 | "files.autoSave": "afterDelay", 11 | "editor.formatOnSave": false, 12 | "[python]": { 13 | "editor.formatOnPaste": false, 14 | "editor.codeActionsOnSave": { 15 | "source.organizeImports": false, 16 | }, 17 | }, 18 | "python.pythonPath": "/Users/rach/miniconda3/bin/python", 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 rachpt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

- 蓝奏云 GUI -

6 | 7 |

8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |

16 | 17 | - 本项目使用`PyQt6`实现图形界面,可以完成蓝奏云的大部分功能 18 | 19 | - 得益于 API 的功能,~~可以间接突破单文件最大 100MB 的限制~~(已关闭),可以上传任意后缀文件,同时增加了批量上传/下载的功能 20 | 21 | - `Python` 依赖见[requirements.txt](https://github.com/rachpt/lanzou-gui/blob/master/requirements.txt),[releases](https://github.com/rachpt/lanzou-gui/releases) 有打包好了的 Windows 可执行程序,但**可能不是最新的** 22 | 23 | # 预览 24 | 25 | ![lanzou-gui_screenshot](https://i.loli.net/2020/07/24/DmRBtuAjhikGep8.png) 26 | 27 | [![what's-new-v0.3.6.gif](https://i.loli.net/2021/01/03/UCkicu6H7QeOyMs.gif)](https://files.catbox.moe/o2b3q1.webp) 28 | 29 | 30 | # 说明 31 | - 默认并发上传下载任务为3,可以自行设置,单个文件还是单线程的; 32 | 33 | - 文件可以直接拖拽到软件界面上传,也可以使用对话框选择; 34 | 35 | - 文件夹最多**4级**,这是蓝奏云的限制; 36 | 37 | - 文件上传后不能改名,同时最好不要创建相同名字的文件夹; 38 | 39 | - 更多说明与详细界面预览详见 [WiKi](https://github.com/rachpt/lanzou-gui/wiki)。 40 | 41 | 42 | # 其他 43 | 44 | Arch Linux [AUR](https://aur.archlinux.org/packages/lanzou-gui/),感谢 [@bruceutut](https://aur.archlinux.org/account/bruceutut) 维护。 45 | 46 | ```sh 47 | # Arch 系 Linux 安装命令 48 | yay -S lanzou-gui 49 | ``` 50 | 51 | python >= 3.6。 52 | 53 | [Gitee 镜像 repo](https://gitee.com/rachpt/lanzou-gui),[命令行版本](https://github.com/zaxtyson/LanZouCloud-CMD)。 54 | 55 | 本项目的目的旨在学习 `PyQt6` 在开发桌面程序方面的应用,如需进行其他目的使用,请遵照许可协议 Fork,使用本软件所造成的一切后果与本人无关。 56 | 57 | # 致谢 58 | 59 | [zaxtyson/LanZouCloud-API](https://github.com/zaxtyson/LanZouCloud-API) 60 | 61 | 62 | # Licenses 63 | 64 | lanzou-gui: Copyright (c) [rachpt](https://gitee.com/rachpt/). See the [MIT LICENSE](https://github.com/rachpt/lanzou-gui/blob/master/LICENSE) for details. 65 | 66 | lanzou-api: Copyright (c) [zaxtyson](https://github.com/zaxtyson/). [MIT LICENSE](https://github.com/zaxtyson/LanZouCloud-API/blob/master/LICENSE). 67 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | -------------------------------------------------------------------------------- /app_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachpt/lanzou-gui/e776633ee80a690994f08908cddbd4714896a2c7/app_icon.icns -------------------------------------------------------------------------------- /app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachpt/lanzou-gui/e776633ee80a690994f08908cddbd4714896a2c7/app_icon.ico -------------------------------------------------------------------------------- /build_app.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | # 本文件用于打包 MacOS 应用 4 | # pyinstaller --clean --noconfirm build_app.spec 5 | 6 | version = '0.5.1' 7 | block_cipher = None 8 | 9 | 10 | a = Analysis(['main.py'], 11 | pathex=['.'], 12 | binaries=[], 13 | datas=[], 14 | hiddenimports=[], 15 | hookspath=[], 16 | runtime_hooks=[], 17 | excludes=['./lanzou/gui/login_assister.py', ], 18 | win_no_prefer_redirects=False, 19 | win_private_assemblies=False, 20 | cipher=block_cipher, 21 | noarchive=False) 22 | 23 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 24 | exe = EXE(pyz, 25 | a.scripts, 26 | a.binaries, 27 | a.zipfiles, 28 | a.datas, 29 | [], 30 | name='lanzou-gui', 31 | debug=False, 32 | bootloader_ignore_signals=False, 33 | strip=False, 34 | upx=True, 35 | upx_exclude=[], 36 | runtime_tmpdir=None, 37 | console=False , 38 | icon='app_icon.icns') 39 | app = BUNDLE(exe, 40 | name='lanzou-gui.app', 41 | icon='./app_icon.icns', 42 | info_plist={ 43 | 'CFBundleDevelopmentRegion': 'Chinese', 44 | 'CFBundleIdentifier': "cn.rachpt.lanzou-gui", 45 | 'CFBundleVersion': version, 46 | 'CFBundleShortVersionString': version, 47 | 'NSHumanReadableCopyright': u"Copyright © 2022, rachpt, All Rights Reserved" 48 | }, 49 | bundle_identifier=None) 50 | -------------------------------------------------------------------------------- /build_exe.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | # 本文件用于打包 Windows 程序 4 | # pyinstaller --clean --noconfirm build_exe.spec 5 | 6 | block_cipher = None 7 | 8 | 9 | a = Analysis(['main.py'], 10 | pathex=['.'], 11 | binaries=[], 12 | datas=[], 13 | hiddenimports=[], 14 | hookspath=[], 15 | runtime_hooks=[], 16 | excludes=['./lanzou/gui/login_assister.py', ], 17 | win_no_prefer_redirects=False, 18 | win_private_assemblies=False, 19 | cipher=block_cipher, 20 | noarchive=False) 21 | 22 | a.binaries = [x for x in a.binaries if 'login_assister' not in x[0]] 23 | 24 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 25 | exe = EXE(pyz, 26 | a.scripts, 27 | [], 28 | exclude_binaries=True, 29 | name='lanzou-gui', 30 | debug=False, 31 | bootloader_ignore_signals=False, 32 | strip=False, 33 | upx=True, 34 | console=False, 35 | version='./version_info.txt', 36 | icon='./app_icon.ico') 37 | coll = COLLECT(exe, 38 | a.binaries, 39 | a.zipfiles, 40 | a.datas, 41 | strip=False, 42 | upx=True, 43 | upx_exclude=[], 44 | name='lanzou-gui') 45 | -------------------------------------------------------------------------------- /lanzou/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['api', 'gui'] 2 | 3 | """ 4 | 是否开启使用 PyQtWebEngine 辅助获取cookie 5 | pyinstaller 打包 PyQtWebEngine 会使体积变大很多 6 | False 时移除 lanzou 目录下 login_assister.py 7 | """ 8 | USE_WEB_ENG = False 9 | -------------------------------------------------------------------------------- /lanzou/api/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 zaxtyson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lanzou/api/__init__.py: -------------------------------------------------------------------------------- 1 | from lanzou.api.core import LanZouCloud 2 | 3 | version = '2.6.7' 4 | 5 | 6 | def why_error(code): 7 | """错误原因""" 8 | if code == LanZouCloud.URL_INVALID: 9 | return '分享链接无效' 10 | elif code == LanZouCloud.LACK_PASSWORD: 11 | return '缺少提取码' 12 | elif code == LanZouCloud.PASSWORD_ERROR: 13 | return '提取码错误' 14 | elif code == LanZouCloud.FILE_CANCELLED: 15 | return '分享链接已失效' 16 | elif code == LanZouCloud.ZIP_ERROR: 17 | return '解压过程异常' 18 | elif code == LanZouCloud.NETWORK_ERROR: 19 | return '网络连接异常' 20 | elif code == LanZouCloud.CAPTCHA_ERROR: 21 | return '验证码错误' 22 | else: 23 | return f'未知错误 {code}' 24 | 25 | 26 | __all__ = ['utils', 'types', 'models', 'LanZouCloud', 'version'] 27 | -------------------------------------------------------------------------------- /lanzou/api/extra.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | from random import choice 4 | 5 | from lanzou.api.utils import USER_AGENT 6 | 7 | timeout = 2 8 | 9 | def get_short_url(url: str): 10 | """短链接生成器""" 11 | headers = {'User-Agent': USER_AGENT} 12 | short_url = "" 13 | api_infos = ['http://xuewaurl.cn/user/info', 'http://yldwz.cn/user/info', 'http://knurl.cn/user/info'] 14 | apis = ['http://pay.jump-api.cn/tcn/web/test', 'http://pay.jump-api.cn/urlcn/web/test'] # 新浪、腾讯 15 | try: 16 | http = requests.get(choice(api_infos), verify=False, headers=headers, timeout=timeout) 17 | infos = http.json() 18 | 19 | uid = infos["uid"] 20 | username = infos["username"] 21 | token = infos["token"] 22 | site_id = infos["site_id"] 23 | role = infos["role"] 24 | fid = infos["fid"] 25 | 26 | post_data = { 27 | "uid": uid, 28 | "username": username, 29 | "token": token, 30 | "site_id": site_id, 31 | "role": role, 32 | "fid": fid, 33 | "url_long": url 34 | } 35 | for api in apis: 36 | resp = requests.post(api, data=post_data, verify=False, headers=headers, timeout=timeout) 37 | if resp.text.startswith('http'): 38 | short_url = resp.text 39 | break 40 | except: pass 41 | 42 | if not short_url: 43 | chinaz_api = 'http://tool.chinaz.com/tools/dwz.aspx' 44 | post_data = {"longurl":url, "aliasurl":""} 45 | try: 46 | html = requests.post(chinaz_api, data=post_data, verify=False, headers=headers, timeout=timeout).text 47 | short_url = re.findall('id="shorturl">(http[^<]*?)', html) 48 | if short_url: 49 | short_url = short_url[0] 50 | except: pass 51 | return short_url 52 | -------------------------------------------------------------------------------- /lanzou/api/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | 容器类,用于储存文件、文件夹,支持 list 的操作,同时支持许多方法方便操作元素 3 | 元素类型为 namedtuple,至少拥有 name id 两个属性才能放入容器 4 | """ 5 | 6 | __all__ = ['FileList', 'FolderList'] 7 | 8 | 9 | class ItemList: 10 | """具有 name, id 属性对象的列表""" 11 | 12 | def __init__(self): 13 | self._items = [] 14 | 15 | def __len__(self): 16 | return len(self._items) 17 | 18 | def __getitem__(self, index): 19 | return self._items[index] 20 | 21 | def __iter__(self): 22 | return iter(self._items) 23 | 24 | def __repr__(self): 25 | return f"" 26 | 27 | def __lt__(self, other): 28 | """用于路径 List 之间排序""" 29 | return '/'.join(i.name for i in self) < '/'.join(i.name for i in other) 30 | 31 | @property 32 | def name_id(self): 33 | """所有 item 的 name-id 列表,兼容旧版""" 34 | return {it.name: it.id for it in self} 35 | 36 | @property 37 | def all_name(self): 38 | """所有 item 的 name 列表""" 39 | return [it.name for it in self] 40 | 41 | def append(self, item): 42 | """在末尾插入元素""" 43 | self._items.append(item) 44 | 45 | def index(self, item): 46 | """获取索引""" 47 | return self._items.index(item) 48 | 49 | def insert(self, pos, item): 50 | """指定位置插入元素""" 51 | self._items.insert(pos, item) 52 | 53 | def clear(self): 54 | """清空元素""" 55 | self._items.clear() 56 | 57 | def filter(self, condition) -> list: 58 | """筛选出满足条件的 item 59 | condition(item) -> True 60 | """ 61 | return [it for it in self if condition(it)] 62 | 63 | def find_by_name(self, name: str): 64 | """使用文件名搜索(仅返回首个匹配项)""" 65 | for item in self: 66 | if name == item.name: 67 | return item 68 | return None 69 | 70 | def find_by_id(self, fid: int): 71 | """使用 id 搜索(精确)""" 72 | for item in self: 73 | if fid == item.id: 74 | return item 75 | return None 76 | 77 | def pop_by_id(self, fid): 78 | for item in self: 79 | if item.id == fid: 80 | self._items.remove(item) 81 | return item 82 | return None 83 | 84 | def update_by_id(self, fid, **kwargs): 85 | """通过 id 搜索元素并更新""" 86 | item = self.find_by_id(fid) 87 | pos = self.index(item) 88 | data = item._asdict() 89 | data.update(kwargs) 90 | self._items[pos] = item.__class__(**data) 91 | 92 | 93 | class FileList(ItemList): 94 | """文件列表类""" 95 | pass 96 | 97 | 98 | class FolderList(ItemList): 99 | """文件夹列表类""" 100 | pass 101 | -------------------------------------------------------------------------------- /lanzou/api/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | API 处理后返回的数据类型 3 | """ 4 | 5 | from collections import namedtuple 6 | from lanzou.api.models import FolderList 7 | 8 | 9 | File = namedtuple('File', ['name', 'id', 'time', 'size', 'type', 'downs', 'has_pwd', 'has_des']) 10 | Folder = namedtuple('Folder', ['name', 'id', 'has_pwd', 'desc']) 11 | FolderId = namedtuple('FolderId', ['name', 'id', 'desc', 'now']) 12 | RecFile = namedtuple('RecFile', ['name', 'id', 'type', 'size', 'time']) 13 | RecFolder = namedtuple('RecFolder', ['name', 'id', 'size', 'time', 'files']) 14 | FileDetail = namedtuple('FileDetail', ['code', 'name', 'size', 'type', 'time', 'desc', 'pwd', 'url', 'durl'], 15 | defaults=(0, *[''] * 8)) 16 | ShareInfo = namedtuple('ShareInfo', ['code', 'name', 'url', 'pwd', 'desc', 'time', 'size'], defaults=(0, *[''] * 6)) 17 | DirectUrlInfo = namedtuple('DirectUrlInfo', ['code', 'name', 'durl']) 18 | FolderInfo = namedtuple('Folder', ['name', 'id', 'pwd', 'time', 'desc', 'url', 'size', 'size_int', 'count'], 19 | defaults=(*('',) * 7, 0, 0)) 20 | FileInFolder = namedtuple('FileInFolder', ['name', 'time', 'size', 'type', 'url', 'pwd'], defaults=('',) * 6) 21 | FolderDetail = namedtuple('FolderDetail', ['code', 'folder', 'files', 'sub_folders'], defaults=(0, FolderInfo(), FileInFolder(), FolderList())) 22 | 23 | # gui 提取界面 name.setData 24 | ShareItem = namedtuple('ShareItem', ['item', 'all', 'count', 'parrent'], defaults=('', None, 1, [])) -------------------------------------------------------------------------------- /lanzou/api/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | API 处理网页数据、数据切片时使用的工具 3 | """ 4 | 5 | import os 6 | import pickle 7 | import re 8 | from typing import Tuple 9 | from datetime import timedelta, datetime 10 | from random import uniform, choices, sample, shuffle, choice 11 | import requests 12 | 13 | from lanzou.debug import logger 14 | 15 | 16 | __all__ = ['remove_notes', 'name_format', 'time_format', 'is_name_valid', 'is_file_url', 17 | 'is_folder_url', 'big_file_split', 'un_serialize', 'let_me_upload', 'USER_AGENT', 18 | 'sum_files_size', 'convert_file_size_to_str', 'calc_acw_sc__v2'] 19 | 20 | 21 | USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0' 22 | 23 | 24 | headers = { 25 | 'User-Agent': USER_AGENT, 26 | # 'Referer': 'https://pan.lanzous.com', # 可以没有 27 | 'Accept-Language': 'zh-CN,zh;q=0.9', 28 | } 29 | 30 | 31 | def remove_notes(html: str) -> str: 32 | """删除网页的注释""" 33 | # 去掉 html 里面的 // 和 注释,防止干扰正则匹配提取数据 34 | # 蓝奏云的前端程序员喜欢改完代码就把原来的代码注释掉,就直接推到生产环境了 =_= 35 | html = re.sub(r'|\s+//\s*.+', '', html) # html 注释 36 | html = re.sub(r'(.+?[,;])\s*//.+', r'\1', html) # js 注释 37 | return html 38 | 39 | 40 | def name_format(name: str) -> str: 41 | """去除非法字符""" 42 | name = name.replace(u'\xa0', ' ').replace(u'\u3000', ' ').replace(' ', ' ') # 去除其它字符集的空白符,去除重复空白字符 43 | return re.sub(r'[$%^!*<>)(+=`\'\"/:;,?]', '', name) 44 | 45 | def convert_file_size_to_int(size_str: str) -> int: 46 | """文件大小描述转化为字节大小""" 47 | if 'G' in size_str: 48 | size_int = float(size_str.replace('G', '')) * (1 << 30) 49 | elif 'M' in size_str: 50 | size_int = float(size_str.replace('M', '')) * (1 << 20) 51 | elif 'K' in size_str: 52 | size_int = float(size_str.replace('K', '')) * (1 << 10) 53 | elif 'B' in size_str: 54 | size_int = float(size_str.replace('B', '')) 55 | else: 56 | size_int = 0 57 | logger.debug(f"Unknown size: {size_str}") 58 | return int(size_int) 59 | 60 | 61 | def sum_files_size(files: object) -> int: 62 | """计算文件夹中所有文件的大小, [files,]: FileInFolder""" 63 | # 此处的 size 用于全选下载判断、展示文件夹大小 64 | total = 0 65 | for file_ in files: 66 | size_str = file_.size 67 | total += convert_file_size_to_int(size_str) 68 | return int(total) 69 | 70 | 71 | def convert_file_size_to_str(total: int) -> str: 72 | if total < 1 << 10: 73 | size = "{:.2f} B".format(total) 74 | elif total < 1 << 20: 75 | size = "{:.2f} K".format(total / (1 << 10)) 76 | elif total < 1 << 30: 77 | size = "{:.2f} M".format(total / (1 << 20)) 78 | else: 79 | size = "{:.2f} G".format(total / (1 << 30)) 80 | 81 | return size 82 | 83 | 84 | def time_format(time_str: str) -> str: 85 | """输出格式化时间 %Y-%m-%d""" 86 | if '秒前' in time_str or '分钟前' in time_str or '小时前' in time_str: 87 | return datetime.today().strftime('%Y-%m-%d') 88 | elif '昨天' in time_str: 89 | return (datetime.today() - timedelta(days=1)).strftime('%Y-%m-%d') 90 | elif '前天' in time_str: 91 | return (datetime.today() - timedelta(days=2)).strftime('%Y-%m-%d') 92 | elif '天前' in time_str: 93 | days = time_str.replace(' 天前', '') 94 | return (datetime.today() - timedelta(days=int(days))).strftime('%Y-%m-%d') 95 | else: 96 | return time_str 97 | 98 | 99 | def is_name_valid(filename: str) -> bool: 100 | """检查文件名是否允许上传""" 101 | 102 | valid_suffix_list = ('ppt', 'xapk', 'ke', 'azw', 'cpk', 'gho', 'dwg', 'db', 'docx', 'deb', 'e', 'ttf', 'xls', 'bat', 103 | 'crx', 'rpm', 'txf', 'pdf', 'apk', 'ipa', 'txt', 'mobi', 'osk', 'dmg', 'rp', 'osz', 'jar', 104 | 'ttc', 'z', 'w3x', 'xlsx', 'cetrainer', 'ct', 'rar', 'mp3', 'pptx', 'mobileconfig', 'epub', 105 | 'imazingapp', 'doc', 'iso', 'img', 'appimage', '7z', 'rplib', 'lolgezi', 'exe', 'azw3', 'zip', 106 | 'conf', 'tar', 'dll', 'flac', 'xpa', 'lua', 'cad', 'hwt', 'accdb', 'ce', 'xmind', 'enc', 'bds', 'bdi', 'ssf', 'it', 'gz') 107 | 108 | return filename.split('.')[-1].lower() in valid_suffix_list 109 | 110 | 111 | def is_file_url(share_url: str) -> bool: 112 | """判断是否为文件的分享链接""" 113 | base_pat = r'https?://(\w[-\w]*\.)?lanzou[a-z].com/.+' 114 | user_pat = r'https?://(\w[-\w]*\.)?lanzou[a-z].com/i[a-zA-Z0-9]{5,}(\?webpage=[a-zA-Z0-9]+?)?/?' # 普通用户 URL 规则 115 | if not re.fullmatch(base_pat, share_url): 116 | return False 117 | elif re.fullmatch(user_pat, share_url): 118 | return True 119 | else: # VIP 用户的 URL 很随意 120 | try: 121 | html = requests.get(share_url, headers=headers).text 122 | html = remove_notes(html) 123 | return True if re.search(r'class="fileinfo"|id="file"|文件描述', html) else False 124 | except (requests.RequestException, Exception) as e: 125 | logger.error(f"Unexpected error: e={e}") 126 | return False 127 | 128 | 129 | def is_folder_url(share_url: str) -> bool: 130 | """判断是否为文件夹的分享链接""" 131 | base_pat = r'https?://(\w[-\w]*\.)?lanzou[a-z].com/.+' 132 | user_pat = r'https?://(\w[-\w]*\.)?lanzou[a-z].com/(/s/)?b[a-zA-Z0-9]{7,}/?' 133 | if not re.fullmatch(base_pat, share_url): 134 | return False 135 | elif re.fullmatch(user_pat, share_url): 136 | return True 137 | else: # VIP 用户的 URL 很随意 138 | try: 139 | html = requests.get(share_url, headers=headers).text 140 | html = remove_notes(html) 141 | return True if re.search(r'id="infos"', html) else False 142 | except (requests.RequestException, Exception) as e: 143 | logger.error(f"Unexpected error: e={e}") 144 | return False 145 | 146 | 147 | def un_serialize(data: bytes): 148 | """反序列化文件信息数据""" 149 | # https://github.com/zaxtyson/LanZouCloud-API/issues/65 150 | is_right_format = False 151 | if data.startswith(b"\x80\x04") and data.endswith(b"u."): 152 | is_right_format = True 153 | if data.startswith(b"\x80\x03") and data.endswith(b"u."): 154 | is_right_format = True 155 | 156 | if not is_right_format: 157 | return None 158 | try: 159 | ret = pickle.loads(data) 160 | if not isinstance(ret, dict): 161 | return None 162 | return ret 163 | except Exception as e: # 这里可能会丢奇怪的异常 164 | logger.debug(f"Pickle e={e}") 165 | return None 166 | 167 | 168 | def big_file_split(file_path: str, max_size: int = 100, start_byte: int = 0) -> Tuple[int, str]: 169 | """将大文件拆分为大小、格式随机的数据块, 可指定文件起始字节位置(用于续传) 170 | :return 数据块文件的大小和绝对路径 171 | """ 172 | file_name = os.path.basename(file_path) 173 | file_size = os.path.getsize(file_path) 174 | tmp_dir = os.path.dirname(file_path) + os.sep + '__' + '.'.join(file_name.split('.')[:-1]) 175 | 176 | if not os.path.exists(tmp_dir): 177 | os.makedirs(tmp_dir) 178 | 179 | def get_random_size() -> int: 180 | """按权重生成一个不超过 max_size 的文件大小""" 181 | reduce_size = choices([uniform(0, max_size/10), uniform(max_size/10, 2*max_size/10), uniform(4*max_size/10, 6*max_size/10), uniform(6*max_size/10, 8*max_size/10)], weights=[2, 5, 2, 1]) 182 | return round((max_size - reduce_size[0]) * 1048576) 183 | 184 | def get_random_name() -> str: 185 | """生成一个随机文件名""" 186 | # 这些格式的文件一般都比较大且不容易触发下载检测 187 | suffix_list = ('zip', 'rar', 'apk', 'ipa', 'exe', 'pdf', '7z', 'tar', 'deb', 'dmg', 'rpm', 'flac') 188 | name = list(file_name.replace('.', '').replace(' ', '')) 189 | name = name + sample('abcdefghijklmnopqrstuvwxyz', 3) + sample('1234567890', 2) 190 | shuffle(name) # 打乱顺序 191 | name = ''.join(name) + '.' + choice(suffix_list) 192 | return name_format(name) # 确保随机名合法 193 | 194 | with open(file_path, 'rb') as big_file: 195 | big_file.seek(start_byte) 196 | left_size = file_size - start_byte # 大文件剩余大小 197 | random_size = get_random_size() 198 | tmp_file_size = random_size if left_size > random_size else left_size 199 | tmp_file_path = tmp_dir + os.sep + get_random_name() 200 | 201 | chunk_size = 524288 # 512KB 202 | left_read_size = tmp_file_size 203 | with open(tmp_file_path, 'wb') as small_file: 204 | while left_read_size > 0: 205 | if left_read_size < chunk_size: # 不足读取一次 206 | small_file.write(big_file.read(left_read_size)) 207 | break 208 | # 一次读取一块,防止一次性读取占用内存 209 | small_file.write(big_file.read(chunk_size)) 210 | left_read_size -= chunk_size 211 | 212 | return tmp_file_size, tmp_file_path 213 | 214 | 215 | def let_me_upload(file_path): 216 | """允许文件上传""" 217 | file_size = os.path.getsize(file_path) / 1024 / 1024 # MB 218 | file_name = os.path.basename(file_path) 219 | 220 | big_file_suffix = ['zip', 'rar', 'apk', 'ipa', 'exe', 'pdf', '7z', 'tar', 'deb', 'dmg', 'rpm', 'flac'] 221 | small_file_suffix = big_file_suffix + ['doc', 'epub', 'mobi', 'mp3', 'ppt', 'pptx'] 222 | big_file_suffix = choice(big_file_suffix) 223 | small_file_suffix = choice(small_file_suffix) 224 | suffix = small_file_suffix if file_size < 30 else big_file_suffix 225 | new_file_path = '.'.join(file_path.split('.')[:-1]) + '.' + suffix 226 | 227 | with open(new_file_path, 'wb') as out_f: 228 | # 写入原始文件数据 229 | with open(file_path, 'rb') as in_f: 230 | chunk = in_f.read(4096) 231 | while chunk: 232 | out_f.write(chunk) 233 | chunk = in_f.read(4096) 234 | # 构建文件 "报尾" 保存真实文件名,大小 512 字节 235 | # 追加数据到文件尾部,并不会影响文件的使用,无需修改即可分享给其他人使用,自己下载时则会去除,确保数据无误 236 | # protocol=4(py3.8默认), 序列化后空字典占 42 字节 237 | padding = 512 - len(file_name.encode('utf-8')) - 42 238 | data = {'name': file_name, 'padding': b'\x00' * padding} 239 | data = pickle.dumps(data, protocol=4) 240 | out_f.write(data) 241 | return new_file_path 242 | 243 | 244 | 245 | def auto_rename(file_path) -> str: 246 | """如果文件存在,则给文件名添加序号""" 247 | if not os.path.exists(file_path): 248 | return file_path 249 | fpath, fname = os.path.split(file_path) 250 | fname_no_ext, ext = os.path.splitext(fname) 251 | flist = [f for f in os.listdir(fpath) if re.fullmatch(rf"{fname_no_ext}\(?\d*\)?{ext}", f)] 252 | count = 1 253 | while f"{fname_no_ext}({count}){ext}" in flist: 254 | count += 1 255 | return fpath + os.sep + fname_no_ext + '(' + str(count) + ')' + ext 256 | 257 | 258 | def calc_acw_sc__v2(html_text: str) -> str: 259 | arg1 = re.search(r"arg1='([0-9A-Z]+)'", html_text) 260 | arg1 = arg1.group(1) if arg1 else "" 261 | acw_sc__v2 = hex_xor(unsbox(arg1), "3000176000856006061501533003690027800375") 262 | return acw_sc__v2 263 | 264 | 265 | # 参考自 https://zhuanlan.zhihu.com/p/228507547 266 | def unsbox(str_arg): 267 | v1 = [15, 35, 29, 24, 33, 16, 1, 38, 10, 9, 19, 31, 40, 27, 22, 23, 25, 13, 6, 11, 39, 18, 20, 8, 14, 21, 32, 26, 2, 268 | 30, 7, 4, 17, 5, 3, 28, 34, 37, 12, 36] 269 | v2 = ["" for _ in v1] 270 | for idx in range(0, len(str_arg)): 271 | v3 = str_arg[idx] 272 | for idx2 in range(len(v1)): 273 | if v1[idx2] == idx + 1: 274 | v2[idx2] = v3 275 | 276 | res = ''.join(v2) 277 | return res 278 | 279 | 280 | def hex_xor(str_arg, args): 281 | res = '' 282 | for idx in range(0, min(len(str_arg), len(args)), 2): 283 | v1 = int(str_arg[idx:idx + 2], 16) 284 | v2 = int(args[idx:idx + 2], 16) 285 | v3 = format(v1 ^ v2, 'x') 286 | if len(v3) == 1: 287 | v3 = '0' + v3 288 | res += v3 289 | 290 | return res 291 | -------------------------------------------------------------------------------- /lanzou/debug.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 调试日志设置,全局常量 3 | ''' 4 | 5 | import os 6 | import logging 7 | 8 | 9 | __all__ = ['logger'] 10 | 11 | 12 | # 全局常量: USER_HOME, DL_DIR, SRC_DIR, BG_IMG, CONFIG_FILE 13 | USER_HOME = os.path.expanduser('~') 14 | if os.name == 'nt': # Windows 15 | root_dir = os.path.dirname(os.path.abspath(__file__)) 16 | root_dir = os.path.dirname(root_dir) 17 | import winreg 18 | 19 | sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' 20 | downloads_guid = '{374DE290-123F-4565-9164-39C4925E467B}' 21 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key: 22 | DL_DIR = winreg.QueryValueEx(key, downloads_guid)[0] 23 | else: # Linux and MacOS ... 24 | root_dir = USER_HOME + os.sep + '.config' + os.sep + 'lanzou-gui' 25 | if not os.path.exists(root_dir): 26 | os.makedirs(root_dir) 27 | DL_DIR = USER_HOME + os.sep + 'Downloads' 28 | 29 | SRC_DIR = root_dir + os.sep + "src" + os.sep 30 | BG_IMG = (SRC_DIR + "default_background_img.jpg").replace('\\', '/') 31 | CONFIG_FILE = root_dir + os.sep + 'config.pkl' 32 | 33 | 34 | # 日志设置 35 | log_file = root_dir + os.sep + 'debug-lanzou-gui.log' 36 | logger = logging.getLogger('lanzou') 37 | fmt_str = "%(asctime)s [%(filename)s:%(lineno)d] %(funcName)s %(levelname)s - %(message)s" 38 | logging.basicConfig(level=logging.ERROR, 39 | filename=log_file, 40 | filemode="a", 41 | format=fmt_str, 42 | datefmt="%Y-%m-%d %H:%M:%S") 43 | 44 | logging.getLogger("requests").setLevel(logging.WARNING) 45 | logging.getLogger("urllib3").setLevel(logging.WARNING) 46 | -------------------------------------------------------------------------------- /lanzou/gui/__init__.py: -------------------------------------------------------------------------------- 1 | version = '0.5.1' 2 | -------------------------------------------------------------------------------- /lanzou/gui/config.py: -------------------------------------------------------------------------------- 1 | from pickle import load, dump 2 | from lanzou.debug import CONFIG_FILE, DL_DIR 3 | 4 | __all__ = ['config'] 5 | 6 | KEY = 152 # config 加密 key 7 | 8 | 9 | default_settings = { 10 | "download_threads": 3, # 同时三个下载任务 11 | "timeout": 5, # 每个请求的超时 s(不包含下载响应体的用时) 12 | "max_size": 100, # 单个文件大小上限 MB 13 | "dl_path": DL_DIR, 14 | "time_fmt": False, # 是否使用年月日时间格式 15 | "to_tray": False, # 关闭到系统托盘 16 | "watch_clipboard": False, # 监听系统剪切板 17 | "debug": False, # 调试 18 | "set_pwd": False, 19 | "pwd": "", 20 | "set_desc": False, 21 | "desc": "", 22 | "upload_delay": 20, # 上传大文件延时 0 - 20s 23 | "allow_big_file": False, 24 | "upgrade": True 25 | } 26 | 27 | 28 | def encrypt(key, s): 29 | b = bytearray(str(s).encode("utf-8")) 30 | n = len(b) 31 | c = bytearray(n * 2) 32 | j = 0 33 | for i in range(0, n): 34 | b1 = b[i] 35 | b2 = b1 ^ key 36 | c1 = b2 % 19 37 | c2 = b2 // 19 38 | c1 = c1 + 46 39 | c2 = c2 + 46 40 | c[j] = c1 41 | c[j + 1] = c2 42 | j = j + 2 43 | return c.decode("utf-8") 44 | 45 | 46 | def decrypt(ksa, s): 47 | c = bytearray(str(s).encode("utf-8")) 48 | n = len(c) 49 | if n % 2 != 0: 50 | return "" 51 | n = n // 2 52 | b = bytearray(n) 53 | j = 0 54 | for i in range(0, n): 55 | c1 = c[j] 56 | c2 = c[j + 1] 57 | j = j + 2 58 | c1 = c1 - 46 59 | c2 = c2 - 46 60 | b2 = c2 * 19 + c1 61 | b1 = b2 ^ ksa 62 | b[i] = b1 63 | return b.decode("utf-8") 64 | 65 | 66 | def save_config(cf): 67 | with open(CONFIG_FILE, 'wb') as f: 68 | dump(cf, f) 69 | 70 | 71 | class Config: 72 | """存储登录用户信息""" 73 | def __init__(self): 74 | self._users = {} 75 | self._cookie = '' 76 | self._name = '' 77 | self._pwd = '' 78 | self._work_id = -1 79 | self._settings = default_settings 80 | 81 | def encode(self, var): 82 | if isinstance(var, dict): 83 | for k, v in var.items(): 84 | var[k] = encrypt(KEY, str(v)) 85 | elif var: 86 | var = encrypt(KEY, str(var)) 87 | return var 88 | 89 | def decode(self, var): 90 | try: 91 | if isinstance(var, dict): 92 | dvar = {} # 新开内存,否则会修改原字典 93 | for k, v in var.items(): 94 | dvar[k] = decrypt(KEY, str(v)) 95 | elif var: 96 | dvar = decrypt(KEY, var) 97 | else: 98 | dvar = None 99 | except Exception: 100 | dvar = None 101 | return dvar 102 | 103 | def update_user(self): 104 | if self._name: 105 | self._users[self._name] = (self._cookie, self._name, self._pwd, 106 | self._work_id, self._settings) 107 | save_config(self) 108 | 109 | def del_user(self, name) -> bool: 110 | name = self.encode(name) 111 | if name in self._users: 112 | del self._users[name] 113 | return True 114 | return False 115 | 116 | def change_user(self, name) -> bool: 117 | name = self.encode(name) 118 | if name in self._users: 119 | self.update_user() # 切换用户前保持目前用户信息 120 | user = self._users[name] 121 | self._cookie = user[0] 122 | self._name = user[1] 123 | self._pwd = user[2] 124 | self._work_id = user[3] 125 | self._settings = user[4] 126 | save_config(self) 127 | return True 128 | return False 129 | 130 | @property 131 | def users_name(self) -> list: 132 | return [self.decode(user) for user in self._users] 133 | 134 | def get_user_info(self, name): 135 | """返回用户名、pwd、cookie""" 136 | en_name = self.encode(name) 137 | if en_name in self._users: 138 | user_info = self._users[en_name] 139 | return (name, self.decode(user_info[2]), self.decode(user_info[0])) 140 | 141 | def default_path(self): 142 | path = default_settings['dl_path'] 143 | self._settings.update({'dl_path': path}) 144 | save_config(self) 145 | 146 | @property 147 | def default_settings(self): 148 | return default_settings 149 | 150 | @property 151 | def name(self): 152 | return self.decode(self._name) 153 | 154 | @property 155 | def pwd(self): 156 | return self.decode(self._pwd) 157 | 158 | @property 159 | def cookie(self): 160 | return self.decode(self._cookie) 161 | 162 | @cookie.setter 163 | def cookie(self, cookie): 164 | self._cookie = self.encode(cookie) 165 | save_config(self) 166 | 167 | @property 168 | def work_id(self): 169 | return self._work_id 170 | 171 | @work_id.setter 172 | def work_id(self, work_id): 173 | self._work_id = work_id 174 | save_config(self) 175 | 176 | def set_cookie(self, cookie): 177 | self._cookie = self.encode(cookie) 178 | save_config(self) 179 | 180 | def set_username(self, username): 181 | self._name = self.encode(username) 182 | save_config(self) 183 | 184 | @property 185 | def path(self): 186 | return self._settings['dl_path'] 187 | 188 | @path.setter 189 | def path(self, path): 190 | self._settings.update({'dl_path': path}) 191 | save_config(self) 192 | 193 | @property 194 | def settings(self): 195 | return self._settings 196 | 197 | @settings.setter 198 | def settings(self, settings): 199 | self._settings = settings 200 | save_config(self) 201 | 202 | def set_infos(self, infos: dict): 203 | self.update_user() # 切换用户前保持目前用户信息 204 | if "name" in infos: 205 | self._name = self.encode(infos["name"]) 206 | if "pwd" in infos: 207 | self._pwd = self.encode(infos["pwd"]) 208 | if "cookie" in infos: 209 | self._cookie = self.encode(infos["cookie"]) 210 | if "path" in infos: 211 | self._settings.update({'dl_path': infos["path"]}) 212 | if "work_id" in infos: 213 | self._work_id = infos["work_id"] 214 | if "settings" in infos: 215 | self._settings = infos["settings"] 216 | save_config(self) 217 | 218 | 219 | # 全局配置对象 220 | try: 221 | with open(CONFIG_FILE, 'rb') as c: 222 | config = load(c) 223 | except: 224 | config = Config() 225 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/__init__.py: -------------------------------------------------------------------------------- 1 | from lanzou.gui.dialogs.about import AboutDialog 2 | from lanzou.gui.dialogs.captcha import CaptchaDialog 3 | from lanzou.gui.dialogs.delete import DeleteDialog 4 | from lanzou.gui.dialogs.info import InfoDialog 5 | from lanzou.gui.dialogs.login import LoginDialog 6 | from lanzou.gui.dialogs.move import MoveFileDialog 7 | from lanzou.gui.dialogs.rec_folder import RecFolderDialog 8 | from lanzou.gui.dialogs.rename import RenameDialog 9 | from lanzou.gui.dialogs.setpwd import SetPwdDialog 10 | from lanzou.gui.dialogs.setting import SettingDialog 11 | from lanzou.gui.dialogs.upload import UploadDialog 12 | from lanzou.gui.dialogs.merge_file import MergeFileDialog 13 | 14 | 15 | __ALL__ = ['LoginDialog', 'UploadDialog', 'InfoDialog', 'RenameDialog', 16 | 'SettingDialog', 'RecFolderDialog', 'SetPwdDialog', 'MoveFileDialog', 17 | 'DeleteDialog', 'KEY', 'AboutDialog', 'CaptchaDialog', 'MergeFileDialog'] 18 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/about.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import Qt, pyqtSignal, QLine, QPoint, PYQT_VERSION_STR, QT_VERSION_STR 2 | from PyQt6.QtGui import QPixmap, QPainter, QPen 3 | from PyQt6.QtWidgets import (QPushButton, QDialog, QLabel, QFormLayout, 4 | QDialogButtonBox, QVBoxLayout, QHBoxLayout) 5 | 6 | from lanzou.gui.qss import others_style, btn_style 7 | from lanzou.debug import SRC_DIR 8 | 9 | 10 | class AboutDialog(QDialog): 11 | check_update = pyqtSignal(str, bool) 12 | 13 | def __init__(self, parent=None): 14 | super(AboutDialog, self).__init__(parent) 15 | self._ver = '' 16 | self._github = 'https://github.com/rachpt/lanzou-gui' 17 | self._api_url = 'https://github.com/zaxtyson/LanZouCloud-API' 18 | self._gitee = 'https://gitee.com/rachpt/lanzou-gui' 19 | self._home_page = 'https://rachpt.cn/lanzou-gui/' 20 | self.initUI() 21 | self.setStyleSheet(others_style) 22 | 23 | def set_values(self, version): 24 | self._ver = version 25 | self.lb_name_text.setText(f"v{version} (点击检查更新)") # 更新版本 26 | 27 | def show_update(self, ver, msg): 28 | self.lb_new_ver = QLabel("新版") # 检测新版 29 | self.lb_new_ver_msg = QLabel() 30 | self.lb_new_ver_msg.setOpenExternalLinks(True) 31 | self.lb_new_ver_msg.setWordWrap(True) 32 | if ver != '0': 33 | self.lb_name_text.setText(f"{self._ver} ➡ {ver}") 34 | self.lb_new_ver_msg.setText(msg) 35 | self.lb_new_ver_msg.setMinimumWidth(700) 36 | if self.form.rowCount() < 5: 37 | self.form.insertRow(1, self.lb_new_ver, self.lb_new_ver_msg) 38 | 39 | def initUI(self): 40 | self.setWindowTitle("关于 lanzou-gui") 41 | about = f'本项目使用PyQt6实现图形界面,可以完成蓝奏云的大部分功能
\ 42 | 得益于 API 的功能,可以间接突破单文件最大 100MB 的限制,同时增加了批量上传/下载的功能
\ 43 | Python 依赖见requirements.txt,\ 44 | releases 有打包好了的 Windows 可执行程序,但可能不是最新的' 45 | project_url = f'主页 | repo | \ 46 | mirror repo' 47 | self.logo = QLabel() # logo 48 | self.logo.setPixmap(QPixmap(SRC_DIR + "logo2.gif")) 49 | self.logo.setStyleSheet("background-color:rgb(255,255,255);") 50 | self.logo.setAlignment(Qt.AlignmentFlag.AlignCenter) 51 | self.lb_qt_ver = QLabel("依赖") # QT 版本 52 | self.lb_qt_text = QLabel(f"QT: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}") # QT 版本 53 | self.lb_name = QLabel("版本") # 版本 54 | self.lb_name_text = QPushButton("") # 版本 55 | self.lb_name_text.setToolTip("点击检查更新") 56 | ver_style = "QPushButton {border:none; background:transparent;font-weight:bold;color:blue;}" 57 | self.lb_name_text.setStyleSheet(ver_style) 58 | self.lb_name_text.clicked.connect(lambda: self.check_update.emit(self._ver, True)) 59 | self.lb_about = QLabel("关于") # about 60 | self.lb_about_text = QLabel() 61 | self.lb_about_text.setText(about) 62 | self.lb_about_text.setOpenExternalLinks(True) 63 | self.lb_author = QLabel("作者") # author 64 | self.lb_author_mail = QLabel("rachpt") 65 | self.lb_author_mail.setOpenExternalLinks(True) 66 | self.lb_update = QLabel("项目") # 更新 67 | self.lb_update_url = QLabel(project_url) 68 | self.lb_update_url.setOpenExternalLinks(True) 69 | self.buttonBox = QDialogButtonBox() 70 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 71 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Close) 72 | self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setText("关闭") 73 | self.buttonBox.rejected.connect(self.reject) 74 | self.buttonBox.setStyleSheet(btn_style) 75 | 76 | self.recommend = QLabel("
大文件推荐使用 cloud189-cli") 77 | self.recommend.setOpenExternalLinks(True) 78 | 79 | self.line = QLine(QPoint(), QPoint(550, 0)) 80 | self.lb_line = QLabel('
') 81 | 82 | vbox = QVBoxLayout() 83 | vbox.addWidget(self.logo) 84 | vbox.addStretch(1) 85 | self.form = QFormLayout() 86 | self.form.setLabelAlignment(Qt.AlignmentFlag.AlignRight) 87 | self.form.setFormAlignment(Qt.AlignmentFlag.AlignLeft) 88 | self.form.setHorizontalSpacing(40) 89 | self.form.setVerticalSpacing(15) 90 | self.form.addRow(self.lb_qt_ver, self.lb_qt_text) 91 | self.form.addRow(self.lb_name, self.lb_name_text) 92 | self.form.addRow(self.lb_update, self.lb_update_url) 93 | self.form.addRow(self.lb_author, self.lb_author_mail) 94 | self.form.addRow(self.lb_about, self.lb_about_text) 95 | self.form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) # 覆盖MacOS的默认样式 96 | vbox.addLayout(self.form) 97 | vbox.addStretch(1) 98 | vbox.addWidget(self.recommend) 99 | vbox.addWidget(self.lb_line) 100 | donate = QLabel() 101 | donate.setText("捐助我 如果你愿意") 102 | donate.setAlignment(Qt.AlignmentFlag.AlignCenter) 103 | hbox = QHBoxLayout() 104 | hbox.addStretch(2) 105 | for it in ["wechat", "alipay", "qqpay"]: 106 | lb = QLabel() 107 | lb.setPixmap(QPixmap(SRC_DIR + f"{it}.jpg")) 108 | hbox.addWidget(lb) 109 | hbox.addStretch(1) 110 | hbox.addWidget(self.buttonBox) 111 | vbox.addWidget(donate) 112 | vbox.addLayout(hbox) 113 | self.setLayout(vbox) 114 | self.setMinimumWidth(720) 115 | 116 | def paintEvent(self, event): 117 | QDialog.paintEvent(self, event) 118 | if not self.line.isNull(): 119 | painter = QPainter(self) 120 | pen = QPen(Qt.GlobalColor.red, 3) 121 | painter.setPen(pen) 122 | painter.drawLine(self.line) 123 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/captcha.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PyQt6.QtCore import Qt, pyqtSignal 3 | from PyQt6.QtGui import QPixmap 4 | from PyQt6.QtWidgets import (QLineEdit, QDialog, QLabel, QDialogButtonBox, QVBoxLayout) 5 | 6 | from lanzou.gui.qss import others_style, btn_style 7 | 8 | class CaptchaDialog(QDialog): 9 | captcha = pyqtSignal(object) 10 | 11 | def __init__(self, parent=None): 12 | super(CaptchaDialog, self).__init__(parent) 13 | self.img_path = os.getcwd() + os.sep + 'captcha.png' 14 | self.initUI() 15 | self.setStyleSheet(others_style) 16 | 17 | def show_img(self): 18 | self.captcha_pixmap = QPixmap(self.img_path) 19 | self.captcha_lb.setPixmap(self.captcha_pixmap) 20 | 21 | def handle(self, img_data): 22 | with open(self.img_path, 'wb') as f: 23 | f.write(img_data) 24 | f.flush() 25 | self.show_img() 26 | 27 | def on_ok(self): 28 | captcha = self.code.text() 29 | self.captcha.emit(captcha) 30 | if os.path.isfile(self.img_path): 31 | os.remove(self.img_path) 32 | 33 | def initUI(self): 34 | self.setWindowTitle("请输入下载验证码") 35 | 36 | self.captcha_lb = QLabel() 37 | self.captcha_pixmap = QPixmap(self.img_path) 38 | self.captcha_lb.setPixmap(self.captcha_pixmap) 39 | 40 | self.code = QLineEdit() 41 | self.code.setPlaceholderText("在此输入验证码") 42 | 43 | self.buttonBox = QDialogButtonBox() 44 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 45 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Reset|QDialogButtonBox.StandardButton.Ok|QDialogButtonBox.StandardButton.Close) 46 | self.buttonBox.button(QDialogButtonBox.StandardButton.Reset).setText("显示图片") 47 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") 48 | self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setText("关闭") 49 | self.buttonBox.setStyleSheet(btn_style) 50 | self.buttonBox.button(QDialogButtonBox.StandardButton.Reset).clicked.connect(self.show_img) 51 | self.buttonBox.accepted.connect(self.on_ok) 52 | self.buttonBox.accepted.connect(self.accept) 53 | self.buttonBox.rejected.connect(self.reject) 54 | 55 | vbox = QVBoxLayout() 56 | vbox.addWidget(self.captcha_lb) 57 | vbox.addStretch(1) 58 | vbox.addWidget(self.code) 59 | vbox.addStretch(1) 60 | vbox.addWidget(self.buttonBox) 61 | self.setLayout(vbox) 62 | self.setMinimumWidth(260) 63 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/delete.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt6.QtCore import Qt, pyqtSignal 3 | from PyQt6.QtGui import QIcon, QStandardItem, QStandardItemModel 4 | from PyQt6.QtWidgets import QDialog, QLabel, QListView, QDialogButtonBox, QVBoxLayout 5 | 6 | from lanzou.gui.others import set_file_icon 7 | from lanzou.gui.qss import dialog_qss_style 8 | from lanzou.debug import SRC_DIR 9 | 10 | 11 | class DeleteDialog(QDialog): 12 | new_infos = pyqtSignal(object) 13 | 14 | def __init__(self, infos, parent=None): 15 | super(DeleteDialog, self).__init__(parent) 16 | self.infos = infos 17 | self.out = [] 18 | self.initUI() 19 | self.setStyleSheet(dialog_qss_style) 20 | 21 | def initUI(self): 22 | self.setWindowTitle("确认删除") 23 | self.setWindowIcon(QIcon(SRC_DIR + "delete.ico")) 24 | self.layout = QVBoxLayout() 25 | self.list_view = QListView() 26 | self.list_view.setViewMode(QListView.ViewMode.ListMode) 27 | # 列表 28 | self.slm = QStandardItem() 29 | self.model = QStandardItemModel() 30 | max_len = 10 31 | count = 0 32 | for info in self.infos: 33 | if info.is_file: # 文件 34 | self.model.appendRow(QStandardItem(set_file_icon(info.name), info.name)) 35 | else: 36 | self.model.appendRow(QStandardItem(QIcon(SRC_DIR + "folder.gif"), info.name)) 37 | self.out.append({'fid': info.id, 'is_file': info.is_file, 'name': info.name}) # id,文件标示, 文件名 38 | count += 1 39 | if max_len < len(info.name): # 使用最大文件名长度 40 | max_len = len(info.name) 41 | self.list_view.setModel(self.model) 42 | 43 | self.lb_name = QLabel("尝试删除以下{}个文件(夹):".format(count)) 44 | self.buttonBox = QDialogButtonBox() 45 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 46 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) 47 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") 48 | self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") 49 | 50 | self.layout.addWidget(self.lb_name) 51 | self.layout.addWidget(self.list_view) 52 | self.layout.addWidget(self.buttonBox) 53 | self.setLayout(self.layout) 54 | 55 | self.buttonBox.accepted.connect(self.btn_ok) 56 | self.buttonBox.accepted.connect(self.accept) 57 | self.buttonBox.rejected.connect(self.reject) 58 | self.setMinimumWidth(400) 59 | self.resize(int(max_len*8), int(count*34+60)) 60 | 61 | def btn_ok(self): 62 | self.new_infos.emit(self.out) 63 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/info.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import Qt, pyqtSignal 2 | from PyQt6.QtGui import QIcon, QPixmap 3 | from PyQt6.QtWidgets import (QLineEdit, QDialog, QLabel, QFormLayout, 4 | QDialogButtonBox, QVBoxLayout) 5 | 6 | from lanzou.gui.qss import dialog_qss_style 7 | from lanzou.gui.others import AutoResizingTextEdit 8 | from lanzou.debug import SRC_DIR 9 | 10 | 11 | class InfoDialog(QDialog): 12 | """文件信息对话框""" 13 | get_dl_link = pyqtSignal(str, str) 14 | closed = pyqtSignal() 15 | 16 | def __init__(self, parent=None): 17 | super().__init__(parent) 18 | self.infos = None 19 | self._short_link_flag = True # 防止多次重试 20 | self.initUI() 21 | self.setStyleSheet(dialog_qss_style) 22 | 23 | def update_ui(self): 24 | self.tx_dl_link.setPlaceholderText("单击获取") 25 | self.tx_name.setText(self.infos.name) 26 | if self.infos.is_file: 27 | self.setWindowTitle("文件信息") 28 | self.lb_name.setText("文件名:") 29 | self.lb_desc.setText("文件描述:") 30 | self.tx_dl_link.setText("") # 清空旧的信息 31 | self.lb_dl_link.setVisible(True) 32 | self.tx_dl_link.setVisible(True) 33 | else: 34 | self.setWindowTitle("文件夹信息") 35 | self.lb_name.setText("文件夹名:") 36 | self.lb_desc.setText("文件夹描述:") 37 | self.lb_dl_link.setVisible(False) 38 | self.tx_dl_link.setVisible(False) 39 | 40 | if self.infos.size: 41 | self.tx_size.setText(self.infos.size) 42 | self.lb_size.setVisible(True) 43 | self.tx_size.setVisible(True) 44 | else: 45 | self.tx_size.setVisible(False) 46 | self.lb_size.setVisible(False) 47 | 48 | if self.infos.time: 49 | self.lb_time.setVisible(True) 50 | self.tx_time.setVisible(True) 51 | self.tx_time.setText(self.infos.time) 52 | else: 53 | self.lb_time.setVisible(False) 54 | self.tx_time.setVisible(False) 55 | 56 | if self.infos.downs: 57 | self.lb_dl_count.setVisible(True) 58 | self.tx_dl_count.setVisible(True) 59 | self.tx_dl_count.setText(str(self.infos.downs)) 60 | else: 61 | self.tx_dl_count.setVisible(False) 62 | self.lb_dl_count.setVisible(False) 63 | 64 | if self.infos.pwd: 65 | self.tx_pwd.setText(self.infos.pwd) 66 | self.tx_pwd.setPlaceholderText("") 67 | else: 68 | self.tx_pwd.setText("") 69 | self.tx_pwd.setPlaceholderText("无") 70 | 71 | if self.infos.desc: 72 | self.tx_desc.setText(self.infos.desc) 73 | self.tx_desc.setPlaceholderText("") 74 | else: 75 | self.tx_desc.setText("") 76 | self.tx_desc.setPlaceholderText("无") 77 | 78 | self.tx_share_url.setText(self.infos.url) 79 | self.adjustSize() 80 | 81 | def set_values(self, infos): 82 | self.infos = infos 83 | self.update_ui() 84 | 85 | def set_dl_link_tx(self, text): 86 | self.tx_dl_link.setText(text) 87 | self.adjustSize() 88 | 89 | def call_get_dl_link(self): 90 | url = self.tx_share_url.text() 91 | pwd = self.tx_pwd.text() 92 | self.get_dl_link.emit(url, pwd) 93 | self.tx_dl_link.setPlaceholderText("后台获取中,请稍候!") 94 | 95 | def call_get_short_url(self): 96 | if self._short_link_flag: 97 | self._short_link_flag = False 98 | self.tx_short.setPlaceholderText("后台获取中,请稍候!") 99 | url = self.tx_share_url.text() 100 | from lanzou.api.extra import get_short_url 101 | 102 | short_url = get_short_url(url) 103 | if short_url: 104 | self.tx_short.setText(short_url) 105 | self.tx_short.setPlaceholderText("") 106 | self._short_link_flag = True 107 | else: 108 | self.tx_short.setText("") 109 | self.tx_short.setPlaceholderText("生成失败!api 可能已经失效") 110 | 111 | def clean(self): 112 | self._short_link_flag = True 113 | self.tx_short.setText("") 114 | self.tx_short.setPlaceholderText("单击获取") 115 | 116 | def initUI(self): 117 | self.setWindowIcon(QIcon(SRC_DIR + "share.ico")) 118 | self.setWindowTitle("文件信息") 119 | self.buttonBox = QDialogButtonBox() 120 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 121 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Close) 122 | self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setText("关闭") 123 | self.buttonBox.rejected.connect(self.reject) 124 | self.buttonBox.rejected.connect(self.clean) 125 | self.buttonBox.rejected.connect(self.closed.emit) 126 | 127 | self.logo = QLabel() 128 | self.logo.setPixmap(QPixmap(SRC_DIR + "q9.gif")) 129 | self.logo.setAlignment(Qt.AlignmentFlag.AlignCenter) 130 | self.logo.setStyleSheet("background-color:rgb(255,204,51);") 131 | 132 | self.lb_name = QLabel() 133 | self.lb_name.setText("文件名:") 134 | self.tx_name = AutoResizingTextEdit() 135 | self.tx_name.setReadOnly(True) 136 | self.tx_name.setMinimumLines(1) 137 | 138 | self.lb_size = QLabel() 139 | self.lb_size.setText("文件大小:") 140 | self.tx_size = QLabel() 141 | 142 | self.lb_time = QLabel() 143 | self.lb_time.setText("上传时间:") 144 | self.tx_time = QLabel() 145 | 146 | self.lb_dl_count = QLabel() 147 | self.lb_dl_count.setText("下载次数:") 148 | self.tx_dl_count = QLabel() 149 | 150 | self.lb_share_url = QLabel() 151 | self.lb_share_url.setText("分享链接:") 152 | self.tx_share_url = QLineEdit() 153 | self.tx_share_url.setReadOnly(True) 154 | 155 | self.lb_pwd = QLabel() 156 | self.lb_pwd.setText("提取码:") 157 | self.tx_pwd = QLineEdit() 158 | self.tx_pwd.setReadOnly(True) 159 | 160 | self.lb_short = QLabel() 161 | self.lb_short.setText("短链接:") 162 | self.tx_short = AutoResizingTextEdit(self) 163 | self.tx_short.setPlaceholderText("单击获取") 164 | self.tx_short.clicked.connect(self.call_get_short_url) 165 | self.tx_short.setReadOnly(True) 166 | self.tx_short.setMinimumLines(1) 167 | 168 | self.lb_desc = QLabel() 169 | self.lb_desc.setText("文件描述:") 170 | self.tx_desc = AutoResizingTextEdit() 171 | self.tx_desc.setReadOnly(True) 172 | self.tx_desc.setMinimumLines(1) 173 | 174 | self.lb_dl_link = QLabel() 175 | self.lb_dl_link.setText("下载直链:") 176 | self.tx_dl_link = AutoResizingTextEdit(self) 177 | self.tx_dl_link.setPlaceholderText("单击获取") 178 | self.tx_dl_link.clicked.connect(self.call_get_dl_link) 179 | self.tx_dl_link.setReadOnly(True) 180 | self.tx_dl_link.setMinimumLines(1) 181 | 182 | vbox = QVBoxLayout() 183 | vbox.addWidget(self.logo) 184 | vbox.addStretch(1) 185 | form = QFormLayout() 186 | form.setLabelAlignment(Qt.AlignmentFlag.AlignRight) 187 | form.addRow(self.lb_name, self.tx_name) 188 | form.addRow(self.lb_size, self.tx_size) 189 | form.addRow(self.lb_time, self.tx_time) 190 | form.addRow(self.lb_dl_count, self.tx_dl_count) 191 | form.addRow(self.lb_share_url, self.tx_share_url) 192 | form.addRow(self.lb_pwd, self.tx_pwd) 193 | form.addRow(self.lb_short, self.tx_short) 194 | form.addRow(self.lb_desc, self.tx_desc) 195 | form.addRow(self.lb_dl_link, self.tx_dl_link) 196 | form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) # 覆盖MacOS的默认样式 197 | vbox.addLayout(form) 198 | vbox.addStretch(1) 199 | vbox.addWidget(self.buttonBox) 200 | 201 | self.setLayout(vbox) 202 | self.setMinimumWidth(500) 203 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/login.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import browser_cookie3 4 | # https://github.com/borisbabic/browser_cookie3/pull/70 5 | # from lanzou import browser_cookie3_n as browser_cookie3 6 | from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QRect, QTimer 7 | from PyQt6.QtGui import QIcon, QPixmap 8 | from PyQt6.QtWidgets import (QDialog, QLabel, QLineEdit, QTextEdit, QPushButton, QFormLayout, 9 | QHBoxLayout, QVBoxLayout, QMessageBox, QFileDialog, QTabWidget, QWidget) 10 | 11 | from lanzou.gui.others import QDoublePushButton, MyLineEdit, AutoResizingTextEdit 12 | from lanzou.gui.qss import dialog_qss_style, btn_style 13 | from lanzou.debug import logger, SRC_DIR 14 | from lanzou import USE_WEB_ENG 15 | 16 | if USE_WEB_ENG: # 此处不能移动到后面,会抛出异常 17 | from lanzou.login_assister import LoginWindow 18 | 19 | 20 | is_windows = True if os.name == 'nt' else False 21 | 22 | 23 | def get_cookie_from_browser(site='https://pc.woozooo.com'): 24 | """直接读取浏览器的 cookie 数据库,优先返回 Firefox cookie,最后为 Chrome 25 | """ 26 | cookie = {} 27 | domain = re.match(r".*://([^/]+)/?", site) 28 | domain = domain.groups()[0] 29 | domain = domain.split(".") 30 | domain = ".".join(domain[-2:]) 31 | cookies = browser_cookie3.load(domain_name=domain) 32 | for c in cookies: 33 | if c.domain in site: 34 | if c.name in ("ylogin", 'phpdisk_info'): 35 | cookie[c.name] = c.value 36 | 37 | return cookie 38 | 39 | 40 | class LoginDialog(QDialog): 41 | """登录对话框""" 42 | 43 | clicked_ok = pyqtSignal() 44 | 45 | def __init__(self, config): 46 | super().__init__() 47 | self._cwd = os.getcwd() 48 | self._config = config 49 | self._cookie_assister = 'login_assister.exe' 50 | self._user = "" 51 | self._pwd = "" 52 | self._cookie = {} 53 | self._del_user = "" 54 | self.initUI() 55 | self.setStyleSheet(dialog_qss_style) 56 | self.setMinimumWidth(560) 57 | self.name_ed.setFocus() 58 | # 信号 59 | self.name_ed.textChanged.connect(self.set_user) 60 | self.pwd_ed.textChanged.connect(self.set_pwd) 61 | self.cookie_ed.textChanged.connect(self.set_cookie) 62 | 63 | def update_selection(self, user): 64 | """显示已经保存的登录用户信息""" 65 | user_info = self._config.get_user_info(user) 66 | if user_info: 67 | self._user = user_info[0] 68 | self._pwd = user_info[1] 69 | self._cookie = user_info[2] 70 | # 更新控件显示内容 71 | self.name_ed.setText(self._user) 72 | self.pwd_ed.setText(self._pwd) 73 | try: text = ";".join([f'{k}={v}' for k, v in self._cookie.items()]) 74 | except: text = '' 75 | self.cookie_ed.setPlainText(text) 76 | 77 | def initUI(self): 78 | self.setWindowTitle("登录蓝奏云") 79 | self.setWindowIcon(QIcon(SRC_DIR + "login.ico")) 80 | logo = QLabel() 81 | logo.setPixmap(QPixmap(SRC_DIR + "logo3.gif")) 82 | logo.setStyleSheet("background-color:rgb(0,153,255);") 83 | logo.setAlignment(Qt.AlignmentFlag.AlignCenter) 84 | 85 | self.tabs = QTabWidget() 86 | self.auto_tab = QWidget() 87 | self.hand_tab = QWidget() 88 | 89 | # Add tabs 90 | self.tabs.addTab(self.auto_tab,"自动获取Cookie") 91 | self.tabs.addTab(self.hand_tab,"手动输入Cookie") 92 | self.auto_get_cookie_ok = AutoResizingTextEdit("🔶点击👇自动获取浏览器登录信息👇") 93 | self.auto_get_cookie_ok.setReadOnly(True) 94 | self.auto_get_cookie_btn = QPushButton("自动读取浏览器登录信息") 95 | auto_cookie_notice = '支持浏览器:Chrome, Chromium, Opera, Edge, Firefox' 96 | self.auto_get_cookie_btn.setToolTip(auto_cookie_notice) 97 | self.auto_get_cookie_btn.clicked.connect(self.call_auto_get_cookie) 98 | self.auto_get_cookie_btn.setStyleSheet("QPushButton {min-width: 210px;max-width: 210px;}") 99 | 100 | self.name_lb = QLabel("&U 用户") 101 | self.name_lb.setAlignment(Qt.AlignmentFlag.AlignCenter) 102 | self.name_ed = QLineEdit() 103 | self.name_lb.setBuddy(self.name_ed) 104 | 105 | self.pwd_lb = QLabel("&P 密码") 106 | self.pwd_lb.setAlignment(Qt.AlignmentFlag.AlignCenter) 107 | self.pwd_ed = QLineEdit() 108 | self.pwd_ed.setEchoMode(QLineEdit.EchoMode.Password) 109 | self.pwd_lb.setBuddy(self.pwd_ed) 110 | 111 | self.cookie_lb = QLabel("&Cookie") 112 | self.cookie_ed = QTextEdit() 113 | notice = "由于滑动验证的存在,需要输入cookie,cookie请使用浏览器获取\n" + \ 114 | "cookie会保存在本地,下次使用。其格式如下:\n ylogin=value1; phpdisk_info=value2" 115 | self.cookie_ed.setPlaceholderText(notice) 116 | self.cookie_lb.setBuddy(self.cookie_ed) 117 | 118 | self.show_input_cookie_btn = QPushButton("显示Cookie输入框") 119 | self.show_input_cookie_btn.setToolTip(notice) 120 | self.show_input_cookie_btn.setStyleSheet("QPushButton {min-width: 110px;max-width: 110px;}") 121 | self.show_input_cookie_btn.clicked.connect(self.change_show_input_cookie) 122 | self.ok_btn = QPushButton("登录") 123 | self.ok_btn.clicked.connect(self.change_ok_btn) 124 | self.cancel_btn = QPushButton("取消") 125 | self.cancel_btn.clicked.connect(self.change_cancel_btn) 126 | lb_line_1 = QLabel() 127 | lb_line_1.setText('
切换用户') 128 | lb_line_2 = QLabel() 129 | lb_line_2.setText('
') 130 | 131 | self.form = QFormLayout() 132 | self.form.setLabelAlignment(Qt.AlignmentFlag.AlignRight) 133 | self.form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) # 覆盖MacOS的默认样式 134 | self.form.addRow(self.name_lb, self.name_ed) 135 | self.form.addRow(self.pwd_lb, self.pwd_ed) 136 | if is_windows: 137 | def set_assister_path(): 138 | """设置辅助登录程序路径""" 139 | assister_path = QFileDialog.getOpenFileName(self, "选择辅助登录程序路径", 140 | self._cwd, "EXE Files (*.exe)") 141 | if not assister_path[0]: 142 | return None 143 | assister_path = os.path.normpath(assister_path[0]) # windows backslash 144 | if assister_path == self._cookie_assister: 145 | return None 146 | self.assister_ed.setText(assister_path) 147 | self._cookie_assister = assister_path 148 | 149 | self.assister_lb = QLabel("登录辅助程序") 150 | self.assister_lb.setAlignment(Qt.AlignmentFlag.AlignCenter) 151 | self.assister_ed = MyLineEdit(self) 152 | self.assister_ed.setText(self._cookie_assister) 153 | self.assister_ed.clicked.connect(set_assister_path) 154 | self.assister_lb.setBuddy(self.assister_ed) 155 | self.form.addRow(self.assister_lb, self.assister_ed) 156 | 157 | hbox = QHBoxLayout() 158 | hbox.addWidget(self.show_input_cookie_btn) 159 | hbox.addStretch(1) 160 | hbox.addWidget(self.ok_btn) 161 | hbox.addWidget(self.cancel_btn) 162 | 163 | user_box = QHBoxLayout() 164 | self.user_num = 0 165 | self.user_btns = {} 166 | for user in self._config.users_name: 167 | user = str(user) # TODO: 可能需要删掉 168 | self.user_btns[user] = QDoublePushButton(user) 169 | self.user_btns[user].setStyleSheet("QPushButton {border:none;}") 170 | if user == self._config.name: 171 | self.user_btns[user].setStyleSheet("QPushButton {background-color:rgb(0,153,2);}") 172 | self.tabs.setCurrentIndex(1) 173 | self.user_btns[user].setToolTip(f"点击选中,双击切换至用户:{user}") 174 | self.user_btns[user].doubleClicked.connect(self.choose_user) 175 | self.user_btns[user].clicked.connect(self.delete_chose_user) 176 | user_box.addWidget(self.user_btns[user]) 177 | self.user_num += 1 178 | user_box.addStretch(1) 179 | 180 | self.layout = QVBoxLayout(self) 181 | self.layout.addWidget(logo) 182 | vbox = QVBoxLayout() 183 | if self._config.name: 184 | vbox.addWidget(lb_line_1) 185 | user_box.setAlignment(Qt.AlignmentFlag.AlignCenter) 186 | vbox.addLayout(user_box) 187 | vbox.addWidget(lb_line_2) 188 | if self.user_num > 1: 189 | self.del_user_btn = QPushButton("删除账户") 190 | self.del_user_btn.setIcon(QIcon(SRC_DIR + "delete.ico")) 191 | self.del_user_btn.setStyleSheet("QPushButton {min-width: 180px;max-width: 180px;}") 192 | self.del_user_btn.clicked.connect(self.call_del_chose_user) 193 | vbox.addWidget(self.del_user_btn) 194 | else: 195 | self.del_user_btn = None 196 | vbox.addStretch(1) 197 | 198 | vbox.addLayout(self.form) 199 | vbox.addStretch(1) 200 | vbox.addLayout(hbox) 201 | vbox.setAlignment(Qt.AlignmentFlag.AlignCenter) 202 | 203 | self.hand_tab.setLayout(vbox) 204 | auto_cookie_vbox = QVBoxLayout() 205 | auto_cookie_vbox.addWidget(self.auto_get_cookie_ok) 206 | auto_cookie_vbox.addWidget(self.auto_get_cookie_btn) 207 | auto_cookie_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter) 208 | self.auto_tab.setLayout(auto_cookie_vbox) 209 | self.layout.addWidget(self.tabs) 210 | self.setLayout(self.layout) 211 | self.update_selection(self._config.name) 212 | 213 | def call_del_chose_user(self): 214 | if self._del_user: 215 | if self._del_user != self._config.name: 216 | self.user_num -= 1 217 | self._config.del_user(self._del_user) 218 | self.user_btns[self._del_user].close() 219 | self._del_user = "" 220 | if self.user_num <= 1: 221 | self.del_user_btn.close() 222 | self.del_user_btn = None 223 | return 224 | else: 225 | title = '不能删除' 226 | msg = '不能删除当前登录账户,请先切换用户!' 227 | else: 228 | title = '请选择账户' 229 | msg = '请单击选择需要删除的账户\n\n注意不能删除当前账户(绿色)' 230 | message_box = QMessageBox(self) 231 | message_box.setIcon(QMessageBox.Icon.Critical) 232 | message_box.setStyleSheet(btn_style) 233 | message_box.setWindowTitle(title) 234 | message_box.setText(msg) 235 | message_box.setStandardButtons(QMessageBox.StandardButton.Close) 236 | buttonC = message_box.button(QMessageBox.StandardButton.Close) 237 | buttonC.setText('关闭') 238 | message_box.exec() 239 | 240 | def delete_chose_user(self): 241 | """更改单击选中需要删除的用户""" 242 | user = str(self.sender().text()) 243 | self._del_user = user 244 | if self.del_user_btn: 245 | self.del_user_btn.setText(f"删除 <{user}>") 246 | 247 | def choose_user(self): 248 | """切换用户""" 249 | user = self.sender().text() 250 | if user != self._config.name: 251 | self.ok_btn.setText("切换用户") 252 | else: 253 | self.ok_btn.setText("登录") 254 | self.update_selection(user) 255 | 256 | def change_show_input_cookie(self): 257 | row_c = 4 if is_windows else 3 258 | if self.form.rowCount() < row_c: 259 | self.org_height = self.height() 260 | self.form.addRow(self.cookie_lb, self.cookie_ed) 261 | self.show_input_cookie_btn.setText("隐藏Cookie输入框") 262 | self.change_height = None 263 | self.adjustSize() 264 | else: 265 | if not self.change_height: 266 | self.change_height = self.height() 267 | if self.cookie_ed.isVisible(): 268 | self.cookie_lb.setVisible(False) 269 | self.cookie_ed.setVisible(False) 270 | self.show_input_cookie_btn.setText("显示Cookie输入框") 271 | start_height, end_height = self.change_height, self.org_height 272 | else: 273 | self.cookie_lb.setVisible(True) 274 | self.cookie_ed.setVisible(True) 275 | self.show_input_cookie_btn.setText("隐藏Cookie输入框") 276 | start_height, end_height = self.org_height, self.change_height 277 | gm = self.geometry() 278 | x, y = gm.x(), gm.y() 279 | wd = self.width() 280 | self.animation = QPropertyAnimation(self, b'geometry') 281 | self.animation.setDuration(400) 282 | self.animation.setStartValue(QRect(x, y, wd, start_height)) 283 | self.animation.setEndValue(QRect(x, y, wd, end_height)) 284 | self.animation.start() 285 | 286 | def set_user(self, user): 287 | self._user = user 288 | if not user: 289 | return None 290 | if user not in self._config.users_name: 291 | self.ok_btn.setText("添加用户") 292 | self.cookie_ed.setPlainText("") 293 | elif user != self._config.name: 294 | self.update_selection(user) 295 | self.ok_btn.setText("切换用户") 296 | else: 297 | self.update_selection(user) 298 | self.ok_btn.setText("登录") 299 | 300 | def set_pwd(self, pwd): 301 | if self._user in self._config.users_name: 302 | user_info = self._config.get_user_info(self._user) 303 | if pwd and pwd != user_info[1]: # 改变密码,cookie作废 304 | self.cookie_ed.setPlainText("") 305 | self._cookie = None 306 | if not pwd: # 输入空密码,表示删除对pwd的存储,并使用以前的cookie 307 | self._cookie = user_info[2] 308 | try: text = ";".join([f'{k}={v}' for k, v in self._cookie.items()]) 309 | except: text = '' 310 | self.cookie_ed.setPlainText(text) 311 | self._pwd = pwd 312 | 313 | def set_cookie(self): 314 | cookies = self.cookie_ed.toPlainText() 315 | if cookies: 316 | try: 317 | self._cookie = {kv.split("=")[0].strip(" "): kv.split("=")[1].strip(" ") for kv in cookies.split(";") if kv.strip(" ") } 318 | except: self._cookie = None 319 | 320 | def change_cancel_btn(self): 321 | self.update_selection(self._config.name) 322 | self.close() 323 | 324 | def change_ok_btn(self): 325 | if self._user and self._pwd: 326 | if self._user not in self._config.users_name: 327 | self._cookie = None 328 | if self._cookie: 329 | up_info = {"name": self._user, "pwd": self._pwd, "cookie": self._cookie, "work_id": -1} 330 | if self.ok_btn.text() == "切换用户": 331 | self._config.change_user(self._user) 332 | else: 333 | self._config.set_infos(up_info) 334 | self.clicked_ok.emit() 335 | self.close() 336 | elif USE_WEB_ENG: 337 | self.web = LoginWindow(self._user, self._pwd) 338 | self.web.cookie.connect(self.get_cookie_by_web) 339 | self.web.setWindowModality(Qt.WindowModality.ApplicationModal) 340 | self.web.exec() 341 | elif os.path.isfile(self._cookie_assister): 342 | try: 343 | result = os.popen(f'{self._cookie_assister} {self._user} {self._pwd}') 344 | cookie = result.read() 345 | try: 346 | self._cookie = {kv.split("=")[0].strip(" "): kv.split("=")[1].strip(" ") for kv in cookie.split(";")} 347 | except: self._cookie = None 348 | if not self._cookie: 349 | return None 350 | up_info = {"name": self._user, "pwd": self._pwd, "cookie": self._cookie, "work_id": -1} 351 | self._config.set_infos(up_info) 352 | self.clicked_ok.emit() 353 | self.close() 354 | except: pass 355 | else: 356 | title = '请使用 Cookie 登录或是选择 登录辅助程序' 357 | msg = '没有输入 Cookie,或者没有找到登录辅助程序!\n\n' + \ 358 | '推荐使用浏览器获取 cookie 填入 cookie 输入框\n\n' + \ 359 | '如果不嫌文件体积大,请下载登录辅助程序:\n' + \ 360 | 'https://github.com/rachpt/lanzou-gui/releases' 361 | message_box = QMessageBox(self) 362 | message_box.setIcon(QMessageBox.Icon.Critical) 363 | message_box.setStyleSheet(btn_style) 364 | message_box.setWindowTitle(title) 365 | message_box.setText(msg) 366 | message_box.setStandardButtons(QMessageBox.StandardButton.Close) 367 | buttonC = message_box.button(QMessageBox.StandardButton.Close) 368 | buttonC.setText('关闭') 369 | message_box.exec() 370 | 371 | def get_cookie_by_web(self, cookie): 372 | """使用辅助登录程序槽函数""" 373 | self._cookie = cookie 374 | self._close_dialog() 375 | 376 | def call_auto_get_cookie(self): 377 | """自动读取浏览器cookie槽函数""" 378 | try: 379 | self._cookie = get_cookie_from_browser() 380 | except Exception as e: 381 | logger.error(f"Browser_cookie3 Error: {e}") 382 | self.auto_get_cookie_ok.setPlainText(f"❌获取失败,错误信息\n{e}") 383 | else: 384 | if self._cookie: 385 | self._user = self._pwd = '' 386 | self.auto_get_cookie_ok.setPlainText("✅获取成功即将登录……") 387 | QTimer.singleShot(2000, self._close_dialog) 388 | else: 389 | self.auto_get_cookie_ok.setPlainText("❌获取失败\n请提前使用支持的浏览器登录蓝奏云,读取前完全退出浏览器!\n支持的浏览器与顺序:\nchrome, chromium, opera, edge, firefox") 390 | 391 | def _close_dialog(self): 392 | """关闭对话框""" 393 | up_info = {"name": self._user, "pwd": self._pwd, "cookie": self._cookie} 394 | self._config.set_infos(up_info) 395 | self.clicked_ok.emit() 396 | self.close() 397 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/merge_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pickle import load 3 | 4 | from PyQt6.QtCore import Qt, pyqtSignal 5 | from PyQt6.QtGui import QPixmap, QIcon 6 | from PyQt6.QtWidgets import ( 7 | QDialog, 8 | QLabel, 9 | QDialogButtonBox, 10 | QVBoxLayout, 11 | QHBoxLayout, 12 | QFileDialog, 13 | ) 14 | 15 | from lanzou.gui.others import MyLineEdit 16 | from lanzou.gui.qss import others_style, btn_style 17 | from lanzou.debug import SRC_DIR 18 | 19 | 20 | def get_minimum_file(file_lst: list) -> str: 21 | """ 返回大小最小的文件名 """ 22 | if not file_lst: 23 | return "" 24 | if len(file_lst) == 1: 25 | return file_lst[0] 26 | res = file_lst[0] 27 | size = os.path.getsize(res) 28 | for f in file_lst[1:]: 29 | _size = os.path.getsize(f) 30 | if _size < size: 31 | res = f 32 | size = _size 33 | 34 | return res 35 | 36 | 37 | def un_serialize(folder): 38 | """反序列化文件信息数据""" 39 | txt_lst = [] 40 | msg = "" 41 | folder_lst = os.listdir(folder) 42 | for item in folder_lst: 43 | _file = os.path.join(folder, item) 44 | if os.path.isfile(_file): 45 | if _file.endswith(".txt"): 46 | txt_lst.append(_file) 47 | record_file = get_minimum_file(txt_lst) 48 | if not record_file: 49 | msg = "没有 txt 记录文件" 50 | return False, msg 51 | record = {} 52 | try: 53 | with open(record_file, "rb") as f_handle: 54 | _record = load(f_handle) 55 | if isinstance(_record, dict): 56 | record = _record 57 | except Exception as e: # 这里可能会丢奇怪的异常 58 | # logger.debug(f"Pickle e={e}") 59 | pass 60 | if not record: 61 | msg = f"{record_file} : 记录文件不对" 62 | return False, msg 63 | else: 64 | files_info = {} 65 | not_complete = False 66 | for _file in record["parts"]: 67 | full_path = os.path.join(folder, _file) 68 | if not os.path.isfile(full_path): 69 | not_complete = True 70 | files_info[_file] = False 71 | 72 | if not_complete: 73 | msg = "文件不全" 74 | return False, msg 75 | merged_file_name = os.path.join(folder, record["name"]) 76 | with open(merged_file_name, "ab") as merge_f: 77 | for _file in record["parts"]: 78 | part_file_name = os.path.join(folder, _file) 79 | with open(part_file_name, "rb") as f: 80 | for data in f: 81 | merge_f.write(data) 82 | 83 | if os.path.getsize(merged_file_name) == record["size"]: 84 | for _file in record["parts"]: 85 | part_file_name = os.path.join(folder, _file) 86 | os.remove(part_file_name) 87 | os.remove(record_file) 88 | return True, "" 89 | else: 90 | msg = "文件大小对不上" 91 | 92 | return False, msg 93 | 94 | 95 | class MergeFileDialog(QDialog): 96 | check_update = pyqtSignal(str, bool) 97 | 98 | def __init__(self, user_home, parent=None): 99 | super(MergeFileDialog, self).__init__(parent) 100 | self.cwd = user_home 101 | self.selected = "" 102 | self.initUI() 103 | self.setStyleSheet(others_style) 104 | 105 | def initUI(self): 106 | self.setWindowTitle("合并文件") 107 | self.setWindowIcon(QIcon(SRC_DIR + "upload.ico")) 108 | self.logo = QLabel() 109 | self.logo.setPixmap(QPixmap(SRC_DIR + "logo3.gif")) 110 | self.logo.setStyleSheet("background-color:rgb(0,153,255);") 111 | self.logo.setAlignment(Qt.AlignmentFlag.AlignCenter) 112 | 113 | # lable 114 | self.choose_lb = QLabel("选择文件夹") 115 | # folder 116 | self.choose_folder = MyLineEdit(self) 117 | self.choose_folder.setObjectName("choose_folder") 118 | self.choose_folder.clicked.connect(self.slot_choose_folder) 119 | self.status = QLabel(self) 120 | 121 | self.buttonBox = QDialogButtonBox() 122 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 123 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) 124 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("提取") 125 | self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("关闭") 126 | self.buttonBox.setStyleSheet(btn_style) 127 | 128 | vbox = QVBoxLayout() 129 | hbox_head = QHBoxLayout() 130 | hbox_button = QHBoxLayout() 131 | hbox_head.addWidget(self.choose_lb) 132 | hbox_head.addWidget(self.choose_folder) 133 | hbox_button.addWidget(self.buttonBox) 134 | vbox.addWidget(self.logo) 135 | vbox.addStretch(1) 136 | vbox.addWidget(self.status) 137 | vbox.addLayout(hbox_head) 138 | vbox.addStretch(1) 139 | vbox.addLayout(hbox_button) 140 | self.setLayout(vbox) 141 | self.setMinimumWidth(350) 142 | 143 | # 设置信号 144 | self.buttonBox.accepted.connect(self.slot_btn_ok) 145 | self.buttonBox.rejected.connect(self.slot_btn_no) 146 | self.buttonBox.rejected.connect(self.reject) 147 | 148 | def slot_choose_folder(self): 149 | dir_choose = QFileDialog.getExistingDirectory(self, "选择文件夹", self.cwd) # 起始路径 150 | if dir_choose == "": 151 | return 152 | self.selected = dir_choose 153 | self.choose_folder.setText(self.selected) 154 | self.status.setText("") 155 | self.cwd = os.path.dirname(dir_choose) 156 | 157 | def slot_btn_no(self): 158 | self.selected = "" 159 | self.choose_folder.setText(self.selected) 160 | self.status.setText("") 161 | 162 | def slot_btn_ok(self): 163 | if self.selected: 164 | success, msg = un_serialize(self.selected) 165 | if success: 166 | text = "提取成功✅" 167 | else: 168 | text = f"提取失败❌, {msg}" 169 | else: 170 | text = "未选择文件夹📂" 171 | self.status.setText(text) 172 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/move.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import Qt, pyqtSignal 2 | from PyQt6.QtGui import QIcon 3 | from PyQt6.QtWidgets import QDialog, QLabel, QGridLayout, QDialogButtonBox, QComboBox 4 | 5 | from lanzou.gui.qss import dialog_qss_style 6 | from lanzou.gui.others import AutoResizingTextEdit 7 | from lanzou.debug import SRC_DIR 8 | 9 | 10 | class MoveFileDialog(QDialog): 11 | '''移动文件对话框''' 12 | new_infos = pyqtSignal(object) 13 | 14 | def __init__(self, parent=None): 15 | super(MoveFileDialog, self).__init__(parent) 16 | self.infos = None 17 | self.dirs = {} 18 | self.initUI() 19 | self.setStyleSheet(dialog_qss_style) 20 | 21 | def update_ui(self): 22 | names = "\n".join([i.name for i in self.infos]) 23 | names_tip = "\n".join([i.name for i in self.infos]) 24 | self.tx_name.setText(names) 25 | self.tx_name.setToolTip(names_tip) 26 | 27 | self.tx_new_path.clear() 28 | f_icon = QIcon(SRC_DIR + "folder.gif") 29 | for f_name, fid in self.dirs.items(): 30 | if len(f_name) > 50: # 防止文件夹名字过长? 31 | f_name = f_name[:47] + "..." 32 | self.tx_new_path.addItem(f_icon, "id:{:>8},name:{}".format(fid, f_name)) 33 | 34 | def set_values(self, infos, all_dirs_dict): 35 | self.infos = infos 36 | self.dirs = all_dirs_dict 37 | self.update_ui() 38 | self.exec() 39 | 40 | def initUI(self): 41 | self.setWindowTitle("移动文件(夹)") 42 | self.setWindowIcon(QIcon(SRC_DIR + "move.ico")) 43 | self.lb_name = QLabel() 44 | self.lb_name.setText("文件(夹)名:") 45 | self.lb_name.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter) 46 | self.tx_name = AutoResizingTextEdit() 47 | self.tx_name.setFocusPolicy(Qt.FocusPolicy.NoFocus) # 只读 48 | self.tx_name.setReadOnly(True) 49 | self.lb_new_path = QLabel() 50 | self.lb_new_path.setText("目标文件夹:") 51 | self.lb_new_path.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter) 52 | self.tx_new_path = QComboBox() 53 | 54 | self.buttonBox = QDialogButtonBox() 55 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 56 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) 57 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") 58 | self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") 59 | 60 | self.grid = QGridLayout() 61 | self.grid.setSpacing(10) 62 | self.grid.addWidget(self.lb_name, 1, 0) 63 | self.grid.addWidget(self.tx_name, 1, 1) 64 | self.grid.addWidget(self.lb_new_path, 2, 0) 65 | self.grid.addWidget(self.tx_new_path, 2, 1) 66 | self.grid.addWidget(self.buttonBox, 3, 0, 1, 2) 67 | self.setLayout(self.grid) 68 | self.buttonBox.accepted.connect(self.btn_ok) 69 | self.buttonBox.accepted.connect(self.accept) 70 | self.buttonBox.rejected.connect(self.reject) 71 | self.setMinimumWidth(280) 72 | 73 | def btn_ok(self): 74 | new_id = self.tx_new_path.currentText().split(",")[0].split(":")[1] 75 | for index in range(len(self.infos)): 76 | self.infos[index].new_id = int(new_id) 77 | self.new_infos.emit(self.infos) 78 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/rec_folder.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import Qt, pyqtSignal 2 | from PyQt6.QtWidgets import QDialog, QLabel, QDialogButtonBox, QVBoxLayout, QPushButton, QHBoxLayout 3 | 4 | from lanzou.gui.others import set_file_icon 5 | from lanzou.gui.qss import others_style, btn_style 6 | 7 | 8 | class RecFolderDialog(QDialog): 9 | out = pyqtSignal(object) 10 | 11 | def __init__(self, files, parent=None): 12 | super(RecFolderDialog, self).__init__(parent) 13 | self.files = files 14 | self.initUI() 15 | self.setStyleSheet(others_style) 16 | 17 | def initUI(self): 18 | self.setWindowTitle("查看回收站文件夹内容") 19 | self.form = QVBoxLayout() 20 | for item in iter(self.files): 21 | ico = QPushButton(set_file_icon(item.name), item.name) 22 | ico.setStyleSheet("QPushButton {border:none; background:transparent; color:black;}") 23 | ico.adjustSize() 24 | it = QLabel(f"({item.size})") 25 | hbox = QHBoxLayout() 26 | hbox.addWidget(ico) 27 | hbox.addStretch(1) 28 | hbox.addWidget(it) 29 | self.form.addLayout(hbox) 30 | 31 | self.form.setSpacing(10) 32 | self.buttonBox = QDialogButtonBox() 33 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 34 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Close) 35 | self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setText("关闭") 36 | self.buttonBox.setStyleSheet(btn_style) 37 | self.buttonBox.rejected.connect(self.reject) 38 | 39 | vbox = QVBoxLayout() 40 | vbox.addLayout(self.form) 41 | vbox.addStretch(1) 42 | vbox.addWidget(self.buttonBox) 43 | self.setLayout(vbox) 44 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/rename.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import Qt, pyqtSignal 2 | from PyQt6.QtGui import QIcon 3 | from PyQt6.QtWidgets import QDialog, QLabel, QGridLayout, QDialogButtonBox, QLineEdit, QTextEdit 4 | 5 | from lanzou.gui.qss import dialog_qss_style 6 | from lanzou.debug import SRC_DIR 7 | 8 | 9 | class RenameDialog(QDialog): 10 | out = pyqtSignal(object) 11 | 12 | def __init__(self, parent=None): 13 | super(RenameDialog, self).__init__(parent) 14 | self.infos = [] 15 | self.min_width = 400 16 | self.initUI() 17 | self.update_text() 18 | self.setStyleSheet(dialog_qss_style) 19 | 20 | def set_values(self, infos=None): 21 | self.infos = infos or [] 22 | self.update_text() # 更新界面 23 | 24 | def initUI(self): 25 | self.setWindowIcon(QIcon(SRC_DIR + "desc.ico")) 26 | self.lb_name = QLabel() 27 | self.lb_name.setText("文件夹名:") 28 | self.lb_name.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter) 29 | self.tx_name = QLineEdit() 30 | self.lb_desc = QLabel() 31 | self.tx_desc = QTextEdit() 32 | self.lb_desc.setText("描  述:") 33 | self.lb_desc.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter) 34 | 35 | self.buttonBox = QDialogButtonBox() 36 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 37 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) 38 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") 39 | self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") 40 | 41 | self.grid = QGridLayout() 42 | self.grid.setSpacing(10) 43 | self.grid.addWidget(self.lb_name, 1, 0) 44 | self.grid.addWidget(self.tx_name, 1, 1) 45 | self.grid.addWidget(self.lb_desc, 2, 0) 46 | self.grid.addWidget(self.tx_desc, 2, 1, 5, 1) 47 | self.grid.addWidget(self.buttonBox, 7, 1, 1, 1) 48 | self.setLayout(self.grid) 49 | self.buttonBox.accepted.connect(self.btn_ok) 50 | self.buttonBox.accepted.connect(self.accept) 51 | self.buttonBox.rejected.connect(self.reject) 52 | 53 | def update_text(self): 54 | self.tx_desc.setFocus() 55 | num = len(self.infos) 56 | if num == 1: 57 | self.lb_name.setVisible(True) 58 | self.tx_name.setVisible(True) 59 | infos = self.infos[0] 60 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setToolTip("") # 去除新建文件夹影响 61 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(True) # 去除新建文件夹影响 62 | self.setWindowTitle("修改文件夹名与描述") 63 | self.tx_name.setText(str(infos.name)) 64 | if infos.desc: 65 | self.tx_desc.setText(str(infos.desc)) 66 | self.tx_desc.setToolTip('原描述:' + str(infos.desc)) 67 | else: 68 | self.tx_desc.setText("") 69 | self.tx_desc.setToolTip('') 70 | self.tx_desc.setPlaceholderText("无") 71 | self.min_width = len(str(infos.name)) * 8 72 | if infos.is_file: 73 | self.setWindowTitle("修改文件描述") 74 | self.tx_name.setFocusPolicy(Qt.FocusPolicy.NoFocus) 75 | self.tx_name.setReadOnly(True) 76 | else: 77 | self.tx_name.setFocusPolicy(Qt.FocusPolicy.StrongFocus) 78 | self.tx_name.setReadOnly(False) 79 | self.tx_name.setFocus() 80 | elif num > 1: 81 | self.lb_name.setVisible(False) 82 | self.tx_name.setVisible(False) 83 | self.setWindowTitle(f"批量修改{num}个文件(夹)的描述") 84 | self.tx_desc.setText('') 85 | self.tx_desc.setPlaceholderText("建议160字数以内。") 86 | 87 | else: 88 | self.setWindowTitle("新建文件夹") 89 | self.tx_name.setText("") 90 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False) 91 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setToolTip("请先输入文件名!") 92 | self.tx_name.textChanged.connect(self.slot_new_ok_btn) 93 | self.tx_name.setPlaceholderText("不支持空格,如有会被自动替换成 _") 94 | self.tx_name.setFocusPolicy(Qt.FocusPolicy.StrongFocus) 95 | self.tx_name.setReadOnly(False) 96 | self.tx_desc.setPlaceholderText("可选项,建议160字数以内。") 97 | self.tx_name.setFocus() 98 | if self.min_width < 400: 99 | self.min_width = 400 100 | self.resize(self.min_width, 200) 101 | 102 | def slot_new_ok_btn(self): 103 | """新建文件夹槽函数""" 104 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(True) 105 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setToolTip("") 106 | 107 | def btn_ok(self): 108 | new_name = self.tx_name.text() 109 | new_des = self.tx_desc.toPlainText() 110 | info_len = len(self.infos) 111 | if info_len == 0: # 在 work_id 新建文件夹 112 | if new_name: 113 | self.out.emit(("new", new_name, new_des)) 114 | elif info_len == 1: 115 | if new_name != self.infos[0].name or new_des != self.infos[0].desc: 116 | self.infos[0].new_des = new_des 117 | self.infos[0].new_name = new_name 118 | self.out.emit(("change", self.infos)) 119 | else: 120 | if new_des: 121 | for infos in self.infos: 122 | infos.new_des = new_des 123 | self.out.emit(("change", self.infos)) 124 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/setpwd.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import Qt, pyqtSignal 2 | from PyQt6.QtGui import QIcon 3 | from PyQt6.QtWidgets import QDialog, QLabel, QGridLayout, QDialogButtonBox, QLineEdit 4 | 5 | from lanzou.gui.qss import dialog_qss_style 6 | from lanzou.gui.models import FileInfos 7 | from lanzou.debug import SRC_DIR 8 | 9 | 10 | class SetPwdDialog(QDialog): 11 | new_infos = pyqtSignal(object) 12 | 13 | def __init__(self, parent=None): 14 | super(SetPwdDialog, self).__init__(parent) 15 | self.infos = [] 16 | self.initUI() 17 | self.update_text() 18 | self.setStyleSheet(dialog_qss_style) 19 | 20 | def set_values(self, infos): 21 | self.infos = infos 22 | self.update_text() # 更新界面 23 | 24 | def set_tip(self): # 用于提示状态 25 | self.setWindowTitle("请稍等……") 26 | 27 | def initUI(self): 28 | self.setWindowTitle("请稍等……") 29 | self.setWindowIcon(QIcon(SRC_DIR + "password.ico")) 30 | self.lb_oldpwd = QLabel() 31 | self.lb_oldpwd.setText("当前提取码:") 32 | self.lb_oldpwd.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter) 33 | self.tx_oldpwd = QLineEdit() 34 | # 当前提取码 只读 35 | self.tx_oldpwd.setFocusPolicy(Qt.FocusPolicy.NoFocus) 36 | self.tx_oldpwd.setReadOnly(True) 37 | self.lb_newpwd = QLabel() 38 | self.lb_newpwd.setText("新的提取码:") 39 | self.lb_newpwd.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter) 40 | self.tx_newpwd = QLineEdit() 41 | 42 | self.buttonBox = QDialogButtonBox() 43 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 44 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) 45 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") 46 | self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") 47 | 48 | self.grid = QGridLayout() 49 | self.grid.setSpacing(10) 50 | self.grid.addWidget(self.lb_oldpwd, 1, 0) 51 | self.grid.addWidget(self.tx_oldpwd, 1, 1) 52 | self.grid.addWidget(self.lb_newpwd, 2, 0) 53 | self.grid.addWidget(self.tx_newpwd, 2, 1) 54 | self.grid.addWidget(self.buttonBox, 3, 0, 1, 2) 55 | self.setLayout(self.grid) 56 | self.buttonBox.accepted.connect(self.btn_ok) 57 | self.buttonBox.accepted.connect(self.accept) 58 | self.buttonBox.accepted.connect(self.set_tip) 59 | self.buttonBox.rejected.connect(self.reject) 60 | self.buttonBox.rejected.connect(self.set_tip) 61 | self.setMinimumWidth(280) 62 | 63 | def update_text(self): 64 | num = len(self.infos) 65 | if num == 1: 66 | self.tx_oldpwd.setVisible(True) 67 | self.lb_oldpwd.setVisible(True) 68 | infos = self.infos[0] 69 | if infos.has_pwd: 70 | self.tx_oldpwd.setText(str(infos.pwd)) 71 | self.tx_oldpwd.setPlaceholderText("") 72 | else: 73 | self.tx_oldpwd.setText("") 74 | self.tx_oldpwd.setPlaceholderText("无") 75 | 76 | if isinstance(infos, FileInfos): # 文件 通过size列判断是否为文件 77 | self.setWindowTitle("修改文件提取码") 78 | self.tx_newpwd.setPlaceholderText("2-6位字符,关闭请留空") 79 | self.tx_newpwd.setMaxLength(6) # 最长6个字符 80 | else: # 文件夹 81 | self.setWindowTitle("修改文件夹名提取码") 82 | self.tx_newpwd.setPlaceholderText("2-12位字符,关闭请留空") 83 | self.tx_newpwd.setMaxLength(12) # 最长12个字符 84 | elif num > 1: 85 | self.tx_oldpwd.setVisible(False) 86 | self.lb_oldpwd.setVisible(False) 87 | self.setWindowTitle(f"批量修改{num}个文件(夹)的提取码") 88 | self.tx_newpwd.setPlaceholderText("2-12位字符,关闭请留空") 89 | self.tx_newpwd.setMaxLength(12) # 最长12个字符 90 | self.tx_newpwd.setText('') 91 | for infos in self.infos: 92 | if isinstance(infos, FileInfos): # 文件 93 | self.tx_newpwd.setPlaceholderText("2-6位字符,文件无法关闭") 94 | self.tx_newpwd.setMaxLength(6) # 最长6个字符 95 | break 96 | 97 | def btn_ok(self): 98 | new_pwd = self.tx_newpwd.text() 99 | for infos in self.infos: 100 | infos.new_pwd = new_pwd 101 | self.new_infos.emit(self.infos) # 最后一位用于标示文件还是文件夹 102 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/setting.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PyQt6.QtCore import Qt, pyqtSignal 3 | from PyQt6.QtGui import QPixmap 4 | from PyQt6.QtWidgets import (QDialog, QLabel, QDialogButtonBox, QLineEdit, QCheckBox, 5 | QHBoxLayout, QVBoxLayout, QFormLayout, QFileDialog) 6 | 7 | from lanzou.gui.qss import dialog_qss_style 8 | from lanzou.gui.others import MyLineEdit, AutoResizingTextEdit 9 | from lanzou.debug import SRC_DIR 10 | 11 | 12 | class SettingDialog(QDialog): 13 | saved = pyqtSignal() 14 | 15 | def __init__(self, parent=None): 16 | super(SettingDialog, self).__init__(parent) 17 | self._config = object 18 | self.download_threads = 3 19 | self.max_size = 100 20 | self.timeout = 5 21 | self.dl_path = None 22 | self.time_fmt = False 23 | self.to_tray = False 24 | self.watch_clipboard = False 25 | self.debug = False 26 | self.set_pwd = False 27 | self.set_desc = False 28 | self.upload_delay = 0 29 | self.allow_big_file = False 30 | self.upgrade = True 31 | self.pwd = "" 32 | self.desc = "" 33 | self.initUI() 34 | self.setStyleSheet(dialog_qss_style) 35 | 36 | def open_dialog(self, config): 37 | """"打开前先更新一下显示界面""" 38 | self._config = config 39 | if self._config.name: 40 | self.setWindowTitle(f"设置 <{self._config.name}>") 41 | else: 42 | self.setWindowTitle("设置") 43 | self.cwd = self._config.path 44 | self.set_values() 45 | self.exec() 46 | 47 | def show_values(self): 48 | """控件显示值""" 49 | self.download_threads_var.setText(str(self.download_threads)) 50 | self.max_size_var.setText(str(self.max_size)) 51 | self.timeout_var.setText(str(self.timeout)) 52 | self.dl_path_var.setText(str(self.dl_path)) 53 | self.time_fmt_box.setChecked(self.time_fmt) 54 | self.to_tray_box.setChecked(self.to_tray) 55 | self.watch_clipboard_box.setChecked(self.watch_clipboard) 56 | self.debug_box.setChecked(self.debug) 57 | self.set_pwd_box.setChecked(self.set_pwd) 58 | self.set_pwd_var.setEnabled(self.set_pwd) 59 | self.set_pwd_var.setText(self.pwd) 60 | self.set_desc_box.setChecked(self.set_desc) 61 | self.set_desc_var.setEnabled(self.set_desc) 62 | self.set_desc_var.setText(self.desc) 63 | self.upload_delay_var.setText(str(self.upload_delay)) 64 | self.big_file_box.setChecked(self.allow_big_file) 65 | self.big_file_box.setText(f"允许上传超过 {self.max_size}MB 的大文件") 66 | # self.big_file_box.setDisabled(True) # 关闭允许上传大文件设置入口 67 | self.upgrade_box.setChecked(self.upgrade) 68 | 69 | def set_values(self, reset=False): 70 | """设置控件对应变量初始值""" 71 | settings = self._config.default_settings if reset else self._config.settings 72 | self.download_threads = settings["download_threads"] 73 | self.max_size = settings["max_size"] 74 | self.timeout = settings["timeout"] 75 | self.dl_path = settings["dl_path"] 76 | self.time_fmt = settings["time_fmt"] 77 | self.to_tray = settings["to_tray"] 78 | self.watch_clipboard = settings["watch_clipboard"] 79 | self.debug = settings["debug"] 80 | self.set_pwd = settings["set_pwd"] 81 | self.pwd = settings["pwd"] 82 | self.set_desc = settings["set_desc"] 83 | self.desc = settings["desc"] 84 | self.upload_delay = settings["upload_delay"] 85 | if 'upgrade' in settings: 86 | self.upgrade = settings["upgrade"] 87 | if 'allow_big_file' in settings: 88 | self.allow_big_file = settings["allow_big_file"] 89 | self.show_values() 90 | 91 | def get_values(self) -> dict: 92 | """读取输入控件的值""" 93 | if self.download_threads_var.text(): 94 | self.download_threads = int(self.download_threads_var.text()) 95 | if self.max_size_var.text(): 96 | self.max_size = int(self.max_size_var.text()) 97 | if self.timeout_var.text(): 98 | self.timeout = int(self.timeout_var.text()) 99 | if self.upload_delay_var.text(): 100 | self.upload_delay = int(self.upload_delay_var.text()) 101 | self.dl_path = str(self.dl_path_var.text()) 102 | self.pwd = str(self.set_pwd_var.toPlainText()) 103 | self.desc = str(self.set_desc_var.toPlainText()) 104 | return {"download_threads": self.download_threads, 105 | "max_size": self.max_size, 106 | "timeout": self.timeout, 107 | "dl_path": self.dl_path, 108 | "time_fmt": self.time_fmt, 109 | "to_tray": self.to_tray, 110 | "watch_clipboard": self.watch_clipboard, 111 | "debug": self.debug, 112 | "set_pwd": self.set_pwd, 113 | "pwd": self.pwd, 114 | "set_desc": self.set_desc, 115 | "desc": self.desc, 116 | "upload_delay": self.upload_delay, 117 | "allow_big_file": self.allow_big_file, 118 | "upgrade": self.upgrade} 119 | 120 | def initUI(self): 121 | self.setWindowTitle("设置") 122 | logo = QLabel() 123 | logo.setPixmap(QPixmap(SRC_DIR + "logo2.gif")) 124 | logo.setStyleSheet("background-color:rgb(255,255,255);") 125 | logo.setAlignment(Qt.AlignmentFlag.AlignCenter) 126 | self.download_threads_lb = QLabel("同时下载文件数") 127 | self.download_threads_var = QLineEdit() 128 | self.download_threads_var.setPlaceholderText("范围:1-9") 129 | self.download_threads_var.setToolTip("范围:1-9") 130 | self.download_threads_var.setInputMask("D") 131 | self.max_size_lb = QLabel("分卷大小(MB)") 132 | self.max_size_var = QLineEdit() 133 | self.max_size_var.setPlaceholderText("普通用户最大100,vip用户根据具体情况设置") 134 | self.max_size_var.setToolTip("普通用户最大100,vip用户根据具体情况设置") 135 | self.max_size_var.setInputMask("D99") 136 | self.timeout_lb = QLabel("请求超时(秒)") 137 | self.timeout_var = QLineEdit() 138 | self.timeout_var.setPlaceholderText("范围:1-99") 139 | self.timeout_var.setToolTip("范围:1-99") 140 | self.timeout_var.setInputMask("D9") 141 | self.upload_delay_lb = QLabel("上传延时(秒)") 142 | self.upload_delay_var = QLineEdit() 143 | self.upload_delay_var.setPlaceholderText("范围:1-99") 144 | self.upload_delay_var.setToolTip("范围:1-99") 145 | self.upload_delay_var.setInputMask("D9") 146 | self.dl_path_lb = QLabel("下载保存路径") 147 | self.dl_path_var = MyLineEdit(self) 148 | self.dl_path_var.clicked.connect(self.set_download_path) 149 | self.time_fmt_box = QCheckBox("使用[年-月-日]时间格式") 150 | self.time_fmt_box.setToolTip("文件上传日期显示格式") 151 | self.to_tray_box = QCheckBox("关闭到系统托盘") 152 | self.to_tray_box.setToolTip("点击关闭软件按钮是最小化软件至系统托盘") 153 | self.watch_clipboard_box = QCheckBox("监听系统剪切板") 154 | self.watch_clipboard_box.setToolTip("检测到系统剪切板中有符合规范的蓝奏链接时自动唤起软件,并提取") 155 | self.debug_box = QCheckBox("开启调试日志") 156 | self.debug_box.setToolTip("记录软件 debug 信息至 debug-lanzou-gui.log 文件") 157 | self.set_pwd_box = QCheckBox("上传文件自动设置密码") 158 | self.set_pwd_var = AutoResizingTextEdit() 159 | self.set_pwd_var.setPlaceholderText(" 2-8 位数字或字母") 160 | self.set_pwd_var.setToolTip("2-8 位数字或字母") 161 | self.set_desc_box = QCheckBox("上传文件自动设置描述") 162 | self.set_desc_var = AutoResizingTextEdit() 163 | self.big_file_box = QCheckBox(f"允许上传超过 {self.max_size}MB 的大文件") 164 | self.big_file_box.setToolTip("开启大文件上传支持 (功能下线)") 165 | self.upgrade_box = QCheckBox("自动检测新版本") 166 | self.upgrade_box.setToolTip("在软件打开时自动检测是否有新的版本发布,如有则弹出更新信息") 167 | 168 | self.time_fmt_box.toggle() 169 | self.time_fmt_box.stateChanged.connect(self.change_time_fmt) 170 | self.to_tray_box.stateChanged.connect(self.change_to_tray) 171 | self.watch_clipboard_box.stateChanged.connect(self.change_watch_clipboard) 172 | self.debug_box.stateChanged.connect(self.change_debug) 173 | self.set_pwd_box.stateChanged.connect(self.change_set_pwd) 174 | self.set_pwd_var.editingFinished.connect(self.check_pwd) 175 | self.set_desc_box.stateChanged.connect(self.change_set_desc) 176 | self.big_file_box.stateChanged.connect(self.change_big_file) 177 | self.upgrade_box.stateChanged.connect(self.change_upgrade) 178 | 179 | buttonBox = QDialogButtonBox() 180 | buttonBox.setOrientation(Qt.Orientation.Horizontal) 181 | buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Reset | QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel) 182 | buttonBox.button(QDialogButtonBox.StandardButton.Reset).setText("重置") 183 | buttonBox.button(QDialogButtonBox.StandardButton.Save).setText("保存") 184 | buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") 185 | buttonBox.button(QDialogButtonBox.StandardButton.Reset).clicked.connect(lambda: self.set_values(reset=True)) 186 | buttonBox.button(QDialogButtonBox.StandardButton.Save).clicked.connect(self.slot_save) 187 | buttonBox.rejected.connect(self.reject) 188 | 189 | form = QFormLayout() 190 | form.setLabelAlignment(Qt.AlignmentFlag.AlignRight) 191 | form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) # 覆盖MacOS的默认样式 192 | form.setSpacing(10) 193 | form.addRow(self.download_threads_lb, self.download_threads_var) 194 | form.addRow(self.timeout_lb, self.timeout_var) 195 | form.addRow(self.upload_delay_lb, self.upload_delay_var) 196 | form.addRow(self.max_size_lb, self.max_size_var) 197 | form.addRow(self.dl_path_lb, self.dl_path_var) 198 | 199 | vbox = QVBoxLayout() 200 | vbox.addWidget(logo) 201 | vbox.addStretch(1) 202 | vbox.addLayout(form) 203 | vbox.addStretch(1) 204 | hbox = QHBoxLayout() 205 | hbox.addWidget(self.time_fmt_box) 206 | hbox.addWidget(self.to_tray_box) 207 | hbox.addWidget(self.watch_clipboard_box) 208 | hbox.addWidget(self.debug_box) 209 | vbox.addLayout(hbox) 210 | vbox.addStretch(1) 211 | hbox_2 = QHBoxLayout() 212 | hbox_2.addWidget(self.set_pwd_box) 213 | hbox_2.addWidget(self.set_pwd_var) 214 | vbox.addLayout(hbox_2) 215 | vbox.addStretch(1) 216 | hbox_3 = QHBoxLayout() 217 | hbox_3.addWidget(self.set_desc_box) 218 | hbox_3.addWidget(self.set_desc_var) 219 | vbox.addLayout(hbox_3) 220 | hbox_4 = QHBoxLayout() 221 | hbox_4.addWidget(self.big_file_box) 222 | hbox_4.addWidget(self.upgrade_box) 223 | vbox.addStretch(1) 224 | vbox.addLayout(hbox_4) 225 | vbox.addStretch(2) 226 | vbox.addWidget(buttonBox) 227 | self.setLayout(vbox) 228 | self.setMinimumWidth(500) 229 | 230 | def change_time_fmt(self, state): 231 | if state == Qt.CheckState.Checked.value: 232 | self.time_fmt = True 233 | else: 234 | self.time_fmt = False 235 | 236 | def change_to_tray(self, state): 237 | if state == Qt.CheckState.Checked.value: 238 | self.to_tray = True 239 | else: 240 | self.to_tray = False 241 | 242 | def change_watch_clipboard(self, state): 243 | if state == Qt.CheckState.Checked.value: 244 | self.watch_clipboard = True 245 | else: 246 | self.watch_clipboard = False 247 | 248 | def change_debug(self, state): 249 | if state == Qt.CheckState.Checked.value: 250 | self.debug = True 251 | else: 252 | self.debug = False 253 | 254 | def change_big_file(self, state): 255 | if state == Qt.CheckState.Checked.value: 256 | self.allow_big_file = True 257 | else: 258 | self.allow_big_file = False 259 | 260 | def change_upgrade(self, state): 261 | if state == Qt.CheckState.Checked.value: 262 | self.upgrade = True 263 | else: 264 | self.upgrade = False 265 | 266 | def change_set_pwd(self, state): 267 | if state == Qt.CheckState.Checked.value: 268 | self.set_pwd = True 269 | self.set_pwd_var.setDisabled(False) 270 | else: 271 | self.set_pwd = False 272 | self.set_pwd_var.setDisabled(True) 273 | 274 | def change_set_desc(self, state): 275 | if state == Qt.CheckState.Checked.value: 276 | self.set_desc = True 277 | self.set_desc_var.setDisabled(False) 278 | else: 279 | self.set_desc = False 280 | self.set_desc_var.setDisabled(True) 281 | 282 | def check_pwd(self): 283 | pwd = self.set_pwd_var.toPlainText() 284 | pwd = ''.join(list(filter(str.isalnum, pwd))) 285 | if len(pwd) < 2: 286 | pwd = "" 287 | self.set_pwd_var.setText(pwd[:8]) 288 | 289 | def set_download_path(self): 290 | """设置下载路径""" 291 | dl_path = QFileDialog.getExistingDirectory(self, "选择文件下载保存文件夹", self.cwd) 292 | dl_path = os.path.normpath(dl_path) # windows backslash 293 | if dl_path == self.dl_path or dl_path == ".": 294 | return None 295 | self.dl_path_var.setText(dl_path) 296 | self.dl_path = dl_path 297 | 298 | def slot_save(self): 299 | """保存槽函数""" 300 | self._config.settings = self.get_values() 301 | self.saved.emit() 302 | self.close() 303 | -------------------------------------------------------------------------------- /lanzou/gui/dialogs/upload.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PyQt6.QtCore import Qt, pyqtSignal 3 | from PyQt6.QtGui import QIcon, QPixmap, QStandardItem, QStandardItemModel 4 | from PyQt6.QtWidgets import (QDialog, QLabel, QDialogButtonBox, QPushButton, QListView, 5 | QVBoxLayout, QHBoxLayout, QAbstractItemView, QFileDialog) 6 | 7 | from lanzou.gui.qss import dialog_qss_style 8 | from lanzou.gui.others import MyListView 9 | from lanzou.gui.models import UpJob 10 | from lanzou.debug import SRC_DIR 11 | 12 | 13 | class UploadDialog(QDialog): 14 | """文件上传对话框""" 15 | new_infos = pyqtSignal(object) 16 | 17 | def __init__(self, user_home): 18 | super().__init__() 19 | self.cwd = user_home 20 | self._folder_id = -1 21 | self._folder_name = "LanZouCloud" 22 | self.set_pwd = False 23 | self.set_desc = False 24 | self.pwd = '' 25 | self.desc = '' 26 | self.allow_big_file = False 27 | self.max_size = 100 28 | self.selected = [] 29 | self.initUI() 30 | self.set_size() 31 | self.setStyleSheet(dialog_qss_style) 32 | 33 | def set_pwd_desc_bigfile(self, settings): 34 | self.set_pwd = settings["set_pwd"] 35 | self.set_desc = settings["set_desc"] 36 | self.pwd = settings["pwd"] 37 | self.desc = settings["desc"] 38 | self.allow_big_file = settings["allow_big_file"] 39 | self.max_size = settings["max_size"] 40 | if self.allow_big_file: 41 | self.btn_chooseMultiFile.setToolTip("") 42 | else: 43 | self.btn_chooseMultiFile.setToolTip(f"文件大小上限 {self.max_size}MB") 44 | 45 | def set_values(self, folder_name, folder_id, files): 46 | self.setWindowTitle("上传文件至 ➩ " + str(folder_name)) 47 | self._folder_id = folder_id 48 | self._folder_name = folder_name 49 | if files: 50 | self.selected = files 51 | self.show_selected() 52 | self.exec() 53 | 54 | def initUI(self): 55 | self.setWindowTitle("上传文件") 56 | self.setWindowIcon(QIcon(SRC_DIR + "upload.ico")) 57 | self.logo = QLabel() 58 | self.logo.setPixmap(QPixmap(SRC_DIR + "logo3.gif")) 59 | self.logo.setStyleSheet("background-color:rgb(0,153,255);") 60 | self.logo.setAlignment(Qt.AlignmentFlag.AlignCenter) 61 | 62 | # btn 1 63 | self.btn_chooseDir = QPushButton("选择文件夹", self) 64 | self.btn_chooseDir.setObjectName("btn_chooseDir") 65 | self.btn_chooseDir.setObjectName("btn_chooseDir") 66 | self.btn_chooseDir.setIcon(QIcon(SRC_DIR + "folder.gif")) 67 | 68 | # btn 2 69 | self.btn_chooseMultiFile = QPushButton("选择多文件", self) 70 | self.btn_chooseDir.setObjectName("btn_chooseMultiFile") 71 | self.btn_chooseMultiFile.setObjectName("btn_chooseMultiFile") 72 | self.btn_chooseMultiFile.setIcon(QIcon(SRC_DIR + "file.ico")) 73 | 74 | # btn 3 75 | self.btn_deleteSelect = QPushButton("移除", self) 76 | self.btn_deleteSelect.setObjectName("btn_deleteSelect") 77 | self.btn_deleteSelect.setIcon(QIcon(SRC_DIR + "delete.ico")) 78 | self.btn_deleteSelect.setToolTip("按 Delete 移除选中文件") 79 | 80 | # 列表 81 | self.list_view = MyListView() 82 | self.list_view.drop_files.connect(self.add_drop_files) 83 | self.list_view.setViewMode(QListView.ViewMode.ListMode) 84 | self.slm = QStandardItem() 85 | self.model = QStandardItemModel() 86 | self.list_view.setModel(self.model) 87 | self.model.removeRows(0, self.model.rowCount()) # 清除旧的选择 88 | self.list_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) 89 | self.list_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) 90 | self.list_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) 91 | 92 | self.buttonBox = QDialogButtonBox() 93 | self.buttonBox.setOrientation(Qt.Orientation.Horizontal) 94 | self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) 95 | self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") 96 | self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") 97 | 98 | vbox = QVBoxLayout() 99 | hbox_head = QHBoxLayout() 100 | hbox_button = QHBoxLayout() 101 | hbox_head.addWidget(self.btn_chooseDir) 102 | hbox_head.addStretch(1) 103 | hbox_head.addWidget(self.btn_chooseMultiFile) 104 | hbox_button.addWidget(self.btn_deleteSelect) 105 | hbox_button.addStretch(1) 106 | hbox_button.addWidget(self.buttonBox) 107 | vbox.addWidget(self.logo) 108 | vbox.addLayout(hbox_head) 109 | vbox.addWidget(self.list_view) 110 | vbox.addLayout(hbox_button) 111 | self.setLayout(vbox) 112 | self.setMinimumWidth(350) 113 | 114 | # 设置信号 115 | self.btn_chooseDir.clicked.connect(self.slot_btn_chooseDir) 116 | self.btn_chooseMultiFile.clicked.connect(self.slot_btn_chooseMultiFile) 117 | self.btn_deleteSelect.clicked.connect(self.slot_btn_deleteSelect) 118 | 119 | self.buttonBox.accepted.connect(self.slot_btn_ok) 120 | self.buttonBox.accepted.connect(self.accept) 121 | self.buttonBox.rejected.connect(self.clear_old) 122 | self.buttonBox.rejected.connect(self.reject) 123 | 124 | def set_size(self): 125 | if self.selected: 126 | h = 18 if len(self.selected) > 18 else 10 127 | w = 40 128 | for i in self.selected: 129 | i_len = len(i) 130 | if i_len > 100: 131 | w = 100 132 | break 133 | if i_len > w: 134 | w = i_len 135 | self.resize(120 + w * 7, h * 30) 136 | else: 137 | self.resize(400, 300) 138 | 139 | def clear_old(self): 140 | self.selected = [] 141 | self.model.removeRows(0, self.model.rowCount()) 142 | self.set_size() 143 | 144 | def show_selected(self): 145 | self.model.removeRows(0, self.model.rowCount()) 146 | for item in self.selected: 147 | if os.path.isfile(item): 148 | self.model.appendRow(QStandardItem(QIcon(SRC_DIR + "file.ico"), item)) 149 | else: 150 | self.model.appendRow(QStandardItem(QIcon(SRC_DIR + "folder.gif"), item)) 151 | self.set_size() 152 | 153 | def backslash(self): 154 | """Windows backslash""" 155 | tasks = {} 156 | for item in self.selected: 157 | url = os.path.normpath(item) 158 | total_size = 0 159 | total_file = 0 160 | if os.path.isfile(url): 161 | total_size = os.path.getsize(url) 162 | if not total_size: 163 | continue # 空文件无法上传 164 | total_file += 1 165 | else: 166 | for filename in os.listdir(url): 167 | file_path = os.path.join(url, filename) 168 | if not os.path.isfile(file_path): 169 | continue # 跳过子文件夹 170 | total_size += os.path.getsize(file_path) 171 | total_file += 1 172 | tasks[url] = UpJob(url=url, 173 | fid=self._folder_id, 174 | folder=self._folder_name, 175 | pwd=self.pwd if self.set_pwd else None, 176 | desc=self.desc if self.set_desc else None, 177 | total_size=total_size, 178 | total_file=total_file) 179 | return tasks 180 | 181 | def slot_btn_ok(self): 182 | tasks = self.backslash() 183 | if self.selected: 184 | self.new_infos.emit(tasks) 185 | self.clear_old() 186 | 187 | def slot_btn_deleteSelect(self): 188 | _indexes = self.list_view.selectionModel().selection().indexes() 189 | if not _indexes: 190 | return 191 | indexes = [] 192 | for i in _indexes: # 获取所选行号 193 | indexes.append(i.row()) 194 | indexes = set(indexes) 195 | for i in sorted(indexes, reverse=True): 196 | self.selected.remove(self.model.item(i, 0).text()) 197 | self.model.removeRow(i) 198 | self.set_size() 199 | 200 | def add_drop_files(self, files): 201 | for item in files: 202 | if item not in self.selected: 203 | self.selected.append(item) 204 | self.show_selected() 205 | 206 | def slot_btn_chooseDir(self): 207 | dir_choose = QFileDialog.getExistingDirectory(self, "选择文件夹", self.cwd) # 起始路径 208 | if dir_choose == "": 209 | return 210 | if dir_choose not in self.selected: 211 | self.selected.append(dir_choose) 212 | self.cwd = os.path.dirname(dir_choose) 213 | self.show_selected() 214 | 215 | def slot_btn_chooseMultiFile(self): 216 | files, _ = QFileDialog.getOpenFileNames(self, "选择多文件", self.cwd, "All Files (*)") 217 | if len(files) == 0: 218 | return 219 | for _file in files: 220 | if _file not in self.selected: 221 | if os.path.getsize(_file) <= self.max_size * 1048576: 222 | self.selected.append(_file) 223 | elif self.allow_big_file: 224 | self.selected.append(_file) 225 | self.show_selected() 226 | 227 | def keyPressEvent(self, e): 228 | if e.key() == Qt.Key.Key_Delete: # delete 229 | self.slot_btn_deleteSelect() 230 | -------------------------------------------------------------------------------- /lanzou/gui/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | 容器类,用于储存 上传、下载 任务,文件、文件夹信息 3 | """ 4 | 5 | import os 6 | 7 | 8 | class Job(): 9 | def __init__(self, url, tp, total_file=1): 10 | self._url = url 11 | self._type = tp 12 | self._total_file = total_file 13 | self._err_info = None 14 | self._run = False 15 | self._rate = 0 16 | self._current = 1 17 | self._speed = '' 18 | self._pause = False 19 | self._added = False 20 | self._now_size = 0 21 | self._total_size = 0 22 | 23 | @property 24 | def url(self): 25 | return self._url 26 | 27 | @property 28 | def info(self): 29 | return self._err_info 30 | 31 | @info.setter 32 | def info(self, info): 33 | self._err_info = info 34 | 35 | @property 36 | def run(self): 37 | return self._run 38 | 39 | @run.setter 40 | def run(self, run): 41 | self._run = run 42 | 43 | @property 44 | def rate(self): 45 | return self._rate 46 | 47 | @rate.setter 48 | def rate(self, rate): 49 | self._rate = rate 50 | 51 | @property 52 | def total_file(self): 53 | return self._total_file 54 | 55 | @total_file.setter 56 | def total_file(self, total_file): 57 | self._total_file = total_file 58 | 59 | @property 60 | def current(self): 61 | return self._current 62 | 63 | @current.setter 64 | def current(self, current): 65 | self._current = current 66 | 67 | @property 68 | def speed(self): 69 | return self._speed 70 | 71 | @speed.setter 72 | def speed(self, speed): 73 | self._speed = speed 74 | 75 | @property 76 | def prog(self): 77 | return f"({self._current}/{self._total_file})" if self._total_file > 1 else '' 78 | 79 | @property 80 | def type(self): 81 | return self._type 82 | 83 | @property 84 | def now_size(self): 85 | return self._now_size 86 | 87 | @now_size.setter 88 | def now_size(self, now_size): 89 | self._now_size = now_size 90 | 91 | @property 92 | def total_size(self): 93 | return self._total_size 94 | 95 | @total_size.setter 96 | def total_size(self, total_size): 97 | self._total_size = total_size 98 | 99 | @property 100 | def pause(self): 101 | return self._pause 102 | 103 | @pause.setter 104 | def pause(self, pause): 105 | self._pause = pause 106 | 107 | @property 108 | def added(self): 109 | return self._added 110 | 111 | @added.setter 112 | def added(self, added): 113 | self._added = added 114 | 115 | 116 | class DlJob(Job): 117 | def __init__(self, infos, path, total_file=1): 118 | """info: lanzou.gui.models.FileInfos | ShareInfo 119 | ShareInfo(code=0, name, url, pwd, desc, time, size) 120 | """ 121 | super(DlJob, self).__init__(infos.url, 'dl', total_file) 122 | self._infos = infos 123 | self._path = path 124 | self.size = infos.size 125 | 126 | @property 127 | def name(self): 128 | return self._infos.name 129 | 130 | @property 131 | def pwd(self): 132 | return self._infos.pwd 133 | 134 | @property 135 | def path(self): 136 | return self._path 137 | 138 | @path.setter 139 | def path(self, path): 140 | self._path = path 141 | 142 | 143 | class UpJob(Job): 144 | def __init__(self, url, fid, folder, pwd=None, desc=None, total_size=0, total_file=1): 145 | super(UpJob, self).__init__(url, 'up') 146 | self._fid = fid 147 | self._folder = folder 148 | self._pwd = pwd 149 | self._desc = desc 150 | self._total_size = total_size 151 | self._total_file = total_file 152 | 153 | @property 154 | def fid(self): 155 | return self._fid 156 | 157 | @property 158 | def name(self): 159 | return os.path.basename(self._url) 160 | 161 | @property 162 | def folder(self): 163 | return self._folder 164 | 165 | @property 166 | def pwd(self): 167 | return self._pwd 168 | 169 | @property 170 | def desc(self): 171 | return self._desc 172 | 173 | 174 | class Tasks(object): 175 | def __init__(self): 176 | self._items = {} 177 | self._dones = {} 178 | self._all = {} 179 | 180 | def __len__(self): 181 | return len(self._all) 182 | 183 | def __getitem__(self, index): 184 | return self._all[index] 185 | 186 | def __iter__(self): 187 | return iter(self._items) 188 | 189 | def add(self, tasks): 190 | """添加任务""" 191 | for key, value in tasks.items(): 192 | if value.rate >= 1000: 193 | self._dones.update({key: value}) 194 | if key in self._items: 195 | del self._items[key] 196 | else: 197 | self._items.update({key: value}) 198 | if key in self._dones: 199 | del self._dones[key] 200 | self._all = {**self._items, **self._dones} 201 | 202 | def update(self): 203 | """更新元素""" 204 | changed = False 205 | for key, value in self._items.copy().items(): 206 | if value.rate >= 1000: 207 | self._dones.update({key: value}) 208 | del self._items[key] 209 | changed = True 210 | if changed: 211 | self._all = {**self._items, **self._dones} 212 | 213 | def items(self): 214 | return self._all.items() 215 | 216 | def values(self): 217 | return self._all.values() 218 | 219 | def clear(self, task=None): 220 | """清空元素""" 221 | if task: 222 | if task.url in self._dones: 223 | del self._dones[task.url] 224 | elif task.url in self._items: 225 | del self._items[task.url] 226 | else: 227 | self._dones.clear() 228 | self._all = {**self._items, **self._dones} 229 | 230 | 231 | class Infos: 232 | def __init__(self, name='', is_file=True, fid='', time='', size='', downs=0, desc='', pwd='', url='', durl=''): 233 | self._name = name 234 | self._is_file = is_file 235 | self._fid = fid 236 | self._time = time 237 | self._size = size 238 | self._downs = downs 239 | self._desc = desc 240 | self._pwd = pwd 241 | self._url = url 242 | self._durl = durl 243 | self._has_pwd = False 244 | self._new_pwd = '' 245 | self._new_des = '' 246 | self._new_name = '' 247 | self._new_fid = '' 248 | 249 | @property 250 | def name(self): 251 | return self._name 252 | 253 | @property 254 | def is_file(self): 255 | return self._is_file 256 | 257 | @is_file.setter 258 | def is_file(self, is_file): 259 | self._is_file = is_file 260 | 261 | @property 262 | def id(self): 263 | return self._fid 264 | 265 | @property 266 | def size(self): 267 | return self._size 268 | 269 | @property 270 | def time(self): 271 | return self._time 272 | 273 | @property 274 | def downs(self): 275 | return self._downs 276 | 277 | @property 278 | def desc(self): 279 | return self._desc 280 | 281 | @desc.setter 282 | def desc(self, desc): 283 | self._desc = desc 284 | 285 | @property 286 | def pwd(self): 287 | return self._pwd 288 | 289 | @pwd.setter 290 | def pwd(self, pwd): 291 | self._pwd = pwd 292 | 293 | @property 294 | def url(self): 295 | return self._url 296 | 297 | @url.setter 298 | def url(self, url): 299 | self._url = url 300 | 301 | @property 302 | def durl(self): 303 | return self._durl 304 | 305 | @durl.setter 306 | def durl(self, durl): 307 | self._durl = durl 308 | 309 | @property 310 | def has_pwd(self): 311 | return self._has_pwd 312 | 313 | @property 314 | def new_pwd(self): 315 | return self._new_pwd 316 | 317 | @new_pwd.setter 318 | def new_pwd(self, new_pwd): 319 | self._new_pwd = new_pwd 320 | 321 | @property 322 | def new_des(self): 323 | return self._new_des 324 | 325 | @new_des.setter 326 | def new_des(self, new_des): 327 | self._new_des = new_des 328 | 329 | @property 330 | def new_name(self): 331 | return self._new_name 332 | 333 | @new_name.setter 334 | def new_name(self, new_name): 335 | self._new_name = new_name 336 | 337 | @property 338 | def new_id(self): 339 | return self._new_fid 340 | 341 | @new_id.setter 342 | def new_id(self, new_id): 343 | self._new_fid = new_id 344 | 345 | 346 | class FileInfos(Infos): 347 | def __init__(self, file): 348 | super(FileInfos, self).__init__(is_file=True) 349 | self._name = file.name 350 | self._fid = file.id 351 | self._time = file.time 352 | self._size = file.size 353 | self._downs = file.downs 354 | self._has_pwd = file.has_pwd 355 | self._has_des = file.has_des 356 | 357 | @property 358 | def has_des(self): 359 | return self._has_des 360 | 361 | 362 | class FolderInfos(Infos): 363 | def __init__(self, folder): 364 | super(FolderInfos, self).__init__(is_file=False) 365 | self._name = folder.name 366 | self._fid = folder.id 367 | self._desc = folder.desc 368 | self._has_pwd = folder.has_pwd 369 | 370 | 371 | class ShareFileInfos(Infos): 372 | def __init__(self, file): 373 | super(ShareFileInfos, self).__init__(is_file=True) 374 | self._name = file.name 375 | self._time = file.time 376 | self._size = file.size 377 | self._url = file.url 378 | self._pwd = file.pwd 379 | -------------------------------------------------------------------------------- /lanzou/gui/others.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 重新封装的控件 3 | ''' 4 | 5 | import os 6 | from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QSize 7 | from PyQt6.QtGui import QTextDocument, QAbstractTextDocumentLayout, QPalette, QFontMetrics, QIcon, QStandardItem 8 | from PyQt6.QtWidgets import (QApplication, QAbstractItemView, QStyle, QListView, QLineEdit, QTableView, 9 | QPushButton, QStyledItemDelegate, QStyleOptionViewItem, QTextEdit, QSizePolicy) 10 | 11 | from lanzou.debug import SRC_DIR 12 | 13 | 14 | def set_file_icon(name): 15 | suffix = name.split(".")[-1] 16 | ico_path = SRC_DIR + f"{suffix}.gif" 17 | if os.path.isfile(ico_path): 18 | return QIcon(ico_path) 19 | else: 20 | return QIcon(SRC_DIR + "file.ico") 21 | 22 | 23 | class QDoublePushButton(QPushButton): 24 | """加入了双击事件的按钮""" 25 | doubleClicked = pyqtSignal() 26 | clicked = pyqtSignal() 27 | 28 | def __init__(self, *args, **kwargs): 29 | QPushButton.__init__(self, *args, **kwargs) 30 | self.timer = QTimer() 31 | self.timer.setSingleShot(True) 32 | self.timer.timeout.connect(self.clicked.emit) 33 | super().clicked.connect(self.checkDoubleClick) 34 | 35 | def checkDoubleClick(self): 36 | if self.timer.isActive(): 37 | self.doubleClicked.emit() 38 | self.timer.stop() 39 | else: 40 | self.timer.start(250) 41 | 42 | 43 | class MyLineEdit(QLineEdit): 44 | """添加单击事件的输入框,用于设置下载路径""" 45 | 46 | clicked = pyqtSignal() 47 | 48 | def __init__(self, parent): 49 | super(MyLineEdit, self).__init__(parent) 50 | 51 | def mouseReleaseEvent(self, QMouseEvent): 52 | if QMouseEvent.button() == Qt.MouseButton.LeftButton: 53 | self.clicked.emit() 54 | 55 | 56 | class MyListView(QListView): 57 | """加入拖拽功能的列表显示器""" 58 | drop_files = pyqtSignal(object) 59 | 60 | def __init__(self): 61 | QListView.__init__(self) 62 | 63 | self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) 64 | self.setDragEnabled(True) 65 | self.setAcceptDrops(True) 66 | self.setDropIndicatorShown(True) 67 | 68 | def dragEnterEvent(self, event): 69 | m = event.mimeData() 70 | if m.hasUrls(): 71 | for url in m.urls(): 72 | if url.isLocalFile(): 73 | event.accept() 74 | return 75 | event.ignore() 76 | 77 | def dropEvent(self, event): 78 | if event.source(): 79 | QListView.dropEvent(self, event) 80 | else: 81 | m = event.mimeData() 82 | if m.hasUrls(): 83 | urls = [url.toLocalFile() for url in m.urls() if url.isLocalFile()] 84 | if urls: 85 | self.drop_files.emit(urls) 86 | event.acceptProposedAction() 87 | 88 | 89 | class AutoResizingTextEdit(QTextEdit): 90 | """添加单击事件的自动改变大小的文本输入框,用于显示描述与下载直链 91 | https://github.com/cameel/auto-resizing-text-edit 92 | https://gist.github.com/hahastudio/4345418 93 | """ 94 | clicked = pyqtSignal() 95 | editingFinished = pyqtSignal() 96 | 97 | def __init__(self, parent=None): 98 | super(AutoResizingTextEdit, self).__init__(parent) 99 | 100 | # This seems to have no effect. I have expected that it will cause self.hasHeightForWidth() 101 | # to start returning True, but it hasn't - that's why I hardcoded it to True there anyway. 102 | # I still set it to True in size policy just in case - for consistency. 103 | size_policy = self.sizePolicy() 104 | size_policy.setHeightForWidth(True) 105 | size_policy.setVerticalPolicy(QSizePolicy.Policy.Preferred) 106 | self.setSizePolicy(size_policy) 107 | self.textChanged.connect(self.updateGeometry) 108 | 109 | self._changed = False 110 | self.setTabChangesFocus(True) 111 | self.textChanged.connect(self._handle_text_changed) 112 | 113 | def setMinimumLines(self, num_lines): 114 | """ Sets minimum widget height to a value corresponding to specified number of lines 115 | in the default font. """ 116 | 117 | self.setMinimumSize(self.minimumSize().width(), self.lineCountToWidgetHeight(num_lines)) 118 | 119 | def heightForWidth(self, width): 120 | margins = self.contentsMargins() 121 | 122 | if width >= margins.left() + margins.right(): 123 | document_width = width - margins.left() - margins.right() 124 | else: 125 | # If specified width can't even fit the margin, there's no space left for the document 126 | document_width = 0 127 | 128 | # Cloning the whole document only to check its size at different width seems wasteful 129 | # but apparently it's the only and preferred way to do this in Qt >= 4. QTextDocument does not 130 | # provide any means to get height for specified width (as some QWidget subclasses do). 131 | # Neither does QTextEdit. In Qt3 Q3TextEdit had working implementation of heightForWidth() 132 | # but it was allegedly just a hack and was removed. 133 | # 134 | # The performance probably won't be a problem here because the application is meant to 135 | # work with a lot of small notes rather than few big ones. And there's usually only one 136 | # editor that needs to be dynamically resized - the one having focus. 137 | document = self.document().clone() 138 | document.setTextWidth(document_width) 139 | 140 | return margins.top() + document.size().height() + margins.bottom() 141 | 142 | def sizeHint(self): 143 | original_hint = super(AutoResizingTextEdit, self).sizeHint() 144 | return QSize(original_hint.width(), self.heightForWidth(original_hint.width())) 145 | 146 | def mouseReleaseEvent(self, QMouseEvent): 147 | if QMouseEvent.button() == Qt.MouseButton.LeftButton: 148 | if not self.toPlainText(): 149 | self.clicked.emit() 150 | 151 | def lineCountToWidgetHeight(self, num_lines): 152 | """ Returns the number of pixels corresponding to the height of specified number of lines 153 | in the default font. """ 154 | 155 | # ASSUMPTION: The document uses only the default font 156 | 157 | assert num_lines >= 0 158 | 159 | widget_margins = self.contentsMargins() 160 | document_margin = self.document().documentMargin() 161 | font_metrics = QFontMetrics(self.document().defaultFont()) 162 | 163 | # font_metrics.lineSpacing() is ignored because it seems to be already included in font_metrics.height() 164 | return ( 165 | widget_margins.top() + 166 | document_margin + 167 | max(num_lines, 1) * font_metrics.height() + 168 | self.document().documentMargin() + 169 | widget_margins.bottom() 170 | ) 171 | 172 | def focusOutEvent(self, event): 173 | if self._changed: 174 | self.editingFinished.emit() 175 | super(AutoResizingTextEdit, self).focusOutEvent(event) 176 | 177 | def _handle_text_changed(self): 178 | self._changed = True 179 | 180 | 181 | class TableDelegate(QStyledItemDelegate): 182 | """Table 富文本""" 183 | def __init__(self, parent=None): 184 | super(TableDelegate, self).__init__(parent) 185 | self.doc = QTextDocument(self) 186 | 187 | def paint(self, painter, option, index): 188 | painter.save() 189 | options = QStyleOptionViewItem(option) 190 | self.initStyleOption(options, index) 191 | self.doc.setHtml(options.text) 192 | options.text = "" # 原字符 193 | style = QApplication.style() if options.widget is None else options.widget.style() 194 | style.drawControl(QStyle.ControlElement.CE_ItemViewItem, options, painter) 195 | 196 | ctx = QAbstractTextDocumentLayout.PaintContext() 197 | 198 | if option.state & QStyle.StateFlag.State_Selected: 199 | ctx.palette.setColor(QPalette.ColorRole.Text, option.palette.color( 200 | QPalette.ColorGroup.Active, QPalette.ColorRole.HighlightedText)) 201 | else: 202 | ctx.palette.setColor(QPalette.ColorRole.Text, option.palette.color( 203 | QPalette.ColorGroup.Active, QPalette.ColorRole.Text)) 204 | 205 | text_rect = style.subElementRect(QStyle.SubElement.SE_ItemViewItemText, options) 206 | 207 | the_fuck_your_shit_up_constant = 3 #  ̄へ ̄ # 208 | margin = (option.rect.height() - options.fontMetrics.height()) // 2 209 | margin = margin - the_fuck_your_shit_up_constant 210 | text_rect.setTop(text_rect.top() + margin) 211 | 212 | painter.translate(text_rect.topLeft()) 213 | painter.setClipRect(text_rect.translated(-text_rect.topLeft())) 214 | self.doc.documentLayout().draw(painter, ctx) 215 | 216 | painter.restore() 217 | 218 | def sizeHint(self, option, index): 219 | options = QStyleOptionViewItem(option) 220 | self.initStyleOption(options, index) 221 | self.doc.setHtml(options.text) 222 | self.doc.setTextWidth(options.rect.width()) 223 | return QSize(self.doc.idealWidth(), self.doc.size().height()) 224 | 225 | 226 | class MyStandardItem(QStandardItem): 227 | def __lt__(self, other): 228 | if self.data(Qt.ItemDataRole.UserRole) and other.data(Qt.ItemDataRole.UserRole): 229 | return self.data(Qt.ItemDataRole.UserRole) < other.data(Qt.ItemDataRole.UserRole) 230 | else: # 没有setData并设置UserRole,则使用默认的方式进行比较排序 231 | return self.text() < other.text() 232 | 233 | 234 | class MyTableView(QTableView): 235 | """加入拖拽功能的表格显示器""" 236 | drop_files = pyqtSignal(object) 237 | 238 | def __init__(self, parent): 239 | super(MyTableView, self).__init__(parent) 240 | self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) 241 | self.setDragEnabled(True) 242 | self.setAcceptDrops(True) 243 | self.setDropIndicatorShown(True) 244 | 245 | def dragEnterEvent(self, event): 246 | m = event.mimeData() 247 | if m.hasUrls(): 248 | for url in m.urls(): 249 | if url.isLocalFile(): 250 | event.accept() 251 | return 252 | event.ignore() 253 | 254 | def dropEvent(self, event): 255 | if event.source(): 256 | QListView.dropEvent(self, event) 257 | else: 258 | m = event.mimeData() 259 | if m.hasUrls(): 260 | urls = [url.toLocalFile() for url in m.urls() if url.isLocalFile()] 261 | if urls: 262 | self.drop_files.emit(urls) 263 | event.acceptProposedAction() 264 | -------------------------------------------------------------------------------- /lanzou/gui/qss.py: -------------------------------------------------------------------------------- 1 | ''' 2 | QSS 样式 3 | ''' 4 | 5 | from lanzou.debug import BG_IMG 6 | from platform import system as platform 7 | 8 | 9 | jobs_btn_redo_style = ''' 10 | QPushButton { 11 | color: white; 12 | background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,stop: 0 #38d, 13 | stop: 0.1 #93e, stop: 0.49 #77c, stop: 0.5 #16b, stop: 1 #07c); 14 | border-width: 1px; 15 | border-color: #339; 16 | border-style: solid; 17 | border-radius: 7; 18 | padding: 3px; 19 | font-size: 13px; 20 | padding-left: 5px; 21 | padding-right: 5px; 22 | min-width: 20px; 23 | min-height: 14px; 24 | max-height: 14px; 25 | } 26 | ''' 27 | 28 | jobs_btn_completed_style = ''' 29 | QPushButton { 30 | color: white; 31 | background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,stop: 0 #18d, 32 | stop: 0.1 #9ae, stop: 0.49 #97c, stop: 0.5 #16b, stop: 1 #77c); 33 | border-width: 1px; 34 | border-color: #339; 35 | border-style: solid; 36 | border-radius: 7; 37 | padding: 3px; 38 | font-size: 13px; 39 | padding-left: 5px; 40 | padding-right: 5px; 41 | min-width: 20px; 42 | min-height: 14px; 43 | max-height: 14px; 44 | } 45 | ''' 46 | 47 | jobs_btn_delete_style = ''' 48 | QPushButton { 49 | color: white; 50 | background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,stop: 0 #f00, 51 | stop: 0.25 #008080, stop: 0.49 #97c, stop: 0.5 #16b, stop: 1 #f00); 52 | border-width: 1px; 53 | border-color: #339; 54 | border-style: solid; 55 | border-radius: 7; 56 | padding: 3px; 57 | font-size: 13px; 58 | padding-left: 5px; 59 | padding-right: 5px; 60 | min-width: 20px; 61 | min-height: 14px; 62 | max-height: 14px; 63 | } 64 | ''' 65 | 66 | jobs_btn_processing_style = ''' 67 | QPushButton { 68 | color: white; 69 | background-color: QLinearGradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(9, 41, 4, 255), stop:0.085 rgba(2, 79, 0, 255), stop:0.19 rgba(50, 147, 22, 255), stop:0.275 rgba(236, 191, 49, 255), stop:0.39 rgba(243, 61, 34, 255), stop:0.555 rgba(135, 81, 60, 255), stop:0.667 rgba(121, 75, 255, 255), stop:0.825 rgba(164, 255, 244, 255), stop:0.885 rgba(104, 222, 71, 255), stop:1 rgba(93, 128, 0, 255)); 70 | border-width: 1px; 71 | border-color: #339; 72 | border-style: solid; 73 | border-radius: 7; 74 | padding: 3px; 75 | font-size: 13px; 76 | padding-left: 5px; 77 | padding-right: 5px; 78 | min-width: 20px; 79 | min-height: 14px; 80 | max-height: 14px; 81 | } 82 | ''' 83 | 84 | jobs_btn_queue_style = ''' 85 | QPushButton { 86 | color: white; 87 | background-color: QLinearGradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(124, 252, 0, 255), stop:0.085 rgba(186, 85, 211, 255), stop:0.19 rgba(148, 0, 211, 255), stop:0.275 rgba(255,20,147, 255), stop:0.39 rgba(112,128,144, 255), stop:0.555 rgba(112,128,144, 255), stop:0.667 rgba(255,20,147, 255), stop:0.825 rgba(148, 0, 211, 255), stop:0.885 rgba(186, 85, 211, 255), stop:1 rgba(124, 252, 0, 255)); 88 | border-width: 1px; 89 | border-color: #339; 90 | border-style: solid; 91 | border-radius: 7; 92 | padding: 3px; 93 | font-size: 13px; 94 | padding-left: 5px; 95 | padding-right: 5px; 96 | min-width: 20px; 97 | min-height: 14px; 98 | max-height: 14px; 99 | } 100 | ''' 101 | 102 | qssStyle = ''' 103 | QPushButton { 104 | background-color: rgba(255, 130, 71, 100); 105 | } 106 | #table_share, #table_jobs, #table_disk, #table_rec { 107 | background-color: rgba(255, 255, 255, 150); 108 | } 109 | QTabWidget::pane { 110 | border: 1px; 111 | /* background:transparent; # 完全透明 */ 112 | background-color: rgba(255, 255, 255, 90); 113 | } 114 | QTabWidget::tab-bar { 115 | background:transparent; 116 | subcontrol-position:center; 117 | } 118 | QTabBar::tab { 119 | min-width:150px; 120 | min-height:30px; 121 | background:transparent; 122 | } 123 | QTabBar::tab:selected { 124 | color: rgb(153, 50, 204); 125 | background:transparent; 126 | font-weight:bold; 127 | } 128 | QTabBar::tab:!selected { 129 | color: rgb(28, 28, 28); 130 | background:transparent; 131 | } 132 | QTabBar::tab:hover { 133 | color: rgb(0, 0, 205); 134 | background:transparent; 135 | } 136 | #tabWidget QTabBar{ 137 | background-color: #AEEEEE; 138 | } 139 | /*提取界面文字颜色*/ 140 | #label_share_url { 141 | color: rgb(255,255,60); 142 | } 143 | #label_dl_path { 144 | color: rgb(255,255,60); 145 | } 146 | /*网盘界面文字颜色*/ 147 | #label_disk_loc { 148 | color: rgb(0,0,0); 149 | font-weight:bold; 150 | } 151 | #disk_tab { 152 | background-color: rgba(255, 255, 255, 120); 153 | } 154 | /*状态栏隐藏控件分隔符*/ 155 | #statusbar { 156 | font-size: 14px; 157 | color: white; 158 | } 159 | #msg_label, #msg_movie_lb { 160 | font-size: 14px; 161 | color: white; 162 | background:transparent; 163 | } 164 | QStatusBar::item { 165 | border: None; 166 | } 167 | /*菜单栏*/ 168 | #menubar { 169 | background-color: transparent; 170 | } 171 | QMenuBar::item { 172 | color:pink; 173 | margin-top:4px; 174 | spacing: 3px; 175 | padding: 1px 10px; 176 | background: transparent; 177 | border-radius: 4px; 178 | } 179 | /* when selected using mouse or keyboard */ 180 | QMenuBar::item:selected { 181 | color: white; 182 | background: #a8a8a8; 183 | } 184 | QMenuBar::item:pressed { 185 | color: lightgreen; 186 | background: #888888; 187 | } 188 | ''' 189 | 190 | if platform() == 'Darwin': # MacOS 不使用自定义样式 191 | qssStyle = '' 192 | else: 193 | qssStyle = qssStyle + f""" 194 | #MainWindow {{ 195 | border-image:url({BG_IMG}); 196 | }}""" 197 | 198 | 199 | btn_style = """ 200 | QPushButton { 201 | color: white; 202 | background-color: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1,stop: 0 #88d, 203 | stop: 0.1 #99e, stop: 0.49 #77c, stop: 0.5 #66b, stop: 1 #77c); 204 | border-width: 1px; 205 | border-color: #339; 206 | border-style: solid; 207 | border-radius: 7; 208 | padding: 3px; 209 | font-size: 13px; 210 | padding-left: 5px; 211 | padding-right: 5px; 212 | min-width: 70px; 213 | min-height: 14px; 214 | max-height: 14px; 215 | } 216 | """ 217 | 218 | others_style = """ 219 | QLabel { 220 | font-weight: 400; 221 | font-size: 14px; 222 | } 223 | QLineEdit { 224 | padding: 1px; 225 | border-style: solid; 226 | border: 2px solid gray; 227 | border-radius: 8px; 228 | } 229 | QTextEdit { 230 | padding: 1px; 231 | border-style: solid; 232 | border: 2px solid gray; 233 | border-radius: 8px; 234 | } 235 | #btn_chooseMultiFile, #btn_chooseDir { 236 | min-width: 90px; 237 | max-width: 90px; 238 | } 239 | """ 240 | 241 | dialog_qss_style = others_style + btn_style 242 | # https://thesmithfam.org/blog/2009/09/10/qt-stylesheets-tutorial/ 243 | -------------------------------------------------------------------------------- /lanzou/gui/todo.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | 1. 设置用户所有分享界面 提前码: 4 | (https://lanzous.com/u/[用户名/注册手机号]) 5 | 6 | https://pc.woozooo.com/mydisk.php?item=profile 7 | POST 8 | 9 | data: { 10 | action=password 11 | task=codepwd 12 | formhash=576b5416 13 | codeoff=1 # 开启密码 14 | code='密码' 15 | } 16 | 17 | resp: 18 | text 19 | ```html 20 |
21 |
提示信息
22 |
23 |

恭喜您,设置成功

24 |
25 | ``` 26 | 27 | 2. 获取使用 cookie 登录用户名: 28 | 29 | https://pc.woozooo.com/mydisk.php?item=profile&action=mypower 30 | GET 31 | 32 | resp: 33 | ```py 34 | re.fandall(r'url ='https://wwa.lanzous.com/u/(\w+)?t2';', resp.text) 35 | ``` 36 | 37 | 3. 个性域名设置: 38 | 39 | ``` 40 | type : 'post', 41 | url : '/doupload.php', 42 | data : { 'task':48,'domain':domainadd}, 43 | dataType : 'json', 44 | success:function(msg){ 45 | if(msg.zt == '1'){ 46 | ``` 47 | 48 | 切换为修改的域名: 49 | ``` 50 | type : 'post', 51 | url : '/doupload.php', 52 | data : { 'task':49,'type':1 }, 53 | dataType : 'json', 54 | success:function(msg){ 55 | if(msg.zt == '1'){ 56 | ``` 57 | 58 | 3. 配置说话: 59 | https://pc.woozooo.com/mydisk.php?item=profile 60 | POST 61 | 62 | data : { 63 | action=password 64 | task=titlei 65 | formhash=3db10c77 66 | titlei='Title' # 标题 67 | titlet='' # 说话 68 | } 69 | 70 | resp: 71 | 72 | ``` 73 |
74 |
提示信息
75 |
76 |

恭喜您,设置成功

77 |
78 | ``` 79 | -------------------------------------------------------------------------------- /lanzou/gui/workers/__init__.py: -------------------------------------------------------------------------------- 1 | from lanzou.gui.workers.manager import TaskManager 2 | from lanzou.gui.workers.desc import DescPwdFetcher 3 | from lanzou.gui.workers.folders import GetAllFoldersWorker 4 | from lanzou.gui.workers.login import LoginLuncher, LogoutWorker 5 | from lanzou.gui.workers.more import GetMoreInfoWorker 6 | from lanzou.gui.workers.pwd import SetPwdWorker 7 | from lanzou.gui.workers.recovery import GetRecListsWorker, RecManipulator 8 | from lanzou.gui.workers.refresh import ListRefresher 9 | from lanzou.gui.workers.rename import RenameMkdirWorker 10 | from lanzou.gui.workers.rm import RemoveFilesWorker 11 | from lanzou.gui.workers.share import GetSharedInfo 12 | from lanzou.gui.workers.update import CheckUpdateWorker 13 | 14 | 15 | __all__ = ['TaskManager', 'GetSharedInfo', 'LoginLuncher', 'DescPwdFetcher', 16 | 'ListRefresher', 'GetRecListsWorker', 'RemoveFilesWorker', 17 | 'GetMoreInfoWorker', 'GetAllFoldersWorker', 'RenameMkdirWorker', 18 | 'SetPwdWorker', 'LogoutWorker', 'RecManipulator', 'CheckUpdateWorker'] 19 | -------------------------------------------------------------------------------- /lanzou/gui/workers/desc.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 2 | from lanzou.api import LanZouCloud 3 | 4 | from lanzou.gui.models import DlJob 5 | from lanzou.debug import logger 6 | 7 | 8 | class DescPwdFetcher(QThread): 9 | '''获取描述与提取码 线程''' 10 | desc = pyqtSignal(object) 11 | tasks = pyqtSignal(object) 12 | msg = pyqtSignal(object, object) 13 | 14 | def __init__(self, parent=None): 15 | super(DescPwdFetcher, self).__init__(parent) 16 | self._disk = None 17 | self.infos = None 18 | self.download = False 19 | self.dl_path = "" 20 | self._mutex = QMutex() 21 | self._is_work = False 22 | 23 | def set_disk(self, disk): 24 | self._disk = disk 25 | 26 | def set_values(self, infos, download=False, dl_path=""): 27 | self.infos = infos # 列表的列表 28 | self.download = download # 标识激发下载器 29 | self.dl_path = dl_path 30 | self.start() 31 | 32 | def __del__(self): 33 | self.wait() 34 | 35 | def stop(self): 36 | self._mutex.lock() 37 | self._is_work = False 38 | self._mutex.unlock() 39 | 40 | def run(self): 41 | if not self._is_work: 42 | self._mutex.lock() 43 | self._is_work = True 44 | try: 45 | if not self.infos: 46 | raise UserWarning 47 | _tasks = {} 48 | _infos = [] 49 | for info in self.infos: 50 | if info.id: # disk 运行 51 | if info.is_file: # 文件 52 | res = self._disk.get_share_info(info.id, is_file=True) 53 | else: # 文件夹 54 | res = self._disk.get_share_info(info.id, is_file=False) 55 | if res.code == LanZouCloud.SUCCESS: 56 | info.pwd = res.pwd 57 | info.url = res.url 58 | info.desc = res.desc 59 | elif res.code == LanZouCloud.NETWORK_ERROR: 60 | self.msg.emit("网络错误,请稍后重试!", 6000) 61 | continue 62 | _infos.append(info) # info -> lanzou.gui.models.FileInfos 63 | _tasks[info.url] = DlJob(infos=info, path=self.dl_path, total_file=1) 64 | if self.download: 65 | self.tasks.emit(_tasks) 66 | else: # 激发简介更新 67 | self.desc.emit(_infos) 68 | except TimeoutError: 69 | self.msg.emit("网络超时,请稍后重试!", 6000) 70 | except UserWarning: 71 | pass 72 | except Exception as e: 73 | logger.error(f"GetPwdFetcher error: e={e}") 74 | self._is_work = False 75 | self._mutex.unlock() 76 | else: 77 | self.msg.emit("后台正在运行指令!请稍后重试", 3100) 78 | -------------------------------------------------------------------------------- /lanzou/gui/workers/down.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 2 | 3 | from lanzou.api.utils import is_folder_url, is_file_url 4 | from lanzou.api import why_error 5 | from lanzou.debug import logger 6 | 7 | 8 | class Downloader(QThread): 9 | '''单个文件下载线程''' 10 | finished_ = pyqtSignal(object) 11 | folder_file_failed = pyqtSignal(object, object) 12 | failed = pyqtSignal() 13 | proc = pyqtSignal(str) 14 | update = pyqtSignal() 15 | 16 | def __init__(self, disk, task, callback, parent=None): 17 | super(Downloader, self).__init__(parent) 18 | self._disk = disk 19 | self._task = task 20 | self._callback_thread = callback(task) 21 | 22 | def stop(self): 23 | self.terminate() 24 | 25 | def _callback(self): 26 | """显示进度条的回调函数""" 27 | if not self._callback_thread.isRunning(): 28 | self.update.emit() 29 | self.proc.emit(self._callback_thread.emit_msg) 30 | self._callback_thread.start() 31 | 32 | def _down_failed(self, code, file): 33 | """显示下载失败的回调函数""" 34 | self.folder_file_failed.emit(code, file) 35 | 36 | def __del__(self): 37 | self.wait() 38 | 39 | def run(self): 40 | if not self._task: 41 | logger.error("Download task is empty!") 42 | return None 43 | self._task.run = True 44 | try: 45 | if is_file_url(self._task.url): # 下载文件 46 | res = self._disk.down_file_by_url(self._task.url, self._task, self._callback) 47 | elif is_folder_url(self._task.url): # 下载文件夹 48 | res = self._disk.down_dir_by_url(self._task, self._callback) 49 | else: 50 | raise UserWarning 51 | if res == 0: 52 | self._task.rate = 1000 # 回调线程可能在休眠 53 | self.update.emit() 54 | else: 55 | self._task.info = why_error(res) 56 | logger.debug(f"Download : res={res}") 57 | self.failed.emit() 58 | except TimeoutError: 59 | self._task.info = "网络连接错误!" 60 | logger.error("Download TimeOut") 61 | self.failed.emit() 62 | except Exception as err: 63 | self._task.info = f"未知错误!err={err}" 64 | logger.error(f"Download error: err={err}") 65 | self.failed.emit() 66 | except UserWarning: pass 67 | self._task.run = False 68 | self.finished_.emit(self._task) 69 | -------------------------------------------------------------------------------- /lanzou/gui/workers/folders.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 3 | from lanzou.api import LanZouCloud 4 | from lanzou.debug import logger 5 | 6 | 7 | class GetAllFoldersWorker(QThread): 8 | '''获取所有文件夹name与fid,用于文件移动''' 9 | infos = pyqtSignal(object, object) 10 | msg = pyqtSignal(str, int) 11 | moved = pyqtSignal(bool, bool, bool) 12 | 13 | def __init__(self, parent=None): 14 | super(GetAllFoldersWorker, self).__init__(parent) 15 | self._disk = None 16 | self.org_infos = None 17 | self._mutex = QMutex() 18 | self._is_work = False 19 | self.move_infos = None 20 | 21 | def set_disk(self, disk): 22 | self._disk = disk 23 | 24 | def set_values(self, org_infos): 25 | self.org_infos = org_infos # 对话框标识文件与文件夹 26 | self.move_infos = [] # 清除上次影响 27 | self.start() 28 | 29 | def move_file(self, infos): 30 | '''移动文件至新的文件夹''' 31 | self.move_infos = infos # file_id, folder_id, f_name, type(size) 32 | self.start() 33 | 34 | def __del__(self): 35 | self.wait() 36 | 37 | def stop(self): 38 | self._mutex.lock() 39 | self._is_work = False 40 | self._mutex.unlock() 41 | 42 | def move_file_folder(self, info, no_err:bool, r_files:bool, r_folders:bool): 43 | """移动文件(夹)""" 44 | # no_err 判断是否需要更新 UI 45 | if info.is_file: # 文件 46 | if self._disk.move_file(info.id, info.new_id) == LanZouCloud.SUCCESS: 47 | self.msg.emit(f"{info.name} 移动成功!", 3000) 48 | no_err = True 49 | r_files = True 50 | else: 51 | self.msg.emit(f"移动文件{info.name}失败!", 4000) 52 | else: # 文件夹 53 | if self._disk.move_folder(info.id, info.new_id) == LanZouCloud.SUCCESS: 54 | self.msg.emit(f"{info.name} 移动成功!", 3000) 55 | no_err = True 56 | r_folders = True 57 | else: 58 | self.msg.emit(f"移动文件夹 {info.name} 失败!移动的文件夹中不能包含子文件夹!", 4000) 59 | return no_err, r_files, r_folders 60 | 61 | def run(self): 62 | if not self._is_work: 63 | self._mutex.lock() 64 | self._is_work = True 65 | if self.move_infos: # 移动文件 66 | no_err = False 67 | r_files = False 68 | r_folders = False 69 | for info in self.move_infos: 70 | try: 71 | no_err, r_files, r_folders = self.move_file_folder(info, no_err, r_files, r_folders) 72 | except TimeoutError: 73 | self.msg.emit(f"移动文件(夹) {info.name} 失败,网络超时!请稍后重试", 5000) 74 | except Exception as e: 75 | logger.error(f"GetAllFoldersWorker error: e={e}") 76 | self.msg.emit(f"移动文件(夹) {info.name} 失败,未知错误!", 5000) 77 | if no_err: # 没有错误就更新ui 78 | sleep(2.1) # 等一段时间后才更新文件列表 79 | self.moved.emit(r_files, r_folders, False) 80 | else: # 获取所有文件夹 81 | try: 82 | self.msg.emit("网络请求中,请稍候……", 0) 83 | all_dirs_dict = self._disk.get_move_folders().name_id 84 | self.infos.emit(self.org_infos, all_dirs_dict) 85 | self.msg.emit("", 0) # 删除提示信息 86 | except TimeoutError: 87 | self.msg.emit("网络超时!稍后重试", 6000) 88 | except Exception as e: 89 | logger.error(f"GetAllFoldersWorker error: e={e}") 90 | self._is_work = False 91 | self._mutex.unlock() 92 | else: 93 | self.msg.emit("后台正在运行,请稍后重试!", 3100) 94 | -------------------------------------------------------------------------------- /lanzou/gui/workers/login.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 2 | from lanzou.api import LanZouCloud 3 | from lanzou.debug import logger 4 | 5 | 6 | class LoginLuncher(QThread): 7 | '''登录线程''' 8 | code = pyqtSignal(bool, str, int) 9 | update_cookie = pyqtSignal(object) 10 | update_username = pyqtSignal(object) 11 | 12 | def __init__(self, parent=None): 13 | super(LoginLuncher, self).__init__(parent) 14 | self._disk = None 15 | self.username = "" 16 | self.password = "" 17 | self.cookie = None 18 | 19 | def set_disk(self, disk): 20 | self._disk = disk 21 | 22 | def set_values(self, username, password, cookie=None): 23 | self.username = username 24 | self.password = password 25 | self.cookie = cookie 26 | self.start() 27 | 28 | def run(self): 29 | try: 30 | if self.cookie: 31 | res = self._disk.login_by_cookie(self.cookie) 32 | if res == LanZouCloud.SUCCESS: 33 | if not self.username: 34 | username = self._disk.get_user_name() 35 | if isinstance(username, str): 36 | self.update_username.emit(username) 37 | logger.debug(f"login by Cookie: username={username}") 38 | self.code.emit(True, "通过Cookie登录成功! ≧◉◡◉≦", 5000) 39 | return None 40 | logger.debug(f"login by Cookie err: res={res}") 41 | if (not self.username or not self.password) and not self.cookie: 42 | logger.debug("login err: No UserName、No cookie") 43 | self.code.emit(False, "登录失败: 没有用户或密码", 3000) 44 | else: 45 | res = self._disk.login(self.username, self.password) 46 | if res == LanZouCloud.SUCCESS: 47 | self.code.emit(True, "登录成功! ≧◉◡◉≦", 5000) 48 | _cookie = self._disk.get_cookie() 49 | self.update_cookie.emit(_cookie) 50 | else: 51 | logger.debug(f"login err: res={res}") 52 | self.code.emit(False, "登录失败,可能是用户名或密码错误!", 8000) 53 | self.update_cookie.emit(None) 54 | except TimeoutError: 55 | self.code.emit(False, "网络超时!", 3000) 56 | except Exception as e: 57 | logger.error(f"LoginLuncher error: e={e}") 58 | 59 | 60 | class LogoutWorker(QThread): 61 | '''登出''' 62 | succeeded = pyqtSignal() 63 | msg = pyqtSignal(str, int) 64 | 65 | def __init__(self, parent=None): 66 | super(LogoutWorker, self).__init__(parent) 67 | self._disk = None 68 | self.update_ui = True 69 | self._mutex = QMutex() 70 | self._is_work = False 71 | 72 | def set_disk(self, disk): 73 | self._disk = disk 74 | 75 | def set_values(self, update_ui=True): 76 | self.update_ui = update_ui 77 | self.start() 78 | 79 | def __del__(self): 80 | self.wait() 81 | 82 | def stop(self): 83 | self._mutex.lock() 84 | self._is_work = False 85 | self._mutex.unlock() 86 | 87 | def run(self): 88 | if not self._is_work: 89 | self._mutex.lock() 90 | self._is_work = True 91 | try: 92 | res = self._disk.logout() 93 | if res == LanZouCloud.SUCCESS: 94 | if self.update_ui: 95 | self.succeeded.emit() 96 | self.msg.emit("已经退出登录!", 4000) 97 | else: 98 | self.msg.emit("失败,请重试!", 5000) 99 | except TimeoutError: 100 | self.msg.emit("网络超时,请稍后重试!", 6000) 101 | except Exception as e: 102 | logger.error(f"LogoutWorker error: e={e}") 103 | self._is_work = False 104 | self._mutex.unlock() 105 | else: 106 | self.msg.emit("后台正在运行,请稍后重试!", 3100) 107 | -------------------------------------------------------------------------------- /lanzou/gui/workers/manager.py: -------------------------------------------------------------------------------- 1 | from time import sleep, time 2 | from random import uniform 3 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 4 | 5 | from lanzou.gui.workers.down import Downloader 6 | from lanzou.gui.workers.upload import Uploader 7 | from lanzou.debug import logger 8 | 9 | 10 | def change_size_unit(total): 11 | if total < 1 << 10: 12 | return "{:.2f} B".format(total) 13 | elif total < 1 << 20: 14 | return "{:.2f} KB".format(total / (1 << 10)) 15 | elif total < 1 << 30: 16 | return "{:.2f} MB".format(total / (1 << 20)) 17 | else: 18 | return "{:.2f} GB".format(total / (1 << 30)) 19 | 20 | 21 | def show_progress(file_name, total_size, now_size, speed=0, symbol="█"): 22 | """显示进度条的回调函数""" 23 | percent = now_size / total_size 24 | # 进度条长总度 25 | file_len = len(file_name) 26 | if file_len >= 20: 27 | bar_len = 20 28 | elif file_len >= 10: 29 | bar_len = 30 30 | else: 31 | bar_len = 40 32 | if total_size >= 1048576: 33 | unit = "MB" 34 | piece = 1048576 35 | else: 36 | unit = "KB" 37 | piece = 1024 38 | bar_str = ("" + symbol * round(bar_len * percent) 39 | + "" + symbol * round(bar_len * (1 - percent)) + "") 40 | msg = "\r{:>5.1f}%\t[{}] {:.1f}/{:.1f}{} | {} | {} ".format( 41 | percent * 100, 42 | bar_str, 43 | now_size / piece, 44 | total_size / piece, 45 | unit, 46 | speed, 47 | file_name, 48 | ) 49 | if total_size == now_size: 50 | msg = msg + "| Done!" 51 | return msg 52 | 53 | 54 | class Callback(QThread): 55 | '''回调显示进度''' 56 | def __init__(self, task, parent=None): 57 | super(Callback, self).__init__(parent) 58 | self._task = task 59 | self._mutex = QMutex() 60 | self._stopped = True 61 | self._emit_msg = '' 62 | 63 | @property 64 | def emit_msg(self): 65 | return self._emit_msg 66 | 67 | def run(self): 68 | if self._stopped: 69 | self._mutex.lock() 70 | self._stopped = False 71 | old_size = self._task.now_size 72 | old_rate = int(1000 * old_size / self._task.total_size) 73 | old_time = time() 74 | sleep(uniform(0.5, 2)) 75 | now_size = self._task.now_size 76 | now_rate = int(1000 * now_size / self._task.total_size) 77 | now_time = time() 78 | if old_size != now_size and old_rate != now_rate: 79 | speed = change_size_unit((now_size - old_size) / (now_time - old_time)) + '/s' 80 | self._task.speed = speed 81 | self._task.rate = now_rate 82 | self._emit_msg = show_progress(self._task.name, self._task.total_size, self._task.now_size, speed) 83 | self._stopped = True 84 | self._mutex.unlock() 85 | 86 | 87 | class TaskManager(QThread): 88 | """任务控制器线程,控制后台上传下载""" 89 | mgr_msg = pyqtSignal(str, int) 90 | mgr_finished = pyqtSignal(int) 91 | update = pyqtSignal() 92 | 93 | def __init__(self, thread=3, parent=None): 94 | super(TaskManager, self).__init__(parent) 95 | self._disk = None 96 | self._tasks = {} 97 | self._queues = [] 98 | self._thread = thread 99 | self._count = 0 100 | self._mutex = QMutex() 101 | self._is_work = False 102 | self._old_msg = "" 103 | self._workers = {} 104 | self._allow_big_file = False 105 | 106 | def set_allow_big_file(self, allow_big_file): 107 | self._allow_big_file = allow_big_file 108 | 109 | def set_disk(self, disk): 110 | self._disk = disk 111 | 112 | def set_thread(self, thread): 113 | self._thread = thread 114 | 115 | def stop_task(self, task): 116 | """暂停任务""" 117 | if task.url in self._workers and self._workers[task.url].isRunning(): 118 | logger.debug(f"Stop job: {task.url}") 119 | try: 120 | self._tasks[task.url].pause = True 121 | self._workers[task.url].stop() 122 | self._tasks[task.url].run = False 123 | except Exception as err: 124 | logger.error(f"Stop task: err={err}") 125 | else: 126 | logger.debug(f"Stop job: {task.url} is not running!") 127 | self.update.emit() 128 | 129 | def start_task(self, task): 130 | """开始已暂停任务""" 131 | if task.url not in self._workers: 132 | self.add_task(task) 133 | elif not self._workers[task.url].isRunning(): 134 | logger.debug(f"Start job: {task}") 135 | self._workers[task.url].start() 136 | self._tasks[task.url].run = True 137 | self._tasks[task.url].pause = False 138 | self.update.emit() 139 | self.start() 140 | 141 | def add_task(self, task): 142 | logger.debug(f"TaskMgr add one: added={task.added}, pause={task.pause}") 143 | if task.url not in self._tasks.keys(): 144 | self._tasks[task.url] = task 145 | task.added = False 146 | task.pause = False 147 | task.info = None 148 | self.start() 149 | 150 | def add_tasks(self, tasks: dict): 151 | logger.debug(f"TaskMgr add: tasks={tasks}") 152 | self._tasks.update(tasks) 153 | self.start() 154 | 155 | def del_task(self, task): 156 | logger.debug(f"TaskMgr del: url={task.url}") 157 | if task in self._queues: 158 | self._queues.remove(task) 159 | if task.url in self._tasks: 160 | del self._tasks[task.url] 161 | if task.url in self._workers: 162 | del self._workers[task.url] 163 | 164 | def _task_to_queue(self): 165 | for task in self._tasks.values(): 166 | if not task.added and not task.pause and task not in self._queues: 167 | logger.debug(f"TaskMgr task2queue: url={task.url}") 168 | self._queues.append(task) 169 | task.added = True 170 | 171 | def __del__(self): 172 | self.wait() 173 | 174 | def _ahead_msg(self, msg): 175 | if self._old_msg != msg: 176 | if self._count == 1: 177 | self.mgr_msg.emit(msg, 0) 178 | else: 179 | self.mgr_msg.emit(f"有{self._count}个后台任务正在运行", 0) 180 | self._old_msg = msg 181 | 182 | def _ahead_error(self): 183 | self.update.emit() 184 | 185 | def _ahead_folder_error(self, code, file): 186 | # 需要单独考虑,不在 task中 187 | pass 188 | 189 | def _update_emit(self): 190 | self.update.emit() 191 | 192 | def _add_thread(self, task): 193 | self.update.emit() 194 | logger.debug(f"TaskMgr count: count={self._count}") 195 | self._count -= 1 196 | del self._workers[task.url] 197 | # 发送所有任务完成信号 198 | failed_task_num = 0 199 | for task in self._tasks.values(): 200 | if not task.info: 201 | if task.rate < 1000: 202 | return None 203 | else: 204 | failed_task_num += 1 205 | logger.debug(f"TaskMgr all finished!: failed_task_num={failed_task_num}") 206 | self.mgr_finished.emit(failed_task_num) 207 | 208 | def stop(self): 209 | self._mutex.lock() 210 | self._is_work = False 211 | self._mutex.unlock() 212 | 213 | def run(self): 214 | if not self._is_work: 215 | self._mutex.lock() 216 | self._is_work = True 217 | while True: 218 | self._task_to_queue() 219 | if not self._queues: 220 | break 221 | while self._count >= self._thread: 222 | self.sleep(1) 223 | self._count += 1 224 | task = self._queues.pop() 225 | logger.debug(f"TaskMgr run: url={task.url}") 226 | if task.type == 'dl': 227 | self._workers[task.url] = Downloader(self._disk, task, Callback) 228 | self.mgr_msg.emit(f"准备下载:{task.name}", 0) 229 | else: 230 | self._workers[task.url] = Uploader(self._disk, task, Callback, self._allow_big_file) 231 | self.mgr_msg.emit(f"准备上传:{task.name}", 0) 232 | try: 233 | self._workers[task.url].finished_.connect(self._add_thread) 234 | self._workers[task.url].proc.connect(self._ahead_msg) 235 | self._workers[task.url].update.connect(self._update_emit) 236 | self._workers[task.url].folder_file_failed.connect(self._ahead_folder_error) 237 | self._workers[task.url].failed.connect(self._ahead_error) 238 | self._workers[task.url].start() 239 | except Exception as err: 240 | logger.error(f"TaskMgr Error: err={err}") 241 | self._is_work = False 242 | self._mutex.unlock() 243 | -------------------------------------------------------------------------------- /lanzou/gui/workers/more.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 2 | from lanzou.api import LanZouCloud 3 | 4 | from lanzou.gui.models import Infos 5 | from lanzou.debug import logger 6 | 7 | 8 | class GetMoreInfoWorker(QThread): 9 | '''获取文件直链、文件(夹)提取码描述,用于登录后显示更多信息''' 10 | infos = pyqtSignal(object) 11 | share_url = pyqtSignal(object) 12 | dl_link = pyqtSignal(object) 13 | msg = pyqtSignal(str, int) 14 | 15 | def __init__(self, parent=None): 16 | super(GetMoreInfoWorker, self).__init__(parent) 17 | self._disk = None 18 | self._infos = None 19 | self._url = '' 20 | self._pwd = '' 21 | self._emit_link = False 22 | self._mutex = QMutex() 23 | self._is_work = False 24 | 25 | def set_disk(self, disk): 26 | self._disk = disk 27 | 28 | def set_values(self, infos, emit_link=False): 29 | self._infos = infos 30 | self._emit_link = emit_link 31 | self.start() 32 | 33 | def get_dl_link(self, url, pwd): 34 | self._url = url 35 | self._pwd = pwd 36 | self.start() 37 | 38 | def __del__(self): 39 | self.wait() 40 | 41 | def stop(self): 42 | self._mutex.lock() 43 | self._is_work = False 44 | self._mutex.unlock() 45 | 46 | def run(self): 47 | # infos: ID/None,文件名,大小,日期,下载次数(dl_count),提取码(pwd),描述(desc),|链接(share-url) 48 | if not self._is_work and self._infos: 49 | self._mutex.lock() 50 | self._is_work = True 51 | try: 52 | if not self._url: # 获取普通信息 53 | if isinstance(self._infos, Infos): 54 | if self._infos.id: # 从 disk 运行 55 | self.msg.emit("网络请求中,请稍候……", 0) 56 | _info = self._disk.get_share_info(self._infos.id, is_file=self._infos.is_file) 57 | self._infos.desc = _info.desc 58 | self._infos.pwd = _info.pwd 59 | self._infos.url = _info.url 60 | if self._emit_link: 61 | self.share_url.emit(self._infos) 62 | else: 63 | self.infos.emit(self._infos) 64 | self.msg.emit("", 0) # 删除提示信息 65 | else: # 获取下载直链 66 | res = self._disk.get_file_info_by_url(self._url, self._pwd) 67 | if res.code == LanZouCloud.SUCCESS: 68 | self.dl_link.emit("{}".format(res.durl or "无")) # 下载直链 69 | elif res.code == LanZouCloud.NETWORK_ERROR: 70 | self.dl_link.emit("网络错误!获取失败") # 下载直链 71 | else: 72 | self.dl_link.emit("其它错误!") # 下载直链 73 | except TimeoutError: 74 | self.msg.emit("网络超时!稍后重试", 6000) 75 | except Exception as e: 76 | logger.error(f"GetMoreInfoWorker error: e={e}") 77 | self._is_work = False 78 | self._url = '' 79 | self._pwd = '' 80 | self._mutex.unlock() 81 | else: 82 | self.msg.emit("后台正在运行,请稍后重试!", 3100) 83 | -------------------------------------------------------------------------------- /lanzou/gui/workers/pwd.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 2 | from lanzou.api import LanZouCloud 3 | from lanzou.debug import logger 4 | 5 | 6 | class SetPwdWorker(QThread): 7 | '''设置文件(夹)提取码 线程''' 8 | msg = pyqtSignal(str, int) 9 | update = pyqtSignal(object, object, object, object) 10 | 11 | def __init__(self, parent=None): 12 | super(SetPwdWorker, self).__init__(parent) 13 | self._disk = None 14 | self.infos = [] 15 | self._work_id = -1 16 | self._mutex = QMutex() 17 | self._is_work = False 18 | 19 | def set_disk(self, disk): 20 | self._disk = disk 21 | 22 | def set_values(self, infos, work_id): 23 | self.infos = infos 24 | self._work_id = work_id 25 | self.start() 26 | 27 | def __del__(self): 28 | self.wait() 29 | 30 | def stop(self): 31 | self._mutex.lock() 32 | self._is_work = False 33 | self._mutex.unlock() 34 | 35 | def run(self): 36 | if not self._is_work: 37 | self._mutex.lock() 38 | self._is_work = True 39 | try: 40 | has_file = False 41 | has_folder = False 42 | failed = False 43 | failed_code = "" 44 | for infos in self.infos: 45 | if infos.is_file: # 文件 46 | has_file = True 47 | new_pwd = infos.new_pwd 48 | if 2 > len(new_pwd) >= 1 or len(new_pwd) > 6: 49 | self.msg.emit("文件提取码为2-6位字符,关闭请留空!", 4000) 50 | raise UserWarning 51 | else: # 文件夹 52 | has_folder = True 53 | new_pwd = infos.new_pwd 54 | if 2 > len(new_pwd) >= 1 or len(new_pwd) > 12: 55 | self.msg.emit("文件夹提取码为0-12位字符,关闭请留空!", 4000) 56 | raise UserWarning 57 | res = self._disk.set_passwd(infos.id, infos.new_pwd, infos.is_file) 58 | if res != LanZouCloud.SUCCESS: 59 | failed_code = failed_code + str(res) 60 | failed = True 61 | if failed: 62 | self.msg.emit(f"❌部分提取码变更失败:{failed_code},请勿使用特殊符号!", 4000) 63 | else: 64 | self.msg.emit("✅提取码变更成功!♬", 3000) 65 | 66 | self.update.emit(self._work_id, has_file, has_folder, False) 67 | except TimeoutError: 68 | self.msg.emit("网络超时,请稍后重试!", 6000) 69 | except UserWarning: 70 | pass 71 | except Exception as e: 72 | logger.error(f"SetPwdWorker error: e={e}") 73 | self._is_work = False 74 | self._mutex.unlock() 75 | else: 76 | self.msg.emit("后台正在运行,请稍后重试!", 3100) 77 | -------------------------------------------------------------------------------- /lanzou/gui/workers/recovery.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 3 | from lanzou.api import LanZouCloud 4 | 5 | from lanzou.api.types import RecFolder, RecFile 6 | from lanzou.debug import logger 7 | 8 | 9 | class GetRecListsWorker(QThread): 10 | '''获取回收站列表''' 11 | folders = pyqtSignal(object) 12 | infos = pyqtSignal(object, object) 13 | msg = pyqtSignal(str, int) 14 | 15 | def __init__(self, parent=None): 16 | super(GetRecListsWorker, self).__init__(parent) 17 | self._disk = None 18 | self._mutex = QMutex() 19 | self._is_work = False 20 | self._folder_id = None 21 | 22 | def set_disk(self, disk): 23 | self._disk = disk 24 | 25 | def set_values(self, fid): 26 | # 用于获取回收站指定文件夹内文件信息 27 | self._folder_id = fid 28 | self.start() 29 | 30 | def __del__(self): 31 | self.wait() 32 | 33 | def stop(self): 34 | self._mutex.lock() 35 | self._is_work = False 36 | self._mutex.unlock() 37 | 38 | def run(self): 39 | if not self._is_work: 40 | self._mutex.lock() 41 | self._is_work = True 42 | try: 43 | if self._folder_id: 44 | file_lists = self._disk.get_rec_file_list(folder_id=self._folder_id) 45 | self._folder_id = None 46 | self.folders.emit(file_lists) 47 | raise UserWarning 48 | dir_lists = self._disk.get_rec_dir_list() 49 | file_lists = self._disk.get_rec_file_list(folder_id=-1) 50 | self.infos.emit(dir_lists, file_lists) 51 | self.msg.emit("刷新列表成功!", 2000) 52 | except TimeoutError: 53 | self.msg.emit("网络超时,请稍后重试!", 6000) 54 | except UserWarning: 55 | pass 56 | except Exception as e: 57 | logger.error(f"GetRecListsWorker error: e={e}") 58 | self._is_work = False 59 | self._mutex.unlock() 60 | else: 61 | self.msg.emit("后台正在运行,请稍后重试!", 3100) 62 | 63 | 64 | class RecManipulator(QThread): 65 | '''操作回收站''' 66 | msg = pyqtSignal(str, int) 67 | succeeded = pyqtSignal() 68 | 69 | def __init__(self, parent=None): 70 | super(RecManipulator, self).__init__(parent) 71 | self._disk = None 72 | self._mutex = QMutex() 73 | self._is_work = False 74 | self._action = None 75 | self._folders = [] 76 | self._files= [] 77 | 78 | def set_disk(self, disk): 79 | self._disk = disk 80 | 81 | def set_values(self, infos, action): 82 | # 操作回收站选定行 83 | self._action = None 84 | self._folders = [] 85 | self._files= [] 86 | for item in infos: 87 | if isinstance(item, RecFile): 88 | self._files.append(item.id) 89 | elif isinstance(item, RecFolder): 90 | self._folders.append(item.id) 91 | self._action = action 92 | self.start() 93 | 94 | def __del__(self): 95 | self.wait() 96 | 97 | def stop(self): 98 | self._mutex.lock() 99 | self._is_work = False 100 | self._mutex.unlock() 101 | 102 | def run(self): 103 | if not self._is_work: 104 | self._mutex.lock() 105 | self._is_work = True 106 | try: 107 | res = None 108 | if self._action == "recovery": 109 | if self._files or self._folders: 110 | res = self._disk.recovery_multi(self._files, self._folders) 111 | if res == LanZouCloud.SUCCESS: 112 | self.msg.emit("选定文件(夹)恢复成功,即将刷新列表", 2500) 113 | elif self._action == "delete": 114 | if self._files or self._folders: 115 | if self._files or self._folders: 116 | res = self._disk.delete_rec_multi(self._files, self._folders) 117 | if res == LanZouCloud.SUCCESS: 118 | self.msg.emit("选定文件(夹)彻底删除成功,即将刷新列表", 2500) 119 | elif self._action == "clean": 120 | res = self._disk.clean_rec() 121 | if res == LanZouCloud.SUCCESS: 122 | self.msg.emit("清空回收站成功,即将刷新列表", 2500) 123 | elif self._action == "recovery_all": 124 | res = self._disk.recovery_all() 125 | if res == LanZouCloud.SUCCESS: 126 | self.msg.emit("文件(夹)全部还原成功,即将刷新列表", 2500) 127 | if isinstance(res, int): 128 | if res == LanZouCloud.FAILED: 129 | self.msg.emit("失败,请重试!", 4500) 130 | elif res == LanZouCloud.NETWORK_ERROR: 131 | self.msg.emit("网络错误,请稍后重试!", 4500) 132 | else: 133 | sleep(2.6) 134 | self.succeeded.emit() 135 | except TimeoutError: 136 | self.msg.emit("网络超时,请稍后重试!", 6000) 137 | except Exception as e: 138 | logger.error(f"RecManipulator error: e={e}") 139 | self._is_work = False 140 | self._action = None 141 | self._folders = [] 142 | self._files= [] 143 | self._mutex.unlock() 144 | else: 145 | self.msg.emit("后台正在运行,请稍后重试!", 3100) 146 | -------------------------------------------------------------------------------- /lanzou/gui/workers/refresh.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 2 | from lanzou.debug import logger 3 | 4 | 5 | class ListRefresher(QThread): 6 | '''跟新目录文件与文件夹列表线程''' 7 | infos = pyqtSignal(object) 8 | err_msg = pyqtSignal(str, int) 9 | 10 | def __init__(self, parent=None): 11 | super(ListRefresher, self).__init__(parent) 12 | self._disk = None 13 | self._fid = -1 14 | self.r_files = True 15 | self.r_folders = True 16 | self.r_path = True 17 | self._mutex = QMutex() 18 | self._is_work = False 19 | 20 | def set_disk(self, disk): 21 | self._disk = disk 22 | 23 | def set_values(self, fid, r_files=True, r_folders=True, r_path=True): 24 | if not self._is_work: 25 | self._fid = fid 26 | self.r_files = r_files 27 | self.r_folders = r_folders 28 | self.r_path = r_path 29 | self.start() 30 | else: 31 | self.err_msg.emit("正在更新目录,请稍后再试!", 3100) 32 | 33 | def __del__(self): 34 | self.wait() 35 | 36 | def stop(self): 37 | self._mutex.lock() 38 | self._is_work = False 39 | self._mutex.unlock() 40 | 41 | def goto_root_dir(self): 42 | self._fid = -1 43 | self.run() 44 | 45 | def run(self): 46 | if not self._is_work: 47 | self._mutex.lock() 48 | self._is_work = True 49 | emit_infos = {} 50 | # 传递更新内容 51 | emit_infos['r'] = {'fid': self._fid, 'files': self.r_files, 'folders': self.r_folders, 'path': self.r_path} 52 | try: 53 | if self.r_files: 54 | # [i.id, i.name, i.size, i.time, i.downs, i.has_pwd, i.has_des] 55 | info = {i.name: i for i in self._disk.get_file_list(self._fid)} 56 | emit_infos['file_list'] = {key: info.get(key) for key in sorted(info.keys())} # {name-File} 57 | if self.r_folders: 58 | folders, full_path = self._disk.get_dir_list(self._fid) 59 | if not full_path and not folders and self._fid != -1: 60 | self.err_msg.emit(f"文件夹id {self._fid} 不存在,将切换到更目录", 2900) 61 | self._is_work = False 62 | self._mutex.unlock() 63 | return self.goto_root_dir() 64 | info = {i.name: i for i in folders} 65 | emit_infos['folder_list'] = {key: info.get(key) for key in sorted(info.keys())} # {name-Folder} 66 | emit_infos['path_list'] = full_path 67 | except TimeoutError: 68 | self.err_msg.emit("网络超时,无法更新目录,稍后再试!", 7000) 69 | except Exception as e: 70 | self.err_msg.emit("未知错误,无法更新目录,稍后再试!", 7000) 71 | logger.error(f"ListRefresher error: e={e}") 72 | else: 73 | self.infos.emit(emit_infos) 74 | self._is_work = False 75 | self._mutex.unlock() 76 | -------------------------------------------------------------------------------- /lanzou/gui/workers/rename.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 3 | from lanzou.api import LanZouCloud 4 | from lanzou.debug import logger 5 | 6 | 7 | class RenameMkdirWorker(QThread): 8 | """重命名、修改简介与新建文件夹 线程""" 9 | # infos = pyqtSignal(object, object) 10 | msg = pyqtSignal(str, int) 11 | update = pyqtSignal(object, object, object, object) 12 | 13 | def __init__(self, parent=None): 14 | super(RenameMkdirWorker, self).__init__(parent) 15 | self._disk = None 16 | self._work_id = -1 17 | self._folder_list = None 18 | self.infos = None 19 | self._mutex = QMutex() 20 | self._is_work = False 21 | 22 | def set_disk(self, disk): 23 | self._disk = disk 24 | 25 | def set_values(self, infos, work_id, folder_list): 26 | self.infos = infos # 对话框标识文件与文件夹 27 | self._work_id = work_id 28 | self._folder_list = folder_list 29 | self.start() 30 | 31 | def __del__(self): 32 | self.wait() 33 | 34 | def stop(self): 35 | self._mutex.lock() 36 | self._is_work = False 37 | self._mutex.unlock() 38 | 39 | def run(self): 40 | if not self._is_work: 41 | self._mutex.lock() 42 | self._is_work = True 43 | 44 | action = self.infos[0] 45 | try: 46 | if action == 'new': # 新建文件夹 47 | new_name = self.infos[1] 48 | new_des = self.infos[2] 49 | if new_name in self._folder_list.keys(): 50 | self.msg.emit(f"文件夹已存在:{new_name}", 7000) 51 | else: 52 | res = self._disk.mkdir(self._work_id, new_name, new_des) 53 | if res == LanZouCloud.MKDIR_ERROR: 54 | self.msg.emit(f"创建文件夹失败:{new_name}", 7000) 55 | else: 56 | sleep(1.5) # 暂停一下,否则无法获取新建的文件夹 57 | self.update.emit(self._work_id, False, True, False) # 此处仅更新文件夹,并显示 58 | self.msg.emit(f"成功创建文件夹:{new_name}", 4000) 59 | else: # 重命名、修改简介 60 | has_file = False 61 | has_folder = False 62 | failed = False 63 | for info in self.infos[1]: 64 | if info.is_file: # 修改文件描述 65 | res = self._disk.set_desc(info.id, info.new_des, is_file=info.is_file) 66 | if res == LanZouCloud.SUCCESS: 67 | has_file = True 68 | else: 69 | failed = True 70 | else: # 修改文件夹,action == "folder" 71 | name = info.new_name or info.nmae 72 | res = self._disk._set_dir_info(info.id, str(name), str(info.new_des)) 73 | if res == LanZouCloud.SUCCESS: 74 | has_folder = True 75 | else: 76 | failed = True 77 | self.update.emit(self._work_id, has_file, has_folder, False) 78 | if failed: 79 | self.msg.emit("有发生错误!", 6000) 80 | else: 81 | self.msg.emit("修改成功!", 4000) 82 | except TimeoutError: 83 | self.msg.emit("网络超时,请稍后重试!", 6000) 84 | except Exception as e: 85 | logger.error(f"RenameMikdirWorker error: e={e}") 86 | 87 | self._is_work = False 88 | self._mutex.unlock() 89 | else: 90 | self.msg.emit("后台正在运行,请稍后重试!", 3100) 91 | -------------------------------------------------------------------------------- /lanzou/gui/workers/rm.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 2 | from lanzou.debug import logger 3 | 4 | 5 | class RemoveFilesWorker(QThread): 6 | '''删除文件(夹)线程''' 7 | msg = pyqtSignal(object, object) 8 | finished = pyqtSignal() 9 | 10 | def __init__(self, parent=None): 11 | super(RemoveFilesWorker, self).__init__(parent) 12 | self._disk = None 13 | self.infos = None 14 | self._mutex = QMutex() 15 | self._is_work = False 16 | 17 | def set_disk(self, disk): 18 | self._disk = disk 19 | 20 | def set_values(self, infos): 21 | self.infos = infos 22 | self.start() 23 | 24 | def __del__(self): 25 | self.wait() 26 | 27 | def stop(self): 28 | self._mutex.lock() 29 | self._is_work = False 30 | self._mutex.unlock() 31 | 32 | def run(self): 33 | if not self._is_work: 34 | self._mutex.lock() 35 | self._is_work = True 36 | if not self.infos: 37 | self._is_work = False 38 | self._mutex.unlock() 39 | return 40 | for i in self.infos: 41 | try: 42 | self._disk.delete(i['fid'], i['is_file']) 43 | except TimeoutError: 44 | self.msg.emit(f"删除 {i['name']} 因网络超时失败!", 3000) 45 | except Exception as e: 46 | logger.error(f"RemoveFileWorker error: e={e}") 47 | self.finished.emit() 48 | self._is_work = False 49 | self._mutex.unlock() 50 | else: 51 | self.msg.emit("后台正在运行删除指令!", 3100) 52 | -------------------------------------------------------------------------------- /lanzou/gui/workers/share.py: -------------------------------------------------------------------------------- 1 | import re 2 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 3 | 4 | from lanzou.api.utils import is_folder_url, is_file_url 5 | from lanzou.api import LanZouCloud 6 | from lanzou.debug import logger 7 | 8 | 9 | class GetSharedInfo(QThread): 10 | '''提取界面获取分享链接信息''' 11 | infos = pyqtSignal(object) 12 | msg = pyqtSignal(str, int) 13 | update = pyqtSignal() 14 | clean = pyqtSignal() 15 | 16 | def __init__(self, parent=None): 17 | super(GetSharedInfo, self).__init__(parent) 18 | self._disk = None 19 | self.share_url = "" 20 | self.pwd = "" 21 | self.is_file = "" 22 | self.is_folder = "" 23 | self._mutex = QMutex() 24 | self._is_work = False 25 | self._pat = r"(https?://(\w[-\w]*\.)?lanzou[a-z].com/[a-z]?[-/a-zA-Z0-9]+)[^a-zA-Z0-9]*([a-zA-Z0-9]+\w+)?" 26 | 27 | def set_disk(self, disk): 28 | self._disk = disk 29 | 30 | def set_values(self, text, pwd_input=""): 31 | '''获取分享链接信息''' 32 | text = text.strip() 33 | pwd_input = pwd_input.strip() 34 | if not text: 35 | self.update.emit() 36 | return None 37 | for share_url, _, pwd in re.findall(self._pat, text): 38 | if is_file_url(share_url): # 文件链接 39 | is_file = True 40 | is_folder = False 41 | self.msg.emit("正在获取文件链接信息……", 20000) 42 | elif is_folder_url(share_url): # 文件夹链接 43 | is_folder = True 44 | is_file = False 45 | self.msg.emit("正在获取文件夹链接信息,可能需要几秒钟,请稍候……", 500000) 46 | else: 47 | self.msg.emit(f"{share_url} 为非法链接!", 0) 48 | self.update.emit() 49 | return None 50 | self.clean.emit() # 清理旧的显示信息 51 | self.share_url = share_url 52 | if pwd_input: 53 | self.pwd = pwd_input 54 | elif pwd: 55 | self.pwd = pwd 56 | else: # 一个或两个汉字的提取码 57 | pwd_ = text.split(' ')[-1].split(':')[-1].split(':')[-1] 58 | self.pwd = pwd_ if 1<= len(pwd_) <= 2 else '' 59 | self.is_file = is_file 60 | self.is_folder = is_folder 61 | self.start() 62 | break 63 | 64 | def __del__(self): 65 | self.wait() 66 | 67 | def stop(self): # 用于手动停止 68 | self._mutex.lock() 69 | self._is_work = False 70 | self._mutex.unlock() 71 | 72 | def emit_msg(self, infos): 73 | '''根据查询信息发送状态信号''' 74 | show_time = 2999 # 提示显示时间,单位 ms 75 | if infos.code == LanZouCloud.FILE_CANCELLED: 76 | self.msg.emit("文件不存在,或已删除!", show_time) 77 | elif infos.code == LanZouCloud.URL_INVALID: 78 | self.msg.emit("链接非法!", show_time) 79 | elif infos.code == LanZouCloud.PASSWORD_ERROR: 80 | self.msg.emit(f"提取码 [{self.pwd}] 错误!", show_time) 81 | elif infos.code == LanZouCloud.LACK_PASSWORD: 82 | self.msg.emit("请在链接后面跟上提取码,空格分割!", show_time) 83 | elif infos.code == LanZouCloud.NETWORK_ERROR: 84 | self.msg.emit("网络错误!", show_time) 85 | elif infos.code == LanZouCloud.SUCCESS: 86 | self.msg.emit("提取成功!", show_time) 87 | else: 88 | self.msg.emit(f"未知错误 code={infos.code}!", show_time * 4) 89 | 90 | def run(self): 91 | if not self._is_work: 92 | self._mutex.lock() 93 | self._is_work = True 94 | try: 95 | if self.is_file: # 链接为文件 96 | _infos = self._disk.get_share_info_by_url(self.share_url, self.pwd) 97 | self.emit_msg(_infos) 98 | self.infos.emit(_infos) 99 | elif self.is_folder: # 链接为文件夹 100 | _infos = self._disk.get_folder_info_by_url(self.share_url, self.pwd) 101 | self.emit_msg(_infos) 102 | self.infos.emit(_infos) 103 | else: 104 | logger.error(f"GetShareInfo error: Not a file or folder!") 105 | except TimeoutError: 106 | self.msg.emit("font color='red'>网络超时!请稍后重试", 5000) 107 | except Exception as e: 108 | self.msg.emit(f"font color='red'>未知错误:{e}", 5000) 109 | logger.error(f"GetShareInfo error: e={e}") 110 | self._is_work = False 111 | self.update.emit() 112 | self._mutex.unlock() 113 | else: 114 | self.msg.emit("后台正在运行,稍后重试!", 4000) 115 | -------------------------------------------------------------------------------- /lanzou/gui/workers/update.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import re 3 | import requests 4 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 5 | from lanzou.debug import logger 6 | 7 | 8 | class CheckUpdateWorker(QThread): 9 | '''检测软件更新''' 10 | infos = pyqtSignal(object, object) 11 | bg_update_infos = pyqtSignal(object, object) 12 | 13 | def __init__(self, parent=None): 14 | super(CheckUpdateWorker, self).__init__(parent) 15 | self._ver = '' 16 | self._manual = False 17 | self._mutex = QMutex() 18 | self._is_work = False 19 | self._folder_id = None 20 | self._api = 'https://api.github.com/repos/rachpt/lanzou-gui/releases/latest' 21 | self._api_mirror = 'https://gitee.com/api/v5/repos/rachpt/lanzou-gui/releases/latest' 22 | 23 | def set_values(self, ver: str, manual: bool=False): 24 | # 检查更新 25 | self._ver = ver 26 | self._manual = manual 27 | self.start() 28 | 29 | def __del__(self): 30 | self.wait() 31 | 32 | def stop(self): 33 | self._mutex.lock() 34 | self._is_work = False 35 | self._mutex.unlock() 36 | 37 | def run(self): 38 | if not self._is_work: 39 | self._mutex.lock() 40 | self._is_work = True 41 | resp = None 42 | try: 43 | resp = requests.get(self._api).json() 44 | except (requests.RequestException, TimeoutError, requests.exceptions.ConnectionError): 45 | logger.debug("chcek update from github error") 46 | try: resp = requests.get(self._api_mirror).json() 47 | except: 48 | logger.debug("chcek update from gitee error") 49 | except Exception as e: 50 | logger.error(f"CheckUpdateWorker error: e={e}") 51 | if resp: 52 | try: 53 | tag_name, msg = resp['tag_name'], resp['body'] 54 | ver = self._ver.replace('v', '').split('-')[0].split('.') 55 | ver2 = tag_name.replace('v', '').split('-')[0].split('.') 56 | local_version = int(ver[0]) * 100 + int(ver[1]) * 10 + int(ver[2]) 57 | remote_version = int(ver2[0]) * 100 + int(ver2[1]) * 10 + int(ver2[2]) 58 | if remote_version > local_version: 59 | urls = re.findall(r'https?://[-\.a-zA-Z0-9/_#?&%@]+', msg) 60 | for url in urls: 61 | new_url = f'{url}' 62 | msg = msg.replace(url, new_url) 63 | msg = msg.replace('\n', '
') 64 | self.infos.emit(tag_name, msg) 65 | if not self._manual: # 打开软件时检测更新 66 | self.bg_update_infos.emit(tag_name, msg) 67 | elif self._manual: 68 | self.infos.emit("0", "目前还没有发布新版本!") 69 | except AttributeError: 70 | if self._manual: 71 | self.infos.emit("v0.0.0", "检查更新时发生异常,请重试!") 72 | except Exception as e: 73 | logger.error(f"Check Update Version error: e={e}") 74 | else: 75 | if self._manual: 76 | self.infos.emit("v0.0.0", f"检查更新时 api.github.comgitee.com 拒绝连接,请稍后重试!") 77 | self._manual = False 78 | self._is_work = False 79 | self._mutex.unlock() 80 | else: 81 | if self._manual: 82 | self.infos.emit("v0.0.0", "后台正在运行,请稍等!") 83 | -------------------------------------------------------------------------------- /lanzou/gui/workers/upload.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PyQt6.QtCore import QThread, pyqtSignal, QMutex 3 | from lanzou.api import LanZouCloud 4 | from lanzou.debug import logger 5 | 6 | 7 | class Uploader(QThread): 8 | '''单个文件上传线程''' 9 | finished_ = pyqtSignal(object) 10 | folder_file_failed = pyqtSignal(object, object) 11 | failed = pyqtSignal() 12 | proc = pyqtSignal(str) 13 | update = pyqtSignal() 14 | 15 | def __init__(self, disk, task, callback, allow_big_file=False, parent=None): 16 | super(Uploader, self).__init__(parent) 17 | self._disk = disk 18 | self._task = task 19 | self._allow_big_file = allow_big_file 20 | self._callback_thread = callback(task) 21 | 22 | def stop(self): 23 | self.terminate() 24 | 25 | def _callback(self): 26 | """显示进度条的回调函数""" 27 | if not self._callback_thread.isRunning(): 28 | self.update.emit() 29 | self.proc.emit(self._callback_thread.emit_msg) 30 | self._callback_thread.start() 31 | 32 | def _down_failed(self, code, file): 33 | """显示下载失败的回调函数""" 34 | self.folder_file_failed.emit(code, file) 35 | 36 | def __del__(self): 37 | self.wait() 38 | 39 | def run(self): 40 | if not self._task: 41 | logger.error("Upload task is empty!") 42 | return None 43 | self._task.run = True 44 | try: 45 | if os.path.isdir(self._task.url): 46 | code, fid, isfile = self._disk.upload_dir(self._task, self._callback, self._allow_big_file) 47 | else: 48 | code, fid, isfile = self._disk.upload_file(self._task, self._task.url, self._task.fid, 49 | self._callback, self._allow_big_file) 50 | except TimeoutError: 51 | self._task.info = LanZouCloud.NETWORK_ERROR 52 | self.update.emit() 53 | except Exception as err: 54 | logger.error(f"Upload error: err={err}") 55 | self._task.info = err 56 | self.update.emit() 57 | else: 58 | if code == LanZouCloud.SUCCESS: 59 | if self._task.pwd: 60 | self._disk.set_passwd(fid, self._task.pwd, is_file=isfile) 61 | if self._task.desc: 62 | self._disk.set_desc(fid, self._task.desc, is_file=isfile) 63 | self._task.rate = 1000 # 回调线程可能在休眠 64 | else: 65 | self.failed.emit() 66 | self._task.run = False 67 | self.finished_.emit(self._task) 68 | -------------------------------------------------------------------------------- /lanzou/login_assister.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt5.QtCore import QUrl, pyqtSignal 3 | from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout 4 | from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile 5 | 6 | 7 | class MyWebEngineView(QWebEngineView): 8 | def __init__(self, user, pwd, *args, **kwargs): 9 | super(MyWebEngineView, self).__init__(*args, **kwargs) 10 | self.cookies = {} 11 | self._user = user 12 | self._pwd = pwd 13 | # 绑定cookie被添加的信号槽 14 | QWebEngineProfile.defaultProfile().cookieStore().cookieAdded.connect(self.onCookieAdd) 15 | self.loadFinished.connect(self._on_load_finished) 16 | self.urlChanged.connect(self._on_load_finished) 17 | 18 | def _on_load_finished(self): 19 | self.page().toHtml(self.Callable) 20 | 21 | def Callable(self, html_str): 22 | try: 23 | self.html = html_str 24 | js = """var l_name=document.getElementsByName('username'); 25 | if (l_name.length > 0) {{ 26 | l_name[0].value = '{}'; 27 | }}; 28 | var l_pwd=document.getElementsByName('password'); 29 | if (l_pwd.length > 0) {{ 30 | l_pwd[0].value = '{}'; 31 | }};""".format(self._user, self._pwd) 32 | self.page().runJavaScript(js) 33 | except: pass 34 | # except Exception as e: 35 | # print("Err:", e) 36 | 37 | def onCookieAdd(self, cookie): 38 | name = cookie.name().data().decode('utf-8') 39 | value = cookie.value().data().decode('utf-8') 40 | self.cookies[name] = value 41 | 42 | def get_cookie(self): 43 | cookie_dict = {} 44 | for key, value in self.cookies.items(): 45 | if key in ('ylogin', 'phpdisk_info'): 46 | cookie_dict[key] = value 47 | return cookie_dict 48 | 49 | 50 | class LoginWindow(QDialog): 51 | cookie = pyqtSignal(object) 52 | 53 | def __init__(self, user=None, pwd=None, gui=False): 54 | super().__init__() 55 | self._user = user 56 | self._pwd = pwd 57 | self._base_url = 'https://pc.woozooo.com/' 58 | self._gui = gui 59 | self.setup() 60 | 61 | def setup(self): 62 | self.setWindowTitle('滑动滑块,完成登录') 63 | url = self._base_url + 'account.php?action=login&ref=/mydisk.php' 64 | QWebEngineProfile.defaultProfile().cookieStore().deleteAllCookies() 65 | self.web = MyWebEngineView(self._user, self._pwd) 66 | self.web.urlChanged.connect(self.get_cookie) 67 | self.web.resize(480, 400) 68 | self.web.load(QUrl(url)) 69 | self.box = QVBoxLayout(self) 70 | self.box.addWidget(self.web) 71 | 72 | def get_cookie(self): 73 | home_url = self._base_url + 'mydisk.php' 74 | if self.web.url().toString() == home_url: 75 | cookie = self.web.get_cookie() 76 | if cookie: 77 | if self._gui: 78 | try: print(";".join([f'{k}={v}' for k, v in cookie.items()]), end='') 79 | except: pass 80 | else: 81 | self.cookie.emit(cookie) 82 | self.reject() 83 | 84 | 85 | if __name__ == "__main__": 86 | if len(sys.argv) == 3: 87 | username, password = sys.argv[1], sys.argv[2] 88 | app = QApplication(sys.argv) 89 | form = LoginWindow(username, password, gui=True) 90 | form.show() 91 | sys.exit(app.exec()) 92 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from PyQt6.QtWidgets import QApplication 5 | 6 | from lanzou.gui.gui import MainWindow, get_lanzou_logo 7 | 8 | 9 | if __name__ == "__main__": 10 | app = QApplication(sys.argv) 11 | app.setWindowIcon(get_lanzou_logo()) 12 | form = MainWindow() 13 | form.show() 14 | form.call_login_launcher() 15 | sys.exit(app.exec()) 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt6 2 | # PyQtWebEngine # 可能需要装 spyder 解决 PyQtWebEngine 版本冲突 3 | requests 4 | requests_toolbelt 5 | browser_cookie3 6 | 7 | # browser_cookie3 requires 8 | # pyaes 9 | # pbkdf2 10 | # keyring 11 | # lz4 12 | # pycryptodome 13 | # SecretStorage 14 | # linux: pip install secretstorage dbus-python -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | from lanzou.gui import version 5 | 6 | setup( 7 | name='lanzou-gui', 8 | version=version, 9 | description='Lanzou Cloud GUI', 10 | license="MIT", 11 | author='rachpt', 12 | author_email='rachpt@126.com', 13 | packages=find_packages(), 14 | package_data={ 15 | '': [] 16 | }, 17 | python_requires=">=3.6", 18 | url='https://github.com/rachpt/lanzou-gui', 19 | keywords=['lanzou', 'lanzoucloud', 'gui', 'application', 'PyQt6', 'Python 3'], 20 | classifiers=( 21 | 'Development Status :: 3 - Alpha', 22 | 'Programming Language :: Python :: 3.8', 23 | 'Programming Language :: Python :: 3.7', 24 | 'Programming Language :: Python :: 3.6', 25 | 'Programming Language :: Python :: 3 :: Only', 26 | 'Environment :: X11 Applications :: Qt', 27 | 'Topic :: Internet', 28 | 'Operating System :: Microsoft :: Windows', 29 | 'Operating System :: POSIX :: Linux', 30 | 'Operating System :: MacOS', 31 | ), 32 | install_requires=[ 33 | 'PyQt6', 34 | 'PyQtWebEngine', 35 | 'requests', 36 | 'requests_toolbelt', 37 | 'browser_cookie3', 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /version_info.txt: -------------------------------------------------------------------------------- 1 | # version infos 2 | 3 | VSVersionInfo( 4 | ffi=FixedFileInfo( 5 | filevers=(0, 5, 1, 0), 6 | prodvers=(0, 5, 1, 0), 7 | mask=0x3f, 8 | flags=0x0, 9 | OS=0x4, 10 | fileType=0x1, 11 | subtype=0x0, 12 | date=(0, 0) 13 | ), 14 | kids=[ 15 | StringFileInfo([ 16 | StringTable( 17 | u'040904B0', 18 | [StringStruct(u'CompanyName', u'rachpt.cn'), 19 | StringStruct(u'FileDescription', u'蓝奏云客户端'), 20 | StringStruct(u'FileVersion', u'0.5.1'), 21 | StringStruct(u'InternalName', u'lanzou-gui'), 22 | StringStruct(u'LegalCopyright', u'Copyright (c) rachpt'), 23 | StringStruct(u'OriginalFilename', u'lanzou-gui_win64_v0.5.1.exe'), 24 | StringStruct(u'ProductName', u'lanzou-gui_win64'), 25 | StringStruct(u'ProductVersion', u'0.5.1 (r220605)')]) 26 | ]), 27 | VarFileInfo([VarStruct(u'Translation', [2052, 1033])]) 28 | ] 29 | ) 30 | --------------------------------------------------------------------------------