├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── custom.md └── workflows │ └── python-app.yml ├── .idea └── vcs.xml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Main.folder.spec ├── Main.onefile.spec ├── Main.py ├── Path.py ├── README.md ├── common ├── Config.py ├── MyThread.py ├── MyWidget.py ├── SignalBus.py ├── Style.py └── Uploader.py ├── requirements.txt ├── res ├── LICENCE.html ├── LICENCE.md ├── icons │ ├── key_black.svg │ ├── key_white.svg │ ├── link_black.svg │ ├── link_white.svg │ ├── logo.ico │ ├── number_black.svg │ ├── number_white.svg │ ├── play_black.svg │ ├── play_white.svg │ ├── server_black.svg │ └── server_white.svg ├── lang │ ├── zh_CN.qm │ └── zh_CN.ts └── qss │ ├── dark │ ├── download_interface.qss │ ├── main.qss │ ├── scroll_interface.qss │ ├── upload_interface.qss │ └── video_card.qss │ └── light │ ├── download_interface.qss │ ├── main.qss │ ├── scroll_interface.qss │ ├── upload_interface.qss │ └── video_card.qss ├── run_pylupdate.bat ├── screenshot.png └── view ├── DownloadInterface.py ├── InfoInterface.py ├── LocalVideoInterface.py ├── SettingInterface.py ├── SubscribeInterface.py ├── TodoListInterface.py └── UploadInterface.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.8 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: "3.8" 23 | - name: set up environment 24 | run: | 25 | sudo apt install python3-wheel 26 | pip install wheel 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Test with pytest 32 | run: | 33 | MainRun 34 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at aye10032@aye10032.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Main.folder.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis( 8 | ['D:\\program\\python\\YouTubeDownLoader\\Main.py'], 9 | pathex=['D:\\program\\python\\YouTubeDownLoader'], 10 | binaries=[], 11 | datas=[ 12 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\key_black.svg', 'res\\icons'), 13 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\key_white.svg', 'res\\icons'), 14 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\link_black.svg', 'res\\icons'), 15 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\link_white.svg', 'res\\icons'), 16 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\logo.ico', 'res\\icons'), 17 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\number_black.svg', 'res\\icons'), 18 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\number_white.svg', 'res\\icons'), 19 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\play_black.svg', 'res\\icons'), 20 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\play_white.svg', 'res\\icons'), 21 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\server_black.svg', 'res\\icons'), 22 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\server_white.svg', 'res\\icons'), 23 | ('D:\\program\\python\\YouTubeDownLoader\\res\\lang\\zh_CN.qm', 'res\\lang'), 24 | ('D:\\program\\python\\YouTubeDownLoader\\res\\lang\\zh_CN.ts', 'res\\lang'), 25 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\download_interface.qss', 'res\\qss\\light'), 26 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\main.qss', 'res\\qss\\light'), 27 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\scroll_interface.qss', 'res\\qss\\light'), 28 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\upload_interface.qss', 'res\\qss\\light'), 29 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\video_card.qss', 'res\\qss\\light'), 30 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\download_interface.qss', 'res\\qss\\dark'), 31 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\main.qss', 'res\\qss\\dark'), 32 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\scroll_interface.qss', 'res\\qss\\dark'), 33 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\upload_interface.qss', 'res\\qss\\dark'), 34 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\video_card.qss', 'res\\qss\\dark'), 35 | ('D:\\program\\python\\YouTubeDownLoader\\res\\LICENCE.html', 'res'), 36 | ], 37 | hiddenimports=[], 38 | hookspath=[], 39 | hooksconfig={}, 40 | runtime_hooks=[], 41 | excludes=[], 42 | win_no_prefer_redirects=False, 43 | win_private_assemblies=False, 44 | cipher=block_cipher, 45 | noarchive=False, 46 | ) 47 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 48 | 49 | exe = EXE( 50 | pyz, 51 | a.scripts, 52 | [], 53 | exclude_binaries=True, 54 | name='YouTubeDownLoader_V', 55 | debug=False, 56 | bootloader_ignore_signals=False, 57 | strip=False, 58 | upx=True, 59 | console=True, 60 | disable_windowed_traceback=False, 61 | argv_emulation=False, 62 | target_arch=None, 63 | codesign_identity=None, 64 | entitlements_file=None, 65 | icon='D:\\program\\python\\YouTubeDownLoader\\res\icons\\logo.ico', 66 | ) 67 | coll = COLLECT( 68 | exe, 69 | a.binaries, 70 | a.zipfiles, 71 | a.datas, 72 | strip=False, 73 | upx=True, 74 | upx_exclude=[], 75 | name='Main', 76 | ) 77 | -------------------------------------------------------------------------------- /Main.onefile.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | block_cipher = None 3 | 4 | 5 | a = Analysis(['D:\\program\\python\\YouTubeDownLoader\\Main.py'], 6 | pathex=['D:\\program\\python\\YouTubeDownLoader'], 7 | binaries=[], 8 | datas=[ 9 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\key_black.svg', 'res\\icons'), 10 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\key_white.svg', 'res\\icons'), 11 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\link_black.svg', 'res\\icons'), 12 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\link_white.svg', 'res\\icons'), 13 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\logo.ico', 'res\\icons'), 14 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\number_black.svg', 'res\\icons'), 15 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\number_white.svg', 'res\\icons'), 16 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\play_black.svg', 'res\\icons'), 17 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\play_white.svg', 'res\\icons'), 18 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\server_black.svg', 'res\\icons'), 19 | ('D:\\program\\python\\YouTubeDownLoader\\res\\icons\\server_white.svg', 'res\\icons'), 20 | ('D:\\program\\python\\YouTubeDownLoader\\res\\lang\\zh_CN.qm', 'res\\lang'), 21 | ('D:\\program\\python\\YouTubeDownLoader\\res\\lang\\zh_CN.ts', 'res\\lang'), 22 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\download_interface.qss', 'res\\qss\\light'), 23 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\main.qss', 'res\\qss\\light'), 24 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\scroll_interface.qss', 'res\\qss\\light'), 25 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\upload_interface.qss', 'res\\qss\\light'), 26 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\light\\video_card.qss', 'res\\qss\\light'), 27 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\download_interface.qss', 'res\\qss\\dark'), 28 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\main.qss', 'res\\qss\\dark'), 29 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\scroll_interface.qss', 'res\\qss\\dark'), 30 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\upload_interface.qss', 'res\\qss\\dark'), 31 | ('D:\\program\\python\\YouTubeDownLoader\\res\\qss\\dark\\video_card.qss', 'res\\qss\\dark'), 32 | ('D:\\program\\python\\YouTubeDownLoader\\res\\LICENCE.html', 'res'), 33 | ], 34 | hiddenimports=[], 35 | hookspath=[], 36 | runtime_hooks=[], 37 | excludes=[], 38 | win_no_prefer_redirects=False, 39 | win_private_assemblies=False, 40 | cipher=block_cipher 41 | ) 42 | pyz = PYZ(a.pure, a.zipped_data, 43 | cipher=block_cipher) 44 | exe = EXE(pyz, 45 | a.scripts, 46 | a.binaries, 47 | a.zipfiles, 48 | a.datas, 49 | name='YouTubeDownLoader_V.exe', 50 | debug=False, 51 | strip=False, 52 | upx=True, 53 | runtime_tmpdir='temp', 54 | console=True, 55 | icon='D:\\program\\python\\YouTubeDownLoader\\res\icons\\logo.ico') -------------------------------------------------------------------------------- /Main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from PyQt5.QtCore import Qt, QTranslator 6 | from PyQt5.QtGui import QIcon 7 | from PyQt5.QtWidgets import QApplication, QLabel, QFrame, QHBoxLayout, QStackedWidget 8 | from qfluentwidgets import NavigationInterface, NavigationItemPosition, setTheme, Theme, PopUpAniStackedWidget, \ 9 | FluentTranslator, Dialog 10 | from qfluentwidgets import FluentIcon as FIF 11 | from qframelesswindow import FramelessWindow, StandardTitleBar 12 | 13 | from Path import BASE_DIR 14 | from common.Config import cfg, VERSION, LOG_PATH, LOG_NAME 15 | from common.SignalBus import signal_bus 16 | from common.Style import StyleSheet 17 | from view.DownloadInterface import DownloadInterface 18 | from view.InfoInterface import InfoInterface 19 | from view.LocalVideoInterface import LocalVideoInterface 20 | from view.SettingInterface import SettingInterface 21 | from view.SubscribeInterface import SubscribeInterface 22 | from view.TodoListInterface import TodoListInterface 23 | from view.UploadInterface import UploadInterface 24 | 25 | 26 | class Window(FramelessWindow): 27 | def __init__(self): 28 | super().__init__() 29 | self.setTitleBar(StandardTitleBar(self)) 30 | 31 | setTheme(Theme.LIGHT) 32 | 33 | self.h_box_layout = QHBoxLayout(self) 34 | self.view = PopUpAniStackedWidget(self) 35 | self.navigation_interface = NavigationInterface(self, showMenuButton=True, showReturnButton=False) 36 | self.stack_widget = QStackedWidget(self) 37 | 38 | self.download_interface = DownloadInterface('edit_interface', self) 39 | self.upload_interface = UploadInterface('upload_interface', self) 40 | self.local_video_interface = LocalVideoInterface('local_video_interface', self) 41 | self.subscribe_interface = SubscribeInterface('subscribe_interface', self) 42 | self.todo_list_interface = TodoListInterface('todo_list_interface', self) 43 | self.info_interface = InfoInterface('info_interface', self) 44 | self.setting_interface = SettingInterface('setting_interface', self) 45 | 46 | self.stack_widget.addWidget(self.download_interface) 47 | self.stack_widget.addWidget(self.upload_interface) 48 | self.stack_widget.addWidget(self.local_video_interface) 49 | self.stack_widget.addWidget(self.subscribe_interface) 50 | self.stack_widget.addWidget(self.todo_list_interface) 51 | self.stack_widget.addWidget(self.info_interface) 52 | self.stack_widget.addWidget(self.setting_interface) 53 | 54 | # initialize layout 55 | self.init_layout() 56 | 57 | # add items to navigation interface 58 | self.init_navigation() 59 | 60 | self.init_window() 61 | self.connect_signal() 62 | 63 | def init_layout(self): 64 | self.h_box_layout.setSpacing(0) 65 | self.h_box_layout.setContentsMargins(0, self.titleBar.height(), 0, 0) 66 | self.h_box_layout.addWidget(self.navigation_interface) 67 | self.h_box_layout.addWidget(self.stack_widget) 68 | self.h_box_layout.setStretchFactor(self.stack_widget, 1) # 缩放因子 69 | 70 | def init_navigation(self): 71 | self.navigation_interface.addItem(routeKey=self.download_interface.objectName(), icon=FIF.EDIT, 72 | text=self.tr('Download'), 73 | onClick=lambda: self.switch_to(self.download_interface)) 74 | self.navigation_interface.addItem(routeKey=self.upload_interface.objectName(), icon=FIF.SEND, 75 | text=self.tr('Upload'), 76 | onClick=lambda: self.switch_to(self.upload_interface)) 77 | self.navigation_interface.addItem(routeKey=self.local_video_interface.objectName(), icon=FIF.HISTORY, 78 | text=self.tr('Local Video'), 79 | onClick=lambda: self.switch_to(self.local_video_interface), 80 | position=NavigationItemPosition.SCROLL) 81 | self.navigation_interface.addItem(routeKey=self.subscribe_interface.objectName(), icon=FIF.RINGER, 82 | text=self.tr('Subscription Information'), 83 | onClick=self.switch_to_subscribe, 84 | position=NavigationItemPosition.SCROLL) 85 | self.navigation_interface.addItem(routeKey=self.todo_list_interface.objectName(), icon=FIF.FEEDBACK, 86 | text=self.tr('TODO List'), 87 | onClick=self.switch_to_todo, 88 | position=NavigationItemPosition.SCROLL) 89 | 90 | self.navigation_interface.addSeparator() 91 | 92 | self.navigation_interface.addItem(routeKey=self.info_interface.objectName(), icon=FIF.INFO, 93 | text=self.tr('Info'), 94 | onClick=lambda: self.switch_to(self.info_interface), 95 | position=NavigationItemPosition.BOTTOM) 96 | self.navigation_interface.addItem(routeKey=self.setting_interface.objectName(), icon=FIF.SETTING, 97 | text=self.tr('Setting'), 98 | onClick=lambda: self.switch_to(self.setting_interface), 99 | position=NavigationItemPosition.BOTTOM) 100 | 101 | self.navigation_interface.setExpandWidth(200) 102 | 103 | self.stack_widget.currentChanged.connect(self.on_current_interface_changed) 104 | self.stack_widget.setCurrentIndex(0) 105 | self.navigation_interface.setCurrentItem(self.download_interface.objectName()) 106 | 107 | def init_window(self): 108 | self.resize(650, 750) 109 | self.setWindowIcon(QIcon(f'{BASE_DIR}/res/icons/logo.ico')) 110 | self.setWindowTitle('YoutubeDownloader V' + VERSION) 111 | 112 | desktop = QApplication.desktop().availableGeometry() 113 | w, h = desktop.width(), desktop.height() 114 | self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2) 115 | 116 | self.navigation_interface.displayModeChanged.connect( 117 | self.titleBar.raise_) 118 | self.titleBar.raise_() 119 | 120 | self.set_qss() 121 | 122 | def connect_signal(self): 123 | signal_bus.path2_download_signal.connect(self.local2_download) 124 | signal_bus.url2_download_signal.connect(self.url2_download) 125 | signal_bus.path2_upload_signal.connect(self.path2_upload) 126 | 127 | def set_qss(self): 128 | StyleSheet.MAIN_WINDOW.apply(self) 129 | 130 | def local2_download(self, path): 131 | self.download_interface.update_ui(path) 132 | self.switch_to(self.download_interface) 133 | 134 | def url2_download(self, url): 135 | self.download_interface.set_url(url) 136 | self.switch_to(self.download_interface) 137 | 138 | def path2_upload(self, path): 139 | self.upload_interface.init_text(path) 140 | self.switch_to(self.upload_interface) 141 | 142 | def switch_to(self, widget): 143 | self.stack_widget.setCurrentWidget(widget) 144 | self.navigation_interface.setCurrentItem(widget.objectName()) 145 | 146 | def switch_to_subscribe(self): 147 | if cfg.get(cfg.api_token) == '': 148 | dialog = Dialog( 149 | self.tr('No API Token!'), 150 | self.tr('You haven\'t set your token yet, please go to the settings screen to set it first'), 151 | self.window()) 152 | dialog.setTitleBarVisible(False) 153 | if dialog.exec(): 154 | self.switch_to(self.setting_interface) 155 | else: 156 | self.switch_to(self.download_interface) 157 | else: 158 | self.switch_to(self.subscribe_interface) 159 | 160 | def switch_to_todo(self): 161 | if cfg.get(cfg.api_server) == '': 162 | dialog = Dialog( 163 | self.tr('No API Server!'), 164 | self.tr('You haven\'t set api server yet, please go to the settings screen to set it first'), 165 | self.window()) 166 | dialog.setTitleBarVisible(False) 167 | if dialog.exec(): 168 | self.switch_to(self.setting_interface) 169 | else: 170 | self.switch_to(self.download_interface) 171 | else: 172 | self.switch_to(self.todo_list_interface) 173 | 174 | def on_current_interface_changed(self, index): 175 | widget = self.stack_widget.widget(index) 176 | self.navigation_interface.setCurrentItem(widget.objectName()) 177 | 178 | 179 | class Logger(object): 180 | def __init__(self, filename='default.log', stream=sys.stdout): 181 | self.terminal = stream 182 | self.log = open(filename, 'a', encoding='utf-8') 183 | 184 | def write(self, message): 185 | try: 186 | self.terminal.write(message) 187 | self.terminal.flush() 188 | self.log.write(message) 189 | self.log.flush() 190 | except Exception as e: 191 | print(str(e)) 192 | 193 | def debug(self, message): 194 | self.terminal.write('[debug]' + message + '\n') 195 | self.terminal.flush() 196 | self.log.write('[debug]' + message + '\n') 197 | self.log.flush() 198 | 199 | def warning(self, message): 200 | self.terminal.write('[warning]' + message + '\n') 201 | self.terminal.flush() 202 | self.log.write('[warning]' + message + '\n') 203 | self.log.flush() 204 | 205 | def error(self, message): 206 | self.terminal.write('[error]' + message + '\n') 207 | self.terminal.flush() 208 | self.log.write('[error]' + message + '\n') 209 | self.log.flush() 210 | 211 | def isatty(self): 212 | return False 213 | 214 | def flush(self): 215 | pass 216 | 217 | 218 | if __name__ == '__main__': 219 | if not os.path.exists(LOG_PATH): 220 | os.makedirs(LOG_PATH) 221 | 222 | sys.stdout = Logger(LOG_PATH + '/' + LOG_NAME + '.log', sys.stdout) 223 | sys.stderr = Logger(LOG_PATH + '/' + LOG_NAME + '.log', sys.stderr) 224 | 225 | logging.basicConfig(filename=LOG_PATH + '/' + LOG_NAME + '.log', level=logging.INFO) 226 | 227 | QApplication.setHighDpiScaleFactorRoundingPolicy( 228 | Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) 229 | QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 230 | QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) 231 | 232 | app = QApplication(sys.argv) 233 | 234 | # internationalization 235 | locale = cfg.get(cfg.language).value 236 | fluentTranslator = FluentTranslator(locale) 237 | settingTranslator = QTranslator() 238 | settingTranslator.load(locale, '', '', f'{BASE_DIR}/res/lang') 239 | 240 | app.installTranslator(fluentTranslator) 241 | app.installTranslator(settingTranslator) 242 | 243 | w = Window() 244 | w.show() 245 | app.exec_() 246 | -------------------------------------------------------------------------------- /Path.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | BASE_DIR = "" 5 | if getattr(sys, 'frozen', False): 6 | # we are running in a |PyInstaller| bundle 7 | BASE_DIR = sys._MEIPASS 8 | else: 9 | # we are running in a normal Python environment 10 | BASE_DIR = os.path.dirname(__file__) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Download & Manage 2 | 3 | 本项目已迁移,请前往[https://github.com/Redstone-Tech-Reupload-Group/YouTubeDownLoader](https://github.com/Redstone-Tech-Reupload-Group/YouTubeDownLoader)查看后续更新 4 | -------------------------------------------------------------------------------- /common/Config.py: -------------------------------------------------------------------------------- 1 | import time 2 | from enum import Enum 3 | 4 | from PyQt5.QtCore import QLocale 5 | from qfluentwidgets import qconfig, OptionsConfigItem, OptionsValidator, QConfig, ConfigItem, \ 6 | RangeConfigItem, RangeValidator, BoolValidator, ConfigSerializer, FolderValidator 7 | 8 | from Path import BASE_DIR 9 | 10 | VERSION = '6.2.0' 11 | LICENCE_PATH = f'{BASE_DIR}/res/LICENCE.html' 12 | 13 | LOG_PATH = 'log' 14 | LOG_NAME = time.strftime("%Y-%m-%d", time.localtime()) 15 | 16 | INFO = 0 17 | SUCCESS = 1 18 | WARNING = 2 19 | 20 | 21 | class Language(Enum): 22 | """ Language enumeration """ 23 | 24 | CHINESE_SIMPLIFIED = QLocale(QLocale.Chinese, QLocale.China) 25 | ENGLISH = QLocale(QLocale.English) 26 | AUTO = QLocale() 27 | 28 | 29 | class LanguageSerializer(ConfigSerializer): 30 | """ Language serializer """ 31 | 32 | def serialize(self, language): 33 | return language.value.name() if language != Language.AUTO else "Auto" 34 | 35 | def deserialize(self, value: str): 36 | return Language(QLocale(value)) if value != "Auto" else Language.AUTO 37 | 38 | 39 | class Config(QConfig): 40 | reprint_id = ConfigItem( 41 | 'DownloadSetting', 'Reprint', '' 42 | ) 43 | proxy_enable = ConfigItem( 44 | "DownloadSetting", "EnableProxy", True, BoolValidator()) 45 | 46 | proxy = ConfigItem( 47 | 'DownloadSetting', 'Proxy', 'http://127.0.0.1:1080' 48 | ) 49 | thread = RangeConfigItem( 50 | "DownloadSetting", "Thread", 4, RangeValidator(1, 16)) 51 | download_folder = ConfigItem( 52 | "DownloadSetting", "DownloadFolder", "download", FolderValidator()) 53 | auto_quality = ConfigItem( 54 | "DownloadSetting", "AutoQuality", True, BoolValidator()) 55 | 56 | api_token = ConfigItem( 57 | "AdvancedSetting", "ApiToken", "", restart=True 58 | ) 59 | subscribe_channels = ConfigItem( 60 | "AdvancedSetting", "SubscribeChannels", []) 61 | api_server = ConfigItem( 62 | "AdvancedSetting", "ApiServer", "", restart=True 63 | ) 64 | 65 | language = OptionsConfigItem( 66 | "System", "Language", Language.AUTO, OptionsValidator(Language), LanguageSerializer(), restart=True) 67 | 68 | 69 | cfg = Config() 70 | qconfig.load(f'config/config.json', cfg) 71 | -------------------------------------------------------------------------------- /common/MyThread.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from urllib.error import URLError 3 | 4 | from PyQt5.QtCore import QThread, pyqtSignal 5 | from yt_dlp import YoutubeDL, DownloadError 6 | from yt_dlp.extractor.youtube import YoutubeIE 7 | 8 | from common.Config import cfg 9 | from common.Uploader import BiliBili, Data 10 | 11 | 12 | class UpdateMessage(QThread): 13 | log_signal = pyqtSignal(dict) 14 | result_signal = pyqtSignal(dict) 15 | finish_signal = pyqtSignal() 16 | error_signal = pyqtSignal() 17 | 18 | def __init__(self, url): 19 | super().__init__() 20 | self.url = url 21 | 22 | def run(self): 23 | try: 24 | logger = logging.getLogger() 25 | handler = logging.StreamHandler() 26 | logger.addHandler(handler) 27 | 28 | ydl_opts = { 29 | 'logger': logger 30 | } 31 | if cfg.get(cfg.proxy_enable): 32 | ydl_opts['proxy'] = cfg.get(cfg.proxy) 33 | ydl_opts['socket_timeout'] = 3000 34 | 35 | # print(ydl_opts) 36 | ydl = YoutubeDL(ydl_opts) 37 | ie = YoutubeIE 38 | ydl.add_info_extractor(ie) 39 | ydl.add_progress_hook(self.my_hook) 40 | info_dict = ydl.extract_info(self.url, download=False, force_generic_extractor=True) 41 | 42 | self.result_signal.emit(info_dict) 43 | self.finish_signal.emit() 44 | except DownloadError as e: 45 | self.error_signal.emit() 46 | except ConnectionRefusedError as e: 47 | self.error_signal.emit() 48 | except URLError as e: 49 | self.error_signal.emit() 50 | 51 | def my_hook(self, d): 52 | self.log_signal.emit(d) 53 | 54 | 55 | class Download(QThread): 56 | log_signal = pyqtSignal(dict) 57 | finish_signal = pyqtSignal() 58 | error_signal = pyqtSignal() 59 | 60 | def __init__(self, url, ydl_opts): 61 | super().__init__() 62 | self.url = url 63 | self.ydl_opts = ydl_opts 64 | # print(ydl_opts) 65 | 66 | def run(self): 67 | try: 68 | logger = logging.getLogger() 69 | handler = logging.StreamHandler() 70 | logger.addHandler(handler) 71 | self.ydl_opts['logger'] = logger 72 | 73 | ydl = YoutubeDL(self.ydl_opts) 74 | ydl.add_progress_hook(self.my_hook) 75 | ydl.download(self.url) 76 | 77 | self.finish_signal.emit() 78 | except DownloadError as e: 79 | self.error_signal.emit() 80 | except ConnectionRefusedError as e: 81 | self.error_signal.emit() 82 | 83 | def my_hook(self, d): 84 | self.log_signal.emit(d) 85 | 86 | 87 | class Upload(QThread): 88 | finish_signal = pyqtSignal() 89 | 90 | def __init__(self, login_access, info: dict, video_list: list): 91 | super().__init__() 92 | self.login_access = login_access 93 | self.info = info 94 | self.video_list = video_list 95 | 96 | def run(self): 97 | video = Data() 98 | video.title = self.info['title'] 99 | video.desc = self.info['desc'] 100 | video.source = self.info['source'] 101 | video.tid = 17 102 | video.set_tag(self.info['tag']) 103 | video.dynamic = self.info['dynamic'] 104 | lines = 'AUTO' 105 | tasks = 3 106 | dtime = 0 # 延后时间,单位秒 107 | with BiliBili(video) as bili: 108 | bili.login("bili.cookie", self.login_access) 109 | 110 | for part in self.video_list: 111 | video_part = bili.upload_file(part['path'], part['name'], lines=lines, tasks=tasks) 112 | video.append(video_part) 113 | video.delay_time(dtime) 114 | video.cover = bili.cover_up(self.info['cover_path']).replace('http:', '') 115 | ret = bili.submit_client() # 提交视频 116 | 117 | self.finish_signal.emit() 118 | -------------------------------------------------------------------------------- /common/MyWidget.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import qfluentwidgets 4 | from PyQt5.QtCore import pyqtSignal, Qt, QRectF 5 | from PyQt5.QtGui import QIcon, QPixmap, QPainter 6 | from PyQt5.QtWidgets import QLabel, QWidget, QVBoxLayout, QGridLayout, QTableWidgetItem, QFrame, \ 7 | QHBoxLayout, QToolButton 8 | from qfluentwidgets import SettingCard, FluentIconBase, Slider, qconfig, LineEdit, TableWidget, \ 9 | TextWrap, PixmapLabel, ExpandLayout, ExpandSettingCard, ConfigItem, PushButton, drawIcon, isDarkTheme 10 | from qfluentwidgets.components.dialog_box.dialog import Dialog 11 | from qfluentwidgets import FluentIcon as FIF 12 | from qframelesswindow import TitleBar 13 | 14 | from Path import BASE_DIR 15 | from common.SignalBus import signal_bus 16 | from common.Style import StyleSheet, MyIcon 17 | 18 | 19 | class CustomTitleBar(TitleBar): 20 | """ Title bar with icon and title """ 21 | 22 | def __init__(self, parent): 23 | super().__init__(parent) 24 | self.setFixedHeight(48) 25 | self.hBoxLayout.removeWidget(self.minBtn) 26 | self.hBoxLayout.removeWidget(self.maxBtn) 27 | self.hBoxLayout.removeWidget(self.closeBtn) 28 | 29 | # add window icon 30 | self.iconLabel = QLabel(self) 31 | self.iconLabel.setFixedSize(18, 18) 32 | self.hBoxLayout.insertSpacing(0, 10) 33 | self.hBoxLayout.insertWidget(1, self.iconLabel, 0, Qt.AlignLeft | Qt.AlignVCenter) 34 | self.window().windowIconChanged.connect(self.setIcon) 35 | 36 | # add title label 37 | self.titleLabel = QLabel(self) 38 | self.hBoxLayout.insertWidget(2, self.titleLabel, 0, Qt.AlignLeft | Qt.AlignVCenter) 39 | self.titleLabel.setObjectName('titleLabel') 40 | self.window().windowTitleChanged.connect(self.setTitle) 41 | 42 | self.vBoxLayout = QVBoxLayout() 43 | self.buttonLayout = QHBoxLayout() 44 | self.buttonLayout.setSpacing(0) 45 | self.buttonLayout.setContentsMargins(0, 0, 0, 0) 46 | self.buttonLayout.setAlignment(Qt.AlignTop) 47 | self.buttonLayout.addWidget(self.minBtn) 48 | self.buttonLayout.addWidget(self.maxBtn) 49 | self.buttonLayout.addWidget(self.closeBtn) 50 | self.vBoxLayout.addLayout(self.buttonLayout) 51 | self.vBoxLayout.addStretch(1) 52 | self.hBoxLayout.addLayout(self.vBoxLayout, 0) 53 | 54 | def setTitle(self, title): 55 | self.titleLabel.setText(title) 56 | self.titleLabel.adjustSize() 57 | 58 | def setIcon(self, icon): 59 | self.iconLabel.setPixmap(QIcon(icon).pixmap(18, 18)) 60 | 61 | 62 | class RangeSettingCard(SettingCard): 63 | """ Setting card with a slider """ 64 | 65 | valueChanged = pyqtSignal(int) 66 | 67 | def __init__(self, configItem, icon: Union[str, QIcon, FluentIconBase], title, content=None, parent=None): 68 | """ 69 | Parameters 70 | ---------- 71 | configItem: RangeConfigItem 72 | configuration item operated by the card 73 | 74 | icon: str | QIcon | FluentIconBase 75 | the icon to be drawn 76 | 77 | title: str 78 | the title of card 79 | 80 | content: str 81 | the content of card 82 | 83 | parent: QWidget 84 | parent widget 85 | """ 86 | super().__init__(icon, title, content, parent) 87 | self.configItem = configItem 88 | self.slider = Slider(Qt.Horizontal, self) 89 | self.valueLabel = QLabel(self) 90 | self.slider.setMinimumWidth(180) 91 | 92 | self.slider.setSingleStep(1) 93 | self.slider.setRange(*configItem.range) 94 | self.slider.setValue(configItem.value) 95 | self.valueLabel.setNum(configItem.value) 96 | 97 | self.hBoxLayout.addStretch(1) 98 | self.hBoxLayout.addWidget(self.valueLabel, 0, Qt.AlignRight) 99 | self.hBoxLayout.addSpacing(6) 100 | self.hBoxLayout.addWidget(self.slider, 0, Qt.AlignRight) 101 | self.hBoxLayout.addSpacing(16) 102 | 103 | self.valueLabel.setObjectName('valueLabel') 104 | configItem.valueChanged.connect(self.setValue) 105 | self.slider.valueChanged.connect(self.__onValueChanged) 106 | 107 | def __onValueChanged(self, value: int): 108 | """ slider value changed slot """ 109 | self.setValue(value) 110 | self.valueChanged.emit(value) 111 | 112 | def setValue(self, value): 113 | qconfig.set(self.configItem, value) 114 | self.valueLabel.setNum(value) 115 | self.valueLabel.adjustSize() 116 | 117 | 118 | class TextDialog(Dialog): 119 | """ Dialog box """ 120 | 121 | yesSignal = pyqtSignal() 122 | cancelSignal = pyqtSignal() 123 | 124 | def __init__(self, title: str, content: str, default: str, parent=None): 125 | super().__init__(title, content, parent=parent) 126 | self.vBoxLayout.removeWidget(self.windowTitleLabel) 127 | self.input_edit = LineEdit(self) 128 | self.input_edit.setText(default) 129 | self.main_widget = QWidget() 130 | self.main_layout = QGridLayout() 131 | self.main_layout.setSpacing(20) 132 | self.main_layout.addWidget(self.titleLabel, 0, 0) 133 | self.main_layout.addWidget(self.contentLabel, 1, 0) 134 | self.main_layout.addWidget(self.input_edit, 2, 0) 135 | self.titleLabel.setContentsMargins(10, 10, 5, 0) 136 | self.contentLabel.setContentsMargins(10, 10, 5, 0) 137 | self.input_edit.setContentsMargins(10, 0, 10, 0) 138 | self.main_widget.setLayout(self.main_layout) 139 | self.vBoxLayout.insertWidget(0, self.main_widget) 140 | 141 | 142 | class TableDialog(Dialog): 143 | """ Dialog box """ 144 | 145 | yesSignal = pyqtSignal() 146 | cancelSignal = pyqtSignal() 147 | audio_code = '' 148 | video_code = '' 149 | 150 | def __init__(self, row: int, col: int, content: [], parent=None): 151 | super().__init__('', '', parent=parent) 152 | self.tableView = TableWidget(self) 153 | 154 | self.tableView.setRowCount(row) 155 | self.tableView.setColumnCount(col) 156 | 157 | for i in range(row): 158 | self.tableView.setItem(i, 0, QTableWidgetItem(content[0][i])) 159 | self.tableView.setItem(i, 1, QTableWidgetItem(content[1][i])) 160 | self.tableView.setItem(i, 2, QTableWidgetItem(content[2][i])) 161 | self.tableView.setItem(i, 3, QTableWidgetItem(content[3][i])) 162 | self.tableView.setItem(i, 4, QTableWidgetItem('None' if content[4][i] is None else str(content[4][i]))) 163 | 164 | self.init_ui() 165 | 166 | def init_ui(self): 167 | self.vBoxLayout.removeWidget(self.contentLabel) 168 | self.vBoxLayout.removeItem(self.textLayout) 169 | 170 | self.tableView.setWordWrap(False) 171 | self.tableView.verticalHeader().hide() 172 | self.tableView.setHorizontalHeaderLabels(['代号', '格式', '描述', '编码信息', '文件大小']) 173 | self.tableView.resizeColumnsToContents() 174 | 175 | self.vBoxLayout.insertWidget(0, self.tableView) 176 | 177 | self.setFixedSize(450, 400) 178 | 179 | self.tableView.itemClicked.connect(self.on_item_clicked) 180 | 181 | def on_item_clicked(self, item: QTableWidgetItem): 182 | row = item.row() 183 | if self.tableView.item(row, 2).text() == 'audio only': 184 | self.audio_code = self.tableView.item(row, 0).text() 185 | else: 186 | self.video_code = self.tableView.item(row, 0).text() 187 | 188 | 189 | class VideoCard(QFrame): 190 | def __init__(self, image: QPixmap, title, content, route_key, index, parent=None): 191 | super().__init__(parent=parent) 192 | self.index = index 193 | self.route_key = route_key 194 | self.path = content 195 | 196 | self.image_widget = PixmapLabel(self) 197 | self.image_widget.setPixmap(image.scaled( 198 | 128, 128, Qt.KeepAspectRatio, Qt.SmoothTransformation 199 | )) 200 | self.title_label = QLabel(title, self) 201 | self.content_label = QLabel(TextWrap.wrap(content, 45, False)[0], self) 202 | 203 | self.hBoxLayout = QHBoxLayout(self) 204 | self.vBoxLayout = QVBoxLayout() 205 | 206 | self.setFixedHeight(90) 207 | self.hBoxLayout.setSpacing(28) 208 | self.hBoxLayout.setContentsMargins(20, 0, 0, 0) 209 | self.vBoxLayout.setSpacing(2) 210 | self.vBoxLayout.setContentsMargins(0, 0, 0, 0) 211 | self.vBoxLayout.setAlignment(Qt.AlignVCenter) 212 | 213 | self.hBoxLayout.setAlignment(Qt.AlignVCenter) 214 | self.hBoxLayout.addWidget(self.image_widget) 215 | self.hBoxLayout.addLayout(self.vBoxLayout) 216 | self.vBoxLayout.addStretch(1) 217 | self.vBoxLayout.addWidget(self.title_label) 218 | self.vBoxLayout.addWidget(self.content_label) 219 | self.vBoxLayout.addStretch(1) 220 | 221 | self.set_qss() 222 | 223 | def mouseReleaseEvent(self, e): 224 | super().mouseReleaseEvent(e) 225 | signal_bus.path2_download_signal.emit(self.path) 226 | 227 | def set_qss(self): 228 | self.title_label.setObjectName('titleLabel') 229 | self.content_label.setObjectName('contentLabel') 230 | 231 | StyleSheet.CARD.apply(self) 232 | 233 | 234 | class TextCard(QFrame): 235 | def __init__(self, title: str, upload_date: str, url: str, route_key, parent=None): 236 | super().__init__(parent=parent) 237 | self.route_key = route_key 238 | self.title = title 239 | self.upload_date = upload_date 240 | self.url = url 241 | 242 | # self.title_label = QLabel(TextWrap.wrap(self.title, 70, False)[0], self) 243 | self.title_label = QLabel(self.title, self) 244 | self.upload_date_label = QLabel(self.upload_date, self) 245 | self.url_label = QLabel(self.url, self) 246 | 247 | self.vBoxLayout = QVBoxLayout(self) 248 | 249 | self.setFixedHeight(75) 250 | # self.setFixedWidth(500) 251 | self.vBoxLayout.setSpacing(2) 252 | self.vBoxLayout.setContentsMargins(10, 0, 10, 0) 253 | self.vBoxLayout.setAlignment(Qt.AlignVCenter) 254 | 255 | self.vBoxLayout.addStretch(1) 256 | self.vBoxLayout.addWidget(self.title_label) 257 | self.vBoxLayout.addWidget(self.url_label) 258 | self.vBoxLayout.addWidget(self.upload_date_label) 259 | self.upload_date_label.setAlignment(Qt.AlignRight) 260 | self.upload_date_label.setContentsMargins(10, 0, 30, 0) 261 | self.vBoxLayout.addStretch(1) 262 | 263 | self.set_qss() 264 | 265 | def mouseReleaseEvent(self, e): 266 | super().mouseReleaseEvent(e) 267 | signal_bus.url2_download_signal.emit(self.url) 268 | 269 | def set_qss(self): 270 | self.title_label.setObjectName('titleLabel') 271 | self.url_label.setObjectName('contentLabel') 272 | self.upload_date_label.setObjectName('contentLabel') 273 | 274 | StyleSheet.CARD.apply(self) 275 | 276 | 277 | class VideoCardView(QWidget): 278 | def __init__(self, title: str, parent=None): 279 | super().__init__(parent=parent) 280 | self.titleLabel = QLabel(title, self) 281 | self.vBoxLayout = QVBoxLayout(self) 282 | self.cardLayout = ExpandLayout() 283 | 284 | self.vBoxLayout.setContentsMargins(0, 0, 0, 0) 285 | self.vBoxLayout.setAlignment(Qt.AlignTop) 286 | self.vBoxLayout.setSpacing(0) 287 | self.cardLayout.setContentsMargins(0, 0, 0, 0) 288 | self.cardLayout.setSpacing(5) 289 | 290 | if not title == '': 291 | self.vBoxLayout.addWidget(self.titleLabel) 292 | self.vBoxLayout.addSpacing(12) 293 | self.vBoxLayout.addLayout(self.cardLayout, 1) 294 | 295 | self.titleLabel.adjustSize() 296 | self.set_qss() 297 | 298 | def add_video_card(self, card: QWidget): 299 | card.setParent(self) 300 | self.cardLayout.addWidget(card) 301 | self.adjustSize() 302 | 303 | def adjustSize(self): 304 | h = self.cardLayout.heightForWidth(self.width()) + 46 305 | return self.resize(self.width(), h) 306 | 307 | def set_qss(self): 308 | self.titleLabel.setObjectName('viewTitleLabel') 309 | 310 | StyleSheet.CARD.apply(self) 311 | 312 | 313 | class ChannelDialog(Dialog): 314 | """ Dialog box """ 315 | 316 | yesSignal = pyqtSignal() 317 | cancelSignal = pyqtSignal() 318 | 319 | def __init__(self, parent=None): 320 | super().__init__('', '', parent=parent) 321 | self.channel_name_label = QLabel(self.tr('Channel Name:'), self) 322 | self.channel_name_input = LineEdit(self) 323 | self.channel_id_label = QLabel(self.tr('Channel ID:'), self) 324 | self.channel_id_input = LineEdit(self) 325 | 326 | self.init_ui() 327 | self.set_qss() 328 | 329 | def init_ui(self): 330 | self.vBoxLayout.removeWidget(self.contentLabel) 331 | self.vBoxLayout.removeItem(self.textLayout) 332 | 333 | self.channel_name_label.setContentsMargins(10, 10, 10, 0) 334 | self.channel_name_input.setContentsMargins(10, 0, 10, 0) 335 | self.channel_id_label.setContentsMargins(10, 10, 10, 0) 336 | self.channel_id_input.setContentsMargins(10, 0, 10, 0) 337 | 338 | self.vBoxLayout.insertSpacing(0, 10) 339 | self.vBoxLayout.insertWidget(0, self.channel_id_input) 340 | self.vBoxLayout.insertWidget(0, self.channel_id_label) 341 | self.vBoxLayout.insertWidget(0, self.channel_name_input) 342 | self.vBoxLayout.insertWidget(0, self.channel_name_label) 343 | 344 | self.setFixedSize(300, 300) 345 | 346 | def set_qss(self): 347 | self.channel_name_label.setObjectName("contentLabel") 348 | self.channel_id_label.setObjectName("contentLabel") 349 | 350 | StyleSheet.CARD.apply(self) 351 | 352 | 353 | class ToolButton(QToolButton): 354 | """ Tool button """ 355 | 356 | def __init__(self, icon, size: tuple, iconSize: tuple, parent=None): 357 | super().__init__(parent=parent) 358 | self.isPressed = False 359 | self._icon = icon 360 | self._iconSize = iconSize 361 | self.setFixedSize(*size) 362 | 363 | def mousePressEvent(self, e): 364 | self.isPressed = True 365 | super().mousePressEvent(e) 366 | 367 | def mouseReleaseEvent(self, e): 368 | self.isPressed = False 369 | super().mouseReleaseEvent(e) 370 | 371 | def paintEvent(self, e): 372 | super().paintEvent(e) 373 | painter = QPainter(self) 374 | painter.setRenderHints(QPainter.Antialiasing | 375 | QPainter.SmoothPixmapTransform) 376 | painter.setOpacity(0.63 if self.isPressed else 1) 377 | w, h = self._iconSize 378 | drawIcon(self._icon, painter, QRectF( 379 | (self.width() - w) / 2, (self.height() - h) / 2, w, h)) 380 | 381 | 382 | class ChannelItem(QWidget): 383 | """ Folder item """ 384 | 385 | removed = pyqtSignal(QWidget) 386 | 387 | def __init__(self, channel: dict, parent=None): 388 | super().__init__(parent=parent) 389 | self.channel = channel 390 | self.hBoxLayout = QHBoxLayout(self) 391 | self.name_label = QLabel(channel['name'], self) 392 | self.removeButton = ToolButton(FIF.CLOSE, (39, 29), (12, 12), self) 393 | 394 | self.setFixedHeight(53) 395 | self.hBoxLayout.setContentsMargins(48, 0, 60, 0) 396 | self.hBoxLayout.addWidget(self.name_label, 0, Qt.AlignLeft) 397 | self.hBoxLayout.addSpacing(16) 398 | self.hBoxLayout.addStretch(1) 399 | self.hBoxLayout.addWidget(self.removeButton, 0, Qt.AlignRight) 400 | self.hBoxLayout.setAlignment(Qt.AlignVCenter) 401 | 402 | self.removeButton.clicked.connect( 403 | lambda: self.removed.emit(self)) 404 | 405 | 406 | class DistListSettingCard(ExpandSettingCard): 407 | channel_changed_signal = pyqtSignal(list) 408 | 409 | def __init__(self, config_item: ConfigItem, title: str, content: str = None, parent=None): 410 | super().__init__(MyIcon.LINK.icon(), title, content, parent) 411 | self.configItem = config_item 412 | self.add_channel_btn = PushButton(self.tr('Add'), self) 413 | 414 | self.channels = qconfig.get(config_item).copy() 415 | self.__initWidget() 416 | 417 | def __initWidget(self): 418 | self.addWidget(self.add_channel_btn) 419 | 420 | # initialize layout 421 | self.viewLayout.setSpacing(0) 422 | self.viewLayout.setAlignment(Qt.AlignTop) 423 | self.viewLayout.setContentsMargins(0, 0, 0, 0) 424 | for channel in self.channels: 425 | self.__add_channel_item(channel['name'], channel['channel_id']) 426 | 427 | self.add_channel_btn.clicked.connect(self.__show_channel_dialog) 428 | 429 | def __show_channel_dialog(self): 430 | """ show folder dialog """ 431 | w = ChannelDialog(self) 432 | w.setTitleBarVisible(False) 433 | if w.exec(): 434 | channel = {'name': w.channel_name_input.text(), 'channel_id': w.channel_id_input.text()} 435 | if not channel or channel in self.channels: 436 | return 437 | 438 | self.__add_channel_item(channel['name'], channel['channel_id']) 439 | self.channels.append(channel) 440 | qconfig.set(self.configItem, self.channels) 441 | self.channel_changed_signal.emit(self.channels) 442 | 443 | def __add_channel_item(self, name: str, channel_id: str): 444 | """ add folder item """ 445 | item = ChannelItem({'name': name, 'channel_id': channel_id}, self.view) 446 | item.removed.connect(self.__show_confirm_dialog) 447 | self.viewLayout.addWidget(item) 448 | self._adjustViewSize() 449 | 450 | def __show_confirm_dialog(self, item: ChannelItem): 451 | """ show confirm dialog """ 452 | name = item.name_label.text() 453 | title = self.tr('Are you sure you want to delete the channel?') 454 | content = self.tr("If you delete the ") + f'"{name}"' + self.tr( 455 | " channel and remove it from the list, the channel will no longer appear in the list") 456 | w = Dialog(title, content, self.window()) 457 | w.yesSignal.connect(lambda: self.__remove_folder(item)) 458 | w.exec_() 459 | 460 | def __remove_folder(self, item: ChannelItem): 461 | """ remove folder """ 462 | for channel in self.channels: 463 | if channel['channel_id'] == item.channel['channel_id']: 464 | self.channels.remove(item.channel) 465 | self.viewLayout.deleteWidget(item) 466 | self._adjustViewSize() 467 | 468 | self.channel_changed_signal.emit(self.channels) 469 | qconfig.set(self.configItem, self.channels) 470 | 471 | return 472 | 473 | 474 | class UploadCard(QFrame): 475 | del_signal = pyqtSignal(str) 476 | 477 | def __init__(self, title: str, path: str, parent=None): 478 | super().__init__(parent=parent) 479 | self.title = title 480 | self.path = path 481 | 482 | self.title_input = LineEdit(self) 483 | self.title_input.setText(title) 484 | self.path_label = QLabel(TextWrap.wrap(self.path, 55, True)[0], self) 485 | 486 | self.del_btn = qfluentwidgets.ToolButton(FIF.DELETE, self) 487 | 488 | self.vBoxLayout = QVBoxLayout(self) 489 | self.hBoxLayout = QHBoxLayout() 490 | 491 | self.setFixedHeight(85) 492 | self.vBoxLayout.setSpacing(5) 493 | self.vBoxLayout.setContentsMargins(10, 0, 10, 0) 494 | self.hBoxLayout.setSpacing(5) 495 | self.hBoxLayout.setContentsMargins(5, 0, 5, 0) 496 | self.vBoxLayout.setAlignment(Qt.AlignVCenter) 497 | 498 | self.vBoxLayout.addStretch(1) 499 | self.vBoxLayout.addSpacing(5) 500 | self.vBoxLayout.addWidget(self.title_input) 501 | self.vBoxLayout.addLayout(self.hBoxLayout) 502 | self.vBoxLayout.addSpacing(5) 503 | self.vBoxLayout.addStretch(1) 504 | 505 | self.hBoxLayout.addWidget(self.path_label, stretch=5) 506 | self.hBoxLayout.addSpacing(5) 507 | self.hBoxLayout.addWidget(self.del_btn, stretch=1, alignment=Qt.AlignBottom) 508 | self.del_btn.clicked.connect(self.on_del_btn_clicked) 509 | 510 | self.set_qss() 511 | 512 | def set_qss(self): 513 | self.path_label.setObjectName('contentLabel') 514 | 515 | StyleSheet.CARD.apply(self) 516 | 517 | def on_del_btn_clicked(self): 518 | self.del_signal.emit(self.objectName()) 519 | -------------------------------------------------------------------------------- /common/SignalBus.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QObject, pyqtSignal 2 | 3 | 4 | class SignalBus(QObject): 5 | """ Signal bus """ 6 | 7 | path2_download_signal = pyqtSignal(str) 8 | url2_download_signal = pyqtSignal(str) 9 | path2_upload_signal = pyqtSignal(str) 10 | log_signal = pyqtSignal(str) 11 | 12 | 13 | signal_bus = SignalBus() 14 | -------------------------------------------------------------------------------- /common/Style.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from qfluentwidgets import StyleSheetBase, Theme, qconfig, FluentIconBase, getIconColor 4 | 5 | from Path import BASE_DIR 6 | 7 | 8 | class StyleSheet(StyleSheetBase, Enum): 9 | """ Style sheet """ 10 | 11 | MAIN_WINDOW = 'main' 12 | DOWNLOAD = 'download_interface' 13 | SCROLL = 'scroll_interface' 14 | UPLOAD = 'upload_interface' 15 | CARD = 'video_card' 16 | 17 | def path(self, theme=Theme.AUTO): 18 | theme = qconfig.theme if theme == Theme.AUTO else theme 19 | 20 | return f"{BASE_DIR}/res/qss/{theme.value.lower()}/{self.value}.qss" 21 | 22 | 23 | class MyIcon(FluentIconBase, Enum): 24 | """ Custom icons """ 25 | 26 | KEY = 'key' 27 | LINK = 'link' 28 | NUMBER = 'number' 29 | PLAY = 'play' 30 | SERVER = 'server' 31 | 32 | def path(self, theme=Theme.AUTO): 33 | return f'{BASE_DIR}/res/icons/{self.value}_{getIconColor(theme)}.svg' 34 | -------------------------------------------------------------------------------- /common/Uploader.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | import hashlib 4 | import json 5 | import math 6 | import os 7 | import re 8 | import shutil 9 | import subprocess 10 | import sys 11 | import time 12 | import urllib.parse 13 | from dataclasses import asdict, dataclass, field, InitVar 14 | from functools import reduce 15 | from json import JSONDecodeError 16 | from os.path import splitext, basename 17 | from pathlib import Path 18 | from typing import Union, Any 19 | from urllib import parse 20 | from urllib.parse import quote 21 | from aiohttp import ClientError, ClientSession 22 | 23 | import requests.utils 24 | import rsa 25 | import xml.etree.ElementTree as ET 26 | 27 | from requests.adapters import HTTPAdapter, Retry 28 | 29 | from common.SignalBus import signal_bus 30 | 31 | 32 | class UploadBase: 33 | def __init__(self, principal, data, persistence_path=None, postprocessor=None): 34 | self.principal = principal 35 | self.persistence_path = persistence_path 36 | self.data = data 37 | self.post_processor = postprocessor 38 | 39 | # @property 40 | @staticmethod 41 | def file_list(index): 42 | file_list = [] 43 | for file_name in os.listdir('.'): 44 | if index in file_name and os.path.isfile(file_name): 45 | file_list.append(file_name) 46 | file_list = sorted(file_list) 47 | return file_list 48 | 49 | @staticmethod 50 | def remove_filelist(file_list): 51 | for r in file_list: 52 | os.remove(r) 53 | print('[info] 删除-' + r) 54 | 55 | def filter_file(self, index): 56 | media_extensions = ['.mp4', '.flv', '.ts'] 57 | file_list = UploadBase.file_list(index) 58 | if len(file_list) == 0: 59 | return False 60 | for f in file_list: 61 | if f.endswith('.part'): 62 | new_name = os.path.splitext(f)[0] 63 | shutil.move(f, new_name) 64 | print(f'[info] {f}存在已更名为{new_name}') 65 | for r in file_list: 66 | name, ext = os.path.splitext(r) 67 | if ext in ('.mp4', '.flv', '.ts'): 68 | file_size = os.path.getsize(r) / 1024 / 1024 69 | threshold = self.data.get('threshold', 2) 70 | if file_size <= threshold: 71 | self.remove_file(r) 72 | print(f'[info] 过滤删除-{r}') 73 | if ext == '.xml': # 过滤不存在对应视频的xml弹幕文件 74 | xml_file_name = name 75 | media_regex = re.compile(r'^{}(\.(mp4|flv|ts))?$'.format( 76 | re.escape(xml_file_name) 77 | )) 78 | if not any(media_regex.match(f'{xml_file_name}{ext2}') for ext2 in media_extensions for x in file_list): 79 | self.remove_file(r) 80 | print(f'无视频,已过滤删除-{r}') 81 | file_list = UploadBase.file_list(index) 82 | if len(file_list) == 0: 83 | print('[info] 视频过滤后无文件可传') 84 | return False 85 | 86 | return True 87 | 88 | def remove_file(self, file_path): 89 | with open(file_path, 'r', encoding='utf-8'): 90 | os.remove(file_path) 91 | 92 | def upload(self, file_list): 93 | raise NotImplementedError() 94 | 95 | def start(self): 96 | if self.filter_file(self.principal): 97 | print('[info] 准备上传' + self.data["format_title"]) 98 | needed2process = self.upload(UploadBase.file_list(self.principal)) 99 | if needed2process: 100 | self.postprocessor(needed2process) 101 | 102 | def postprocessor(self, data): 103 | # data = file_list 104 | if self.post_processor is None: 105 | return self.remove_filelist(data) 106 | for post_processor in self.post_processor: 107 | if post_processor == 'rm': 108 | self.remove_filelist(data) 109 | continue 110 | if post_processor.get('mv'): 111 | for file in data: 112 | path = Path(file) 113 | dest = Path(post_processor['mv']) 114 | if not dest.is_dir(): 115 | dest.mkdir(parents=True, exist_ok=True) 116 | try: 117 | shutil.move(path, dest / path.name) 118 | except Exception as e: 119 | print(e) 120 | continue 121 | print(f"[info] move to {(dest / path.name).absolute()}") 122 | if post_processor.get('run'): 123 | try: 124 | process_output = subprocess.check_output( 125 | post_processor['run'], shell=True, 126 | input=reduce(lambda x, y: x + str(Path(y).absolute()) + '\n', data, ''), 127 | stderr=subprocess.STDOUT, text=True) 128 | print(f'[info] {process_output.rstrip()}') 129 | except subprocess.CalledProcessError as e: 130 | print(f'[error] {e.output}') 131 | continue 132 | 133 | 134 | class BiliBili: 135 | def __init__(self, video: 'Data'): 136 | self.app_key = None 137 | self.appsec = None 138 | if self.app_key is None or self.appsec is None: 139 | self.app_key = 'ae57252b0c09105d' 140 | self.appsec = 'c75875c596a69eb55bd119e74b07cfe3' 141 | self.__session = requests.Session() 142 | self.video = video 143 | self.__session.mount('https://', HTTPAdapter(max_retries=Retry(total=5))) 144 | self.__session.headers.update({ 145 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108", 146 | "Referer": "https://www.bilibili.com/", 'Connection': 'keep-alive' 147 | }) 148 | self.cookies = None 149 | self.access_token = None 150 | self.refresh_token = None 151 | self.account = None 152 | self.__bili_jct = None 153 | self._auto_os = None 154 | self.persistence_path = 'engine/bili.cookie' 155 | 156 | def check_tag(self, tag): 157 | r = self.__session.get("https://member.bilibili.com/x/vupre/web/topic/tag/check?tag=" + tag).json() 158 | if r["code"] == 0: 159 | return True 160 | else: 161 | return False 162 | 163 | def get_qrcode(self): 164 | params = { 165 | "appkey": "4409e2ce8ffd12b8", 166 | "local_id": "0", 167 | "ts": int(time.time()), 168 | } 169 | params["sign"] = hashlib.md5( 170 | f"{urllib.parse.urlencode(params)}59b43e04ad6965f34319062b478f83dd".encode()).hexdigest() 171 | response = self.__session.post("http://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code", data=params, 172 | timeout=5) 173 | r = response.json() 174 | if r and r["code"] == 0: 175 | return r 176 | 177 | async def login_by_qrcode(self, value): 178 | params = { 179 | "appkey": "4409e2ce8ffd12b8", 180 | "auth_code": value["data"]["auth_code"], 181 | "local_id": "0", 182 | "ts": int(time.time()), 183 | } 184 | params["sign"] = hashlib.md5( 185 | f"{urllib.parse.urlencode(params)}59b43e04ad6965f34319062b478f83dd".encode()).hexdigest() 186 | for i in range(0, 120): 187 | await asyncio.sleep(1) 188 | response = self.__session.post("http://passport.bilibili.com/x/passport-tv-login/qrcode/poll", data=params, 189 | timeout=5) 190 | r = response.json() 191 | if r and r["code"] == 0: 192 | return r 193 | raise "Qrcode timeout" 194 | 195 | def tid_archive(self, cookies): 196 | requests.utils.add_dict_to_cookiejar(self.__session.cookies, cookies) 197 | response = self.__session.get("https://member.bilibili.com/x/vupre/web/archive/pre") 198 | return response.json() 199 | 200 | def login(self, persistence_path, user): 201 | self.persistence_path = persistence_path 202 | if os.path.isfile(persistence_path): 203 | print('使用持久化内容上传') 204 | signal_bus.log_signal.emit('使用持久化内容上传') 205 | self.load() 206 | if user.get('cookies'): 207 | self.cookies = user['cookies'] 208 | if user.get('access_token'): 209 | self.access_token = user['access_token'] 210 | if user.get('account'): 211 | self.account = user['account'] 212 | if self.cookies: 213 | try: 214 | self.login_by_cookies(self.cookies) 215 | except: 216 | print('[error] login error') 217 | signal_bus.log_signal.emit('[error] login error') 218 | self.login_by_password(**self.account) 219 | else: 220 | self.login_by_password(**self.account) 221 | self.store() 222 | 223 | def load(self): 224 | try: 225 | with open(self.persistence_path) as f: 226 | self.cookies = json.load(f) 227 | self.access_token = self.cookies['access_token'] 228 | except (JSONDecodeError, KeyError): 229 | print('[error] 加载cookie出错') 230 | 231 | def store(self): 232 | with open(self.persistence_path, "w") as f: 233 | json.dump({**self.cookies, 234 | 'access_token': self.access_token, 235 | 'refresh_token': self.refresh_token 236 | }, f) 237 | 238 | def send_sms(self, phone_number, country_code): 239 | params = { 240 | "actionKey": "appkey", 241 | "appkey": "783bbb7264451d82", 242 | "build": 6510400, 243 | "channel": "bili", 244 | "cid": country_code, 245 | "device": "phone", 246 | "mobi_app": "android", 247 | "platform": "android", 248 | "tel": phone_number, 249 | "ts": int(time.time()), 250 | } 251 | sign = hashlib.md5(f"{urllib.parse.urlencode(params)}2653583c8873dea268ab9386918b1d65".encode()).hexdigest() 252 | payload = f"{urllib.parse.urlencode(params)}&sign={sign}" 253 | response = self.__session.post("https://passport.bilibili.com/x/passport-login/sms/send", data=payload, 254 | timeout=5) 255 | return response.json() 256 | 257 | def login_by_sms(self, code, params): 258 | params["code"] = code 259 | params["sign"] = hashlib.md5( 260 | f"{urllib.parse.urlencode(params)}59b43e04ad6965f34319062b478f83dd".encode()).hexdigest() 261 | response = self.__session.post("https://passport.bilibili.com/x/passport-login/login/sms", data=params, 262 | timeout=5) 263 | r = response.json() 264 | if r and r["code"] == 0: 265 | return r 266 | 267 | def login_by_password(self, username, password): 268 | print('使用账号上传') 269 | key_hash, pub_key = self.get_key() 270 | encrypt_password = base64.b64encode(rsa.encrypt(f'{key_hash}{password}'.encode(), pub_key)) 271 | payload = { 272 | "actionKey": 'appkey', 273 | "appkey": self.app_key, 274 | "build": 6270200, 275 | "captcha": '', 276 | "challenge": '', 277 | "channel": 'bili', 278 | "device": 'phone', 279 | "mobi_app": 'android', 280 | "password": encrypt_password, 281 | "permission": 'ALL', 282 | "platform": 'android', 283 | "seccode": "", 284 | "subid": 1, 285 | "ts": int(time.time()), 286 | "username": username, 287 | "validate": "", 288 | } 289 | response = self.__session.post("https://passport.bilibili.com/x/passport-login/oauth2/login", timeout=5, 290 | data={**payload, 'sign': self.sign(parse.urlencode(payload))}) 291 | r = response.json() 292 | if r['code'] != 0 or r.get('data') is None or r['data'].get('cookie_info') is None: 293 | raise RuntimeError(r) 294 | try: 295 | for cookie in r['data']['cookie_info']['cookies']: 296 | self.__session.cookies.set(cookie['name'], cookie['value']) 297 | if 'bili_jct' == cookie['name']: 298 | self.__bili_jct = cookie['value'] 299 | self.cookies = self.__session.cookies.get_dict() 300 | self.access_token = r['data']['token_info']['access_token'] 301 | self.refresh_token = r['data']['token_info']['refresh_token'] 302 | except: 303 | raise RuntimeError(r) 304 | return r 305 | 306 | def login_by_cookies(self, cookie): 307 | print('使用cookies上传') 308 | requests.utils.add_dict_to_cookiejar(self.__session.cookies, cookie) 309 | if 'bili_jct' in cookie: 310 | self.__bili_jct = cookie["bili_jct"] 311 | data = self.__session.get("https://api.bilibili.com/x/web-interface/nav", timeout=5).json() 312 | if data["code"] != 0: 313 | raise Exception(data) 314 | 315 | def sign(self, param): 316 | return hashlib.md5(f"{param}{self.appsec}".encode()).hexdigest() 317 | 318 | def get_key(self): 319 | url = "https://passport.bilibili.com/x/passport-login/web/key" 320 | payload = { 321 | 'appkey': f'{self.app_key}', 322 | 'sign': self.sign(f"appkey={self.app_key}"), 323 | } 324 | response = self.__session.get(url, data=payload, timeout=5) 325 | r = response.json() 326 | if r and r["code"] == 0: 327 | return r['data']['hash'], rsa.PublicKey.load_pkcs1_openssl_pem(r['data']['key'].encode()) 328 | 329 | def probe(self): 330 | ret = self.__session.get('https://member.bilibili.com/preupload?r=probe', timeout=5).json() 331 | print(f"[info] 线路:{ret['lines']}") 332 | data, auto_os = None, None 333 | min_cost = 0 334 | if ret['probe'].get('get'): 335 | method = 'get' 336 | else: 337 | method = 'post' 338 | data = bytes(int(1024 * 0.1 * 1024)) 339 | for line in ret['lines']: 340 | start = time.perf_counter() 341 | test = self.__session.request(method, f"https:{line['probe_url']}", data=data, timeout=30) 342 | cost = time.perf_counter() - start 343 | print(line['query'], cost) 344 | if test.status_code != 200: 345 | return 346 | if not min_cost or min_cost > cost: 347 | auto_os = line 348 | min_cost = cost 349 | auto_os['cost'] = min_cost 350 | return auto_os 351 | 352 | def upload_file(self, filepath: str, part_name: str, lines='AUTO', tasks=3): 353 | """上传本地视频文件,返回视频信息dict 354 | b站目前支持4种上传线路upos, kodo, gcs, bos 355 | gcs: {"os":"gcs","query":"bucket=bvcupcdngcsus&probe_version=20221109", 356 | "probe_url":"//storage.googleapis.com/bvcupcdngcsus/OK"}, 357 | bos: {"os":"bos","query":"bucket=bvcupcdnboshb&probe_version=20221109", 358 | "probe_url":"??"} 359 | """ 360 | preferred_upos_cdn = None 361 | if not self._auto_os: 362 | if lines == 'kodo': 363 | self._auto_os = {"os": "kodo", "query": "bucket=bvcupcdnkodobm&probe_version=20221109", 364 | "probe_url": "//up-na0.qbox.me/crossdomain.xml"} 365 | elif lines == 'bda2': 366 | self._auto_os = {"os": "upos", "query": "upcdn=bda2&probe_version=20221109", 367 | "probe_url": "//upos-sz-upcdnbda2.bilivideo.com/OK"} 368 | preferred_upos_cdn = 'bda2' 369 | elif lines == 'cs-bda2': 370 | self._auto_os = {"os": "upos", "query": "upcdn=bda2&probe_version=20221109", 371 | "probe_url": "//upos-cs-upcdnbda2.bilivideo.com/OK"} 372 | preferred_upos_cdn = 'bda2' 373 | elif lines == 'ws': 374 | self._auto_os = {"os": "upos", "query": "upcdn=ws&probe_version=20221109", 375 | "probe_url": "//upos-sz-upcdnws.bilivideo.com/OK"} 376 | preferred_upos_cdn = 'ws' 377 | elif lines == 'qn': 378 | self._auto_os = {"os": "upos", "query": "upcdn=qn&probe_version=20221109", 379 | "probe_url": "//upos-sz-upcdnqn.bilivideo.com/OK"} 380 | preferred_upos_cdn = 'qn' 381 | elif lines == 'cs-qn': 382 | self._auto_os = {"os": "upos", "query": "upcdn=qn&probe_version=20221109", 383 | "probe_url": "//upos-cs-upcdnqn.bilivideo.com/OK"} 384 | preferred_upos_cdn = 'qn' 385 | elif lines == 'cos': 386 | self._auto_os = {"os": "cos", "query": "", 387 | "probe_url": ""} 388 | elif lines == 'cos-internal': 389 | self._auto_os = {"os": "cos-internal", "query": "", 390 | "probe_url": ""} 391 | else: 392 | self._auto_os = self.probe() 393 | print( 394 | f"[info] 线路选择 => {self._auto_os['os']}: {self._auto_os['query']}. time: {self._auto_os.get('cost')}") 395 | if self._auto_os['os'] == 'upos': 396 | upload = self.upos 397 | elif self._auto_os['os'] == 'cos': 398 | upload = self.cos 399 | elif self._auto_os['os'] == 'cos-internal': 400 | upload = lambda *args, **kwargs: self.cos(*args, **kwargs, internal=True) 401 | elif self._auto_os['os'] == 'kodo': 402 | upload = self.kodo 403 | else: 404 | print(f"[error] NoSearch:{self._auto_os['os']}") 405 | signal_bus.log_signal.emit(f"[error] NoSearch:{self._auto_os['os']}") 406 | raise NotImplementedError(self._auto_os['os']) 407 | print(f"[info] os: {self._auto_os['os']}") 408 | signal_bus.log_signal.emit(f"[info] os: {self._auto_os['os']}") 409 | total_size = os.path.getsize(filepath) 410 | with open(filepath, 'rb') as f: 411 | query = { 412 | 'r': self._auto_os['os'] if self._auto_os['os'] != 'cos-internal' else 'cos', 413 | 'profile': 'ugcupos/bup' if 'upos' == self._auto_os['os'] else "ugcupos/bupfetch", 414 | 'ssl': 0, 415 | 'version': '2.8.12', 416 | 'build': 2081200, 417 | 'name': part_name, 418 | 'size': total_size, 419 | } 420 | resp = self.__session.get( 421 | f"https://member.bilibili.com/preupload?{self._auto_os['query']}", params=query, 422 | timeout=5) 423 | ret = resp.json() 424 | print(f"preupload: {ret}") 425 | if preferred_upos_cdn: 426 | original_endpoint: str = ret['endpoint'] 427 | if re.match(r'//upos-(sz|cs)-upcdn(bda2|ws|qn)\.bilivideo\.com', original_endpoint): 428 | if re.match(r'bda2|qn|ws', preferred_upos_cdn): 429 | print(f"[debug] Preferred UpOS CDN: {preferred_upos_cdn}") 430 | new_endpoint = re.sub(r'upcdn(bda2|qn|ws)', f'upcdn{preferred_upos_cdn}', original_endpoint) 431 | print(f"[debug] {original_endpoint} => {new_endpoint}") 432 | ret['endpoint'] = new_endpoint 433 | else: 434 | print(f"Unrecognized preferred_upos_cdn: {preferred_upos_cdn}") 435 | else: 436 | print( 437 | f"Assigned UpOS endpoint {original_endpoint} was never seen before, something else might have changed, so will not modify it") 438 | return asyncio.run(upload(f, total_size, ret, tasks=tasks)) 439 | 440 | async def cos(self, file, name, total_size, ret, chunk_size=10485760, tasks=3, internal=False): 441 | filename = name 442 | url = ret["url"] 443 | if internal: 444 | url = url.replace("cos.accelerate", "cos-internal.ap-shanghai") 445 | biz_id = ret["biz_id"] 446 | post_headers = { 447 | "Authorization": ret["post_auth"], 448 | } 449 | put_headers = { 450 | "Authorization": ret["put_auth"], 451 | } 452 | 453 | initiate_multipart_upload_result = ET.fromstring(self.__session.post(f'{url}?uploads&output=json', timeout=5, 454 | headers=post_headers).content) 455 | upload_id = initiate_multipart_upload_result.find('UploadId').text 456 | # 开始上传 457 | parts = [] # 分块信息 458 | chunks = math.ceil(total_size / chunk_size) # 获取分块数量 459 | 460 | async def upload_chunk(session, chunks_data, params): 461 | async with session.put(url, params=params, raise_for_status=True, 462 | data=chunks_data, headers=put_headers) as r: 463 | end = time.perf_counter() - start 464 | parts.append({"Part": {"PartNumber": params['chunk'] + 1, "ETag": r.headers['Etag']}}) 465 | sys.stdout.write(f"\r{params['end'] / 1000 / 1000 / end:.2f}MB/s " 466 | f"=> {params['partNumber'] / chunks:.1%}") 467 | 468 | start = time.perf_counter() 469 | await self._upload({ 470 | 'uploadId': upload_id, 471 | 'chunks': chunks, 472 | 'total': total_size 473 | }, file, chunk_size, upload_chunk, tasks=tasks) 474 | cost = time.perf_counter() - start 475 | fetch_headers = { 476 | "X-Upos-Fetch-Source": ret["fetch_headers"]["X-Upos-Fetch-Source"], 477 | "X-Upos-Auth": ret["fetch_headers"]["X-Upos-Auth"], 478 | "Fetch-Header-Authorization": ret["fetch_headers"]["Fetch-Header-Authorization"] 479 | } 480 | parts = sorted(parts, key=lambda x: x['Part']['PartNumber']) 481 | complete_multipart_upload = ET.Element('CompleteMultipartUpload') 482 | for part in parts: 483 | part_et = ET.SubElement(complete_multipart_upload, 'Part') 484 | part_number = ET.SubElement(part_et, 'PartNumber') 485 | part_number.text = str(part['Part']['PartNumber']) 486 | e_tag = ET.SubElement(part_et, 'ETag') 487 | e_tag.text = part['Part']['ETag'] 488 | xml = ET.tostring(complete_multipart_upload) 489 | ii = 0 490 | while ii <= 3: 491 | try: 492 | res = self.__session.post(url, params={'uploadId': upload_id}, data=xml, headers=post_headers, 493 | timeout=15) 494 | if res.status_code == 200: 495 | break 496 | raise IOError(res.text) 497 | except IOError: 498 | ii += 1 499 | print("[info] 请求合并分片出现问题,尝试重连,次数:" + str(ii)) 500 | signal_bus.log_signal.emit("[info] 请求合并分片出现问题,尝试重连,次数:" + str(ii)) 501 | time.sleep(15) 502 | ii = 0 503 | while ii <= 3: 504 | try: 505 | res = self.__session.post("https:" + ret["fetch_url"], headers=fetch_headers, timeout=15).json() 506 | if res.get('OK') == 1: 507 | print(f'[info] {filename} uploaded >> {total_size / 1000 / 1000 / cost:.2f}MB/s. {res}') 508 | signal_bus.log_signal.emit( 509 | f'[info] {filename} uploaded >> {total_size / 1000 / 1000 / cost:.2f}MB/s. {res}') 510 | return {"title": splitext(filename)[0], "filename": ret["bili_filename"], "desc": ""} 511 | raise IOError(res) 512 | except IOError: 513 | ii += 1 514 | print("[info] 上传出现问题,尝试重连,次数:" + str(ii)) 515 | signal_bus.log_signal.emit("[info] 上传出现问题,尝试重连,次数:" + str(ii)) 516 | time.sleep(15) 517 | 518 | async def kodo(self, file, name, total_size, ret, chunk_size=4194304, tasks=3): 519 | filename = name 520 | bili_filename = ret['bili_filename'] 521 | key = ret['key'] 522 | endpoint = f"https:{ret['endpoint']}" 523 | token = ret['uptoken'] 524 | fetch_url = ret['fetch_url'] 525 | fetch_headers = ret['fetch_headers'] 526 | url = f'{endpoint}/mkblk' 527 | headers = { 528 | 'Authorization': f"UpToken {token}", 529 | } 530 | # 开始上传 531 | parts = [] # 分块信息 532 | chunks = math.ceil(total_size / chunk_size) # 获取分块数量 533 | 534 | async def upload_chunk(session, chunks_data, params): 535 | async with session.post(f'{url}/{len(chunks_data)}', 536 | data=chunks_data, headers=headers) as response: 537 | end = time.perf_counter() - start 538 | ctx = await response.json() 539 | parts.append({"index": params['chunk'], "ctx": ctx['ctx']}) 540 | sys.stdout.write(f"\r{params['end'] / 1000 / 1000 / end:.2f}MB/s " 541 | f"=> {params['partNumber'] / chunks:.1%}") 542 | 543 | start = time.perf_counter() 544 | await self._upload({}, file, chunk_size, upload_chunk, tasks=tasks) 545 | cost = time.perf_counter() - start 546 | 547 | print(f'[info] {filename} uploaded >> {total_size / 1000 / 1000 / cost:.2f}MB/s') 548 | signal_bus.log_signal.emit(f'[info] {filename} uploaded >> {total_size / 1000 / 1000 / cost:.2f}MB/s') 549 | parts.sort(key=lambda x: x['index']) 550 | self.__session.post(f"{endpoint}/mkfile/{total_size}/key/{base64.urlsafe_b64encode(key.encode()).decode()}", 551 | data=','.join(map(lambda x: x['ctx'], parts)), headers=headers, timeout=10) 552 | r = self.__session.post(f"https:{fetch_url}", headers=fetch_headers, timeout=5).json() 553 | if r["OK"] != 1: 554 | raise Exception(r) 555 | return {"title": splitext(filename)[0], "filename": bili_filename, "desc": ""} 556 | 557 | async def upos(self, file, name, total_size, ret, tasks=3): 558 | filename = name 559 | chunk_size = ret['chunk_size'] 560 | auth = ret["auth"] 561 | endpoint = ret["endpoint"] 562 | biz_id = ret["biz_id"] 563 | upos_uri = ret["upos_uri"] 564 | url = f"https:{endpoint}/{upos_uri.replace('upos://', '')}" # 视频上传路径 565 | headers = { 566 | "X-Upos-Auth": auth 567 | } 568 | # 向上传地址申请上传,得到上传id等信息 569 | upload_id = self.__session.post(f'{url}?uploads&output=json', timeout=15, 570 | headers=headers).json()["upload_id"] 571 | # 开始上传 572 | parts = [] # 分块信息 573 | chunks = math.ceil(total_size / chunk_size) # 获取分块数量 574 | 575 | async def upload_chunk(session, chunks_data, params): 576 | async with session.put(url, params=params, raise_for_status=True, 577 | data=chunks_data, headers=headers): 578 | end = time.perf_counter() - start 579 | parts.append({"partNumber": params['chunk'] + 1, "eTag": "etag"}) 580 | sys.stdout.write(f"\r{params['end'] / 1000 / 1000 / end:.2f}MB/s " 581 | f"=> {params['partNumber'] / chunks:.1%}") 582 | signal_bus.log_signal.emit(f"\r{params['end'] / 1000 / 1000 / end:.2f}MB/s " 583 | f"=> {params['partNumber'] / chunks:.1%}") 584 | 585 | start = time.perf_counter() 586 | await self._upload({ 587 | 'uploadId': upload_id, 588 | 'chunks': chunks, 589 | 'total': total_size 590 | }, file, chunk_size, upload_chunk, tasks=tasks) 591 | cost = time.perf_counter() - start 592 | p = { 593 | 'name': filename, 594 | 'uploadId': upload_id, 595 | 'biz_id': biz_id, 596 | 'output': 'json', 597 | 'profile': 'ugcupos/bup' 598 | } 599 | attempt = 0 600 | while attempt <= 5: # 一旦放弃就会丢失前面所有的进度,多试几次吧 601 | try: 602 | r = self.__session.post(url, params=p, json={"parts": parts}, headers=headers, timeout=15).json() 603 | if r.get('OK') == 1: 604 | print(f'[info] {filename} uploaded >> {total_size / 1000 / 1000 / cost:.2f}MB/s. {r}') 605 | signal_bus.log_signal.emit( 606 | f'[info] {filename} uploaded >> {total_size / 1000 / 1000 / cost:.2f}MB/s. {r}') 607 | return {"title": splitext(filename)[0], "filename": splitext(basename(upos_uri))[0], "desc": ""} 608 | raise IOError(r) 609 | except IOError: 610 | attempt += 1 611 | print(f"请求合并分片时出现问题,尝试重连,次数:" + str(attempt)) 612 | signal_bus.log_signal.emit(f"请求合并分片时出现问题,尝试重连,次数:{attempt}") 613 | time.sleep(15) 614 | 615 | @staticmethod 616 | async def _upload(params, file, chunk_size, afunc, tasks=3): 617 | params['chunk'] = -1 618 | 619 | async def upload_chunk(): 620 | while True: 621 | chunks_data = file.read(chunk_size) 622 | if not chunks_data: 623 | return 624 | params['chunk'] += 1 625 | params['size'] = len(chunks_data) 626 | params['partNumber'] = params['chunk'] + 1 627 | params['start'] = params['chunk'] * chunk_size 628 | params['end'] = params['start'] + params['size'] 629 | clone = params.copy() 630 | for i in range(10): 631 | try: 632 | await afunc(session, chunks_data, clone) 633 | break 634 | except (asyncio.TimeoutError, ClientError) as e: 635 | print(f"[error] retry chunk{clone['chunk']} >> {i + 1}. {e}") 636 | signal_bus.log_signal.emit(f"[error] retry chunk{clone['chunk']} >> {i + 1}. {e}") 637 | 638 | async with ClientSession() as session: 639 | await asyncio.gather(*[upload_chunk() for _ in range(tasks)]) 640 | 641 | def submit(self, submit_api=None): 642 | if not self.video.title: 643 | self.video.title = self.video.videos[0]["title"] 644 | self.__session.get('https://member.bilibili.com/x/geetest/pre/add', timeout=5) 645 | 646 | if submit_api is None: 647 | total_info = self.__session.get('http://api.bilibili.com/x/space/myinfo', timeout=15).json() 648 | if total_info.get('data') is None: 649 | print(f'[error] {total_info}') 650 | total_info = total_info.get('data') 651 | if total_info['level'] > 3 and total_info['follower'] > 1000: 652 | user_weight = 2 653 | else: 654 | user_weight = 1 655 | print(f'[info] 用户权重: {user_weight}') 656 | submit_api = 'web' if user_weight == 2 else 'client' 657 | ret = None 658 | if submit_api == 'web': 659 | ret = self.submit_web() 660 | if ret["code"] == 21138: 661 | print(f'[info] 改用客户端接口提交{ret}') 662 | submit_api = 'client' 663 | if submit_api == 'client': 664 | ret = self.submit_client() 665 | if not ret: 666 | raise Exception(f'不存在的选项:{submit_api}') 667 | if ret["code"] == 0: 668 | return ret 669 | else: 670 | raise Exception(ret) 671 | 672 | def submit_web(self): 673 | print('[info] 使用网页端api提交') 674 | return self.__session.post(f'https://member.bilibili.com/x/vu/web/add?csrf={self.__bili_jct}', timeout=5, 675 | json=asdict(self.video)).json() 676 | 677 | def submit_client(self): 678 | print('[info] 使用客户端api端提交') 679 | signal_bus.log_signal.emit('[info] 使用客户端api端提交') 680 | if not self.access_token: 681 | if self.account is None: 682 | raise RuntimeError("Access token is required, but account and access_token does not exist!") 683 | self.login_by_password(**self.account) 684 | self.store() 685 | while True: 686 | ret = self.__session.post(f'http://member.bilibili.com/x/vu/client/add?access_key={self.access_token}', 687 | timeout=5, json=asdict(self.video)).json() 688 | if ret['code'] == -101: 689 | print(f'[info] 刷新token{ret}') 690 | signal_bus.log_signal.emit(f'[info] 刷新token{ret}') 691 | self.login_by_password(**self.account) 692 | self.store() 693 | continue 694 | return ret 695 | 696 | def cover_up(self, img: str): 697 | """ 698 | :param img: img path or stream 699 | :return: img URL 700 | """ 701 | from PIL import Image 702 | from io import BytesIO 703 | 704 | with Image.open(img) as im: 705 | # 宽和高,需要16:10 706 | xsize, ysize = im.size 707 | if xsize / ysize > 1.6: 708 | delta = xsize - ysize * 1.6 709 | region = im.crop((delta / 2, 0, xsize - delta / 2, ysize)) 710 | else: 711 | delta = ysize - xsize * 10 / 16 712 | region = im.crop((0, delta / 2, xsize, ysize - delta / 2)) 713 | buffered = BytesIO() 714 | region.save(buffered, format=im.format) 715 | r = self.__session.post( 716 | url='https://member.bilibili.com/x/vu/web/cover/up', 717 | data={ 718 | 'cover': b'data:image/jpeg;base64,' + (base64.b64encode(buffered.getvalue())), 719 | 'csrf': self.__bili_jct 720 | }, timeout=30 721 | ) 722 | buffered.close() 723 | res = r.json() 724 | if res.get('data') is None: 725 | raise Exception(res) 726 | return res['data']['url'] 727 | 728 | def get_tags(self, upvideo, typeid="", desc="", cover="", groupid=1, vfea=""): 729 | """ 730 | 上传视频后获得推荐标签 731 | :param vfea: 732 | :param groupid: 733 | :param typeid: 734 | :param desc: 735 | :param cover: 736 | :param upvideo: 737 | :return: 返回官方推荐的tag 738 | """ 739 | url = f'https://member.bilibili.com/x/web/archive/tags?' \ 740 | f'typeid={typeid}&title={quote(upvideo["title"])}&filename=filename&desc={desc}&cover={cover}' \ 741 | f'&groupid={groupid}&vfea={vfea}' 742 | return self.__session.get(url=url, timeout=5).json() 743 | 744 | def __enter__(self): 745 | return self 746 | 747 | def __exit__(self, e_t, e_v, t_b): 748 | self.close() 749 | 750 | def close(self): 751 | """Closes all adapters and as such the session""" 752 | self.__session.close() 753 | 754 | 755 | @dataclass 756 | class Data: 757 | """ 758 | cover: 封面图片,可由recovers方法得到视频的帧截图 759 | """ 760 | copyright: int = 2 761 | source: str = '' 762 | tid: int = 21 763 | cover: str = '' 764 | title: str = '' 765 | desc_format_id: int = 0 766 | desc: str = '' 767 | dynamic: str = '' 768 | subtitle: dict = field(init=False) 769 | tag: Union[list, str] = '' 770 | videos: list = field(default_factory=list) 771 | dtime: Any = None 772 | open_subtitle: InitVar[bool] = False 773 | 774 | def __post_init__(self, open_subtitle): 775 | self.subtitle = {"open": int(open_subtitle), "lan": ""} 776 | if self.dtime and self.dtime - int(time.time()) <= 14400: 777 | self.dtime = None 778 | if isinstance(self.tag, list): 779 | self.tag = ','.join(self.tag) 780 | 781 | def delay_time(self, dtime: int): 782 | """设置延时发布时间,距离提交大于2小时,格式为10位时间戳""" 783 | if dtime - int(time.time()) > 7200: 784 | self.dtime = dtime 785 | 786 | def set_tag(self, tag: list): 787 | """设置标签,tag为数组""" 788 | self.tag = ','.join(tag) 789 | 790 | def append(self, video): 791 | self.videos.append(video) 792 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.5 2 | google_api_python_client==2.97.0 3 | Pillow==10.0.0 4 | PyQt5==5.15.9 5 | PyQt5_Frameless_Window==0.3.2 6 | PyQt_Fluent_Widgets==1.2.0 7 | requests==2.31.0 8 | rsa~=4.9 9 | yt_dlp==2023.7.6 10 | httplib2~=0.22.0 11 | PySocks==1.7.1 12 | pysubs2==1.6.1 -------------------------------------------------------------------------------- /res/LICENCE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LICENCE 731 | 732 |
733 |

本软件是红石科技搬运组下载及视频管理工具 734 | 仅供学习、参考使用

关于

版本:6.0.0

作者:Aye10032

下载地址:https://github.com/Aye10032/YouTubeDownLoader

使用依赖

 

建议安装的第三方库

 

735 | 736 | -------------------------------------------------------------------------------- /res/LICENCE.md: -------------------------------------------------------------------------------- 1 | 本软件是红石科技搬运组下载及视频管理工具 2 | 仅供学习、参考使用 3 | 4 | ## 关于 5 | 版本:6.0.0 6 | 7 | 作者:[Aye10032](https://github.com/Aye10032) 8 | 9 | 下载地址:[https://github.com/Aye10032/YouTubeDownLoader](https://github.com/Aye10032/YouTubeDownLoader) 10 | 11 | ## 使用依赖 12 | 13 | - [yt_dlp](https://github.com/yt-dlp/yt-dlp) 14 | - [PyQt-Fluent-Widgets](https://github.com/zhiyiYo/PyQt-Fluent-Widgets) 15 | - [biliup](https://github.com/biliup/biliup) 16 | 17 | 18 | 19 | ## 建议安装的第三方库 20 | 21 | - ffmpeg 22 | - PhantomJS -------------------------------------------------------------------------------- /res/icons/key_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /res/icons/key_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /res/icons/link_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /res/icons/link_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /res/icons/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aye10032/YouTubeDownLoader/598af4be68b94a1f9b1b60174221adf6d38d9667/res/icons/logo.ico -------------------------------------------------------------------------------- /res/icons/number_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /res/icons/number_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /res/icons/play_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /res/icons/play_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /res/icons/server_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /res/icons/server_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /res/lang/zh_CN.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aye10032/YouTubeDownLoader/598af4be68b94a1f9b1b60174221adf6d38d9667/res/lang/zh_CN.qm -------------------------------------------------------------------------------- /res/lang/zh_CN.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ChannelDialog 6 | 7 | 8 | Channel Name: 9 | 频道名称: 10 | 11 | 12 | 13 | Channel ID: 14 | 频道ID: 15 | 16 | 17 | 18 | DistListSettingCard 19 | 20 | 21 | Add 22 | 添加 23 | 24 | 25 | 26 | Are you sure you want to delete the channel? 27 | 你确定要删除这个频道吗? 28 | 29 | 30 | 31 | If you delete the 32 | 如果你删除了 33 | 34 | 35 | 36 | channel and remove it from the list, the channel will no longer appear in the list 37 | 频道,那么它的相关信息将不再出现在列表中 38 | 39 | 40 | 41 | DownloadInterface 42 | 43 | 44 | Download Video 45 | 下载视频 46 | 47 | 48 | 49 | Origin Link 50 | 源链接 51 | 52 | 53 | 54 | Auto Quality 55 | 默认质量 56 | 57 | 58 | 59 | Quality 60 | 视频质量 61 | 62 | 63 | 64 | Get Info 65 | 下载简介 66 | 67 | 68 | 69 | Video Title 70 | 标题 71 | 72 | 73 | 74 | Reprinter Info 75 | 搬运信息 76 | 77 | 78 | 79 | On 80 | 81 | 82 | 83 | 84 | Off 85 | 86 | 87 | 88 | 89 | save download information 90 | 保存视频下载信息 91 | 92 | 93 | 94 | open download folder 95 | 打开下载文件夹 96 | 97 | 98 | 99 | play download video 100 | 用默认播放器打开下载视频 101 | 102 | 103 | 104 | open youtube link 105 | 打开源链接 106 | 107 | 108 | 109 | copy description 110 | 复制简介 111 | 112 | 113 | 114 | turn to upload page 115 | 转到上传界面 116 | 117 | 118 | 119 | auto quality is enabled, you can start downloading the video directly 120 | 当前使用默认质量,请直接下载视频 121 | 122 | 123 | 124 | you should choose quality first 125 | 请先选择下载质量 126 | 127 | 128 | 129 | description download complete 130 | 简介下载完毕 131 | 132 | 133 | 134 | download complete 135 | 下载完毕 136 | 137 | 138 | 139 | quality configure complete, now you can start download 140 | 下载质量设置完毕,请开始下载 141 | 142 | 143 | 144 | the content of the title has been copied 145 | 标题内容已复制 146 | 147 | 148 | 149 | the content of the reprint has been copied 150 | 转载信息已复制 151 | 152 | 153 | 154 | you haven't downloaded any videos yet 155 | 你还没有下载任何视频 156 | 157 | 158 | 159 | video information is saved 160 | 视频信息已保存 161 | 162 | 163 | 164 | you haven't entered a video link yet 165 | 你还没有输入源链接 166 | 167 | 168 | 169 | the content of the description has been copied 170 | 简介内容已复制 171 | 172 | 173 | 174 | Video Download 175 | 视频下载 176 | 177 | 178 | 179 | LocalVideoInterface 180 | 181 | 182 | Download List 183 | 下载历史 184 | 185 | 186 | 187 | SettingInterface 188 | 189 | 190 | Settings 191 | 设置 192 | 193 | 194 | 195 | Download Setting 196 | 下载设置 197 | 198 | 199 | 200 | Edit 201 | 编辑 202 | 203 | 204 | 205 | Reprinter ID 206 | 搬运者ID 207 | 208 | 209 | 210 | Enable Proxy 211 | 使用代理 212 | 213 | 214 | 215 | Whether to enable web proxy 216 | 是否使用网络代理 217 | 218 | 219 | 220 | Proxy Setting 221 | 代理设置 222 | 223 | 224 | 225 | Number of threads 226 | 下载线程 227 | 228 | 229 | 230 | Choose folder 231 | 选择文件夹 232 | 233 | 234 | 235 | Download directory 236 | 下载文件夹 237 | 238 | 239 | 240 | System Setting 241 | 系统设置 242 | 243 | 244 | 245 | Application theme 246 | 主题 247 | 248 | 249 | 250 | Change the appearance of your application 251 | 改变应用程序外观 252 | 253 | 254 | 255 | Light 256 | 257 | 258 | 259 | 260 | Dark 261 | 262 | 263 | 264 | 265 | Use system setting 266 | 跟随系统设置 267 | 268 | 269 | 270 | Theme color 271 | 主题颜色 272 | 273 | 274 | 275 | Change the theme color of you application 276 | 设置应用程序的主题颜色 277 | 278 | 279 | 280 | Language 281 | 语言 282 | 283 | 284 | 285 | Set your preferred language for UI 286 | 设置应用程序语言 287 | 288 | 289 | 290 | Configuration takes effect after restart 291 | 292 | 293 | 294 | 295 | Nick Name 296 | ID 297 | 298 | 299 | 300 | please input your nick name: 301 | 请输入你的ID: 302 | 303 | 304 | 305 | manual proxy configuration: 306 | 代理设置: 307 | 308 | 309 | 310 | Advanced setting 311 | 高级设置 312 | 313 | 314 | 315 | Google Api Token 316 | Google 开发者密钥 317 | 318 | 319 | 320 | Subscribe Channels 321 | 订阅频道列表 322 | 323 | 324 | 325 | Api Server 326 | API服务器 327 | 328 | 329 | 330 | API Token Setting 331 | 密钥设置 332 | 333 | 334 | 335 | set your google develop token: 336 | 设置你的google develop token: 337 | 338 | 339 | 340 | API Server 341 | API服务器 342 | 343 | 344 | 345 | manual your server address: 346 | 输入api服务器地址: 347 | 348 | 349 | 350 | SubscribeInterface 351 | 352 | 353 | Subscribe List 354 | 订阅列表 355 | 356 | 357 | 358 | TodoListInterface 359 | 360 | 361 | TODO List 362 | 待搬运列表 363 | 364 | 365 | 366 | UploadInterface 367 | 368 | 369 | Upload 370 | 上传 371 | 372 | 373 | 374 | Video Title 375 | 标题 376 | 377 | 378 | 379 | Cover 380 | 封面 381 | 382 | 383 | 384 | Video 385 | 视频 386 | 387 | 388 | 389 | Reprint Info 390 | 来源 391 | 392 | 393 | 394 | Tag 395 | 标签 396 | 397 | 398 | 399 | Description 400 | 简介 401 | 402 | 403 | 404 | no cookies found 405 | 未找到cookie 406 | 407 | 408 | 409 | Source 410 | 来源 411 | 412 | 413 | 414 | Dynamic 415 | 动态 416 | 417 | 418 | 419 | no videos, plead add video first! 420 | 没有视频,请先添加分P! 421 | 422 | 423 | 424 | Window 425 | 426 | 427 | Local Video 428 | 下载列表 429 | 430 | 431 | 432 | Subscription Information 433 | 订阅信息 434 | 435 | 436 | 437 | TODO List 438 | 待搬运列表 439 | 440 | 441 | 442 | Info 443 | 关于 444 | 445 | 446 | 447 | Setting 448 | 设置 449 | 450 | 451 | 452 | Download 453 | 视频下载 454 | 455 | 456 | 457 | Upload 458 | 视频上传 459 | 460 | 461 | 462 | No API Token! 463 | 未设置Google密钥! 464 | 465 | 466 | 467 | You haven't set your token yet, please go to the settings screen to set it first 468 | 你还没有设置开发者密钥,请先前往设置界面设置 469 | 470 | 471 | 472 | No API Server! 473 | 未设置API服务器! 474 | 475 | 476 | 477 | You haven't set api server yet, please go to the settings screen to set it first 478 | 你还没有设置API服务器地址,请前往设置界面进行设置 479 | 480 | 481 | 482 | -------------------------------------------------------------------------------- /res/qss/dark/download_interface.qss: -------------------------------------------------------------------------------- 1 | #Title{ 2 | font-size: 24px; 3 | font-family: 'Segoe UI', 'Microsoft YaHei'; 4 | color: white; 5 | } 6 | 7 | #Text{ 8 | font-size: 14px; 9 | font-family: 'Segoe UI', 'Microsoft YaHei'; 10 | color: white; 11 | } 12 | 13 | LineEdit{ 14 | border: 1px solid white; 15 | } 16 | 17 | TextEdit#desc{ 18 | font-size: 12px; 19 | font-family: 'Segoe UI', 'Microsoft YaHei'; 20 | color: white; 21 | } 22 | 23 | DownloadInterface{ 24 | border: 1px solid rgb(36, 36, 36); 25 | border-right: none; 26 | border-bottom: none; 27 | border-top-left-radius: 10px; 28 | background-color: rgb(51, 51, 51); 29 | } -------------------------------------------------------------------------------- /res/qss/dark/main.qss: -------------------------------------------------------------------------------- 1 | Widget { 2 | border: 1px solid rgb(29, 29, 29); 3 | border-right: none; 4 | border-bottom: none; 5 | border-top-left-radius: 10px; 6 | background-color: rgb(39, 39, 39); 7 | } 8 | 9 | QStackedWidget { 10 | border: 1px solid rgb(29, 29, 29); 11 | border-right: none; 12 | border-bottom: none; 13 | border-top-left-radius: 10px; 14 | background-color: rgb(39, 39, 39); 15 | } 16 | 17 | Window { 18 | background-color: rgb(32, 32, 32); 19 | } 20 | 21 | MinimizeButton { 22 | qproperty-normalColor: white; 23 | qproperty-normalBackgroundColor: transparent; 24 | qproperty-hoverColor: white; 25 | qproperty-hoverBackgroundColor: rgba(255, 255, 255, 26); 26 | qproperty-pressedColor: white; 27 | qproperty-pressedBackgroundColor: rgba(255, 255, 255, 51) 28 | } 29 | 30 | 31 | MaximizeButton { 32 | qproperty-normalColor: white; 33 | qproperty-normalBackgroundColor: transparent; 34 | qproperty-hoverColor: white; 35 | qproperty-hoverBackgroundColor: rgba(255, 255, 255, 26); 36 | qproperty-pressedColor: white; 37 | qproperty-pressedBackgroundColor: rgba(255, 255, 255, 51) 38 | } 39 | 40 | CloseButton { 41 | qproperty-normalColor: white; 42 | qproperty-normalBackgroundColor: transparent; 43 | } 44 | 45 | StandardTitleBar > QLabel { 46 | color: white; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /res/qss/dark/scroll_interface.qss: -------------------------------------------------------------------------------- 1 | LocalVideoInterface,SettingInterface,SubscribeInterface,TodoListInterface,InfoInterface{ 2 | border: 1px solid rgb(36, 36, 36); 3 | border-right: none; 4 | border-bottom: none; 5 | border-top-left-radius: 10px; 6 | background-color: rgb(51, 51, 51); 7 | } 8 | 9 | ScrollArea { 10 | background-color: rgb(39, 39, 39); 11 | border: none; 12 | } 13 | 14 | QWidget#ScrollWidget{ 15 | background-color: rgb(39, 39, 39); 16 | } 17 | 18 | QLabel#Title{ 19 | font-size: 24px; 20 | font-family: 'Segoe UI', 'Microsoft YaHei'; 21 | color: white; 22 | } 23 | 24 | QScrollBar { 25 | background: transparent; 26 | width: 4px; 27 | margin-top: 32px; 28 | margin-bottom: 0; 29 | padding-right: 2px; 30 | } 31 | 32 | QScrollBar::sub-line { 33 | background: transparent; 34 | } 35 | 36 | QScrollBar::add-line { 37 | background: transparent; 38 | } 39 | 40 | QScrollBar::handle { 41 | background: rgb(122, 122, 122); 42 | border: 2px solid rgb(128, 128, 128); 43 | border-radius: 1px; 44 | min-height: 32px; 45 | } 46 | 47 | QScrollBar::add-page:vertical, 48 | QScrollBar::sub-page:vertical { 49 | background: none; 50 | } -------------------------------------------------------------------------------- /res/qss/dark/upload_interface.qss: -------------------------------------------------------------------------------- 1 | UploadInterface { 2 | border: 1px solid rgb(36, 36, 36); 3 | border-right: none; 4 | border-bottom: none; 5 | border-top-left-radius: 10px; 6 | background-color: rgb(51, 51, 51); 7 | } 8 | 9 | #Title{ 10 | font-size: 24px; 11 | font-family: 'Segoe UI', 'Microsoft YaHei'; 12 | color: white; 13 | } 14 | 15 | #Text{ 16 | font-size: 14px; 17 | font-family: 'Segoe UI', 'Microsoft YaHei'; 18 | color: white; 19 | } 20 | 21 | #content { 22 | color: rgb(208, 208, 208); 23 | font-size: 12px; 24 | font-family: 'Segoe UI', 'Microsoft YaHei'; 25 | } 26 | 27 | LineEdit{ 28 | border: 1px solid white; 29 | } 30 | 31 | #desc{ 32 | font-size: 12px; 33 | font-family: 'Segoe UI', 'Microsoft YaHei'; 34 | } 35 | 36 | ScrollArea { 37 | background-color: rgb(39, 39, 39); 38 | border: none; 39 | } 40 | 41 | QWidget#ScrollWidget{ 42 | background-color: rgb(39, 39, 39); 43 | } 44 | 45 | QScrollBar { 46 | background: transparent; 47 | width: 4px; 48 | margin-top: 32px; 49 | margin-bottom: 0; 50 | padding-right: 2px; 51 | } 52 | 53 | QScrollBar::sub-line { 54 | background: transparent; 55 | } 56 | 57 | QScrollBar::add-line { 58 | background: transparent; 59 | } 60 | 61 | QScrollBar::handle { 62 | background: rgb(122, 122, 122); 63 | border: 2px solid rgb(128, 128, 128); 64 | border-radius: 1px; 65 | min-height: 32px; 66 | } 67 | 68 | QScrollBar::add-page:vertical, 69 | QScrollBar::sub-page:vertical { 70 | background: none; 71 | } -------------------------------------------------------------------------------- /res/qss/dark/video_card.qss: -------------------------------------------------------------------------------- 1 | VideoCard,TextCard { 2 | border: 1px solid rgb(35, 35, 35); 3 | border-radius: 6px; 4 | background-color: rgb(50, 50, 50); 5 | } 6 | 7 | UploadCard { 8 | background-color: rgb(62, 62, 62); 9 | border-radius: 6px; 10 | border-color: rgb(37, 37, 37); 11 | } 12 | 13 | VideoCard:hover,TextCard:hover{ 14 | background-color: rgb(62, 62, 62); 15 | border-color: rgb(37, 37, 37); 16 | } 17 | 18 | #titleLabel { 19 | color: white; 20 | font-size: 14px; 21 | font-family: 'Segoe UI', 'Microsoft YaHei'; 22 | font-weight: bold; 23 | } 24 | 25 | #contentLabel { 26 | color: rgb(208, 208, 208); 27 | font-size: 12px; 28 | font-family: 'Segoe UI', 'Microsoft YaHei'; 29 | } 30 | 31 | #viewTitleLabel { 32 | color: white; 33 | font-size: 20px; 34 | font-family: "Segoe UI SemiBold", "Microsoft YaHei"; 35 | } -------------------------------------------------------------------------------- /res/qss/light/download_interface.qss: -------------------------------------------------------------------------------- 1 | QLabel#Title{ 2 | font-size: 24px; 3 | font-family: 'Segoe UI', 'Microsoft YaHei'; 4 | } 5 | 6 | QLabel#Text{ 7 | font-size: 14px; 8 | font-family: 'Segoe UI', 'Microsoft YaHei'; 9 | } 10 | 11 | LineEdit{ 12 | border: 1px solid black; 13 | } 14 | 15 | TextEdit#desc{ 16 | font-size: 12px; 17 | font-family: 'Segoe UI', 'Microsoft YaHei'; 18 | } 19 | 20 | DownloadInterface,UploadInterface { 21 | border: 1px solid rgb(229, 229, 229); 22 | border-right: none; 23 | border-bottom: none; 24 | border-top-left-radius: 10px; 25 | background-color: rgb(249, 249, 249); 26 | } -------------------------------------------------------------------------------- /res/qss/light/main.qss: -------------------------------------------------------------------------------- 1 | Widget { 2 | border: 1px solid rgb(229, 229, 229); 3 | border-right: none; 4 | border-bottom: none; 5 | border-top-left-radius: 10px; 6 | background-color: rgb(249, 249, 249); 7 | } 8 | 9 | QStackedWidget { 10 | border: 1px solid rgb(229, 229, 229); 11 | border-right: none; 12 | border-bottom: none; 13 | border-top-left-radius: 10px; 14 | background-color: rgb(249, 249, 249); 15 | } 16 | 17 | Window { 18 | background-color: rgb(243, 243, 243); 19 | } 20 | 21 | MinimizeButton { 22 | qproperty-normalColor: black; 23 | qproperty-normalBackgroundColor: transparent; 24 | qproperty-hoverColor: black; 25 | qproperty-hoverBackgroundColor: rgba(0, 0, 0, 26); 26 | qproperty-pressedColor: black; 27 | qproperty-pressedBackgroundColor: rgba(0, 0, 0, 51) 28 | } 29 | 30 | 31 | MaximizeButton { 32 | qproperty-normalColor: black; 33 | qproperty-normalBackgroundColor: transparent; 34 | qproperty-hoverColor: black; 35 | qproperty-hoverBackgroundColor: rgba(0, 0, 0, 26); 36 | qproperty-pressedColor: black; 37 | qproperty-pressedBackgroundColor: rgba(0, 0, 0, 51) 38 | } 39 | 40 | CloseButton { 41 | qproperty-normalColor: black; 42 | qproperty-normalBackgroundColor: transparent; 43 | } -------------------------------------------------------------------------------- /res/qss/light/scroll_interface.qss: -------------------------------------------------------------------------------- 1 | LocalVideoInterface,SettingInterface,SubscribeInterface,TodoListInterface,InfoInterface{ 2 | border: 1px solid rgb(229, 229, 229); 3 | border-right: none; 4 | border-bottom: none; 5 | border-top-left-radius: 10px; 6 | background-color: rgb(249, 249, 249); 7 | } 8 | 9 | ScrollArea { 10 | background-color: rgb(249, 249, 249); 11 | border: none; 12 | } 13 | 14 | QWidget#ScrollWidget{ 15 | background-color: rgb(249, 249, 249); 16 | } 17 | 18 | QLabel#Title{ 19 | font-size: 24px; 20 | font-family: 'Segoe UI', 'Microsoft YaHei'; 21 | } 22 | 23 | QScrollBar { 24 | background: transparent; 25 | width: 4px; 26 | margin-top: 32px; 27 | margin-bottom: 0; 28 | padding-right: 2px; 29 | } 30 | 31 | QScrollBar::sub-line { 32 | background: transparent; 33 | } 34 | 35 | QScrollBar::add-line { 36 | background: transparent; 37 | } 38 | 39 | QScrollBar::handle { 40 | background: rgb(122, 122, 122); 41 | border: 2px solid rgb(128, 128, 128); 42 | border-radius: 1px; 43 | min-height: 32px; 44 | } 45 | 46 | QScrollBar::add-page:vertical, 47 | QScrollBar::sub-page:vertical { 48 | background: none; 49 | } -------------------------------------------------------------------------------- /res/qss/light/upload_interface.qss: -------------------------------------------------------------------------------- 1 | UploadInterface { 2 | border: 1px solid rgb(229, 229, 229); 3 | border-right: none; 4 | border-bottom: none; 5 | border-top-left-radius: 10px; 6 | background-color: rgb(249, 249, 249); 7 | } 8 | 9 | #Title{ 10 | font-size: 24px; 11 | font-family: 'Segoe UI', 'Microsoft YaHei'; 12 | } 13 | 14 | #Text{ 15 | font-size: 14px; 16 | font-family: 'Segoe UI', 'Microsoft YaHei'; 17 | } 18 | 19 | #content { 20 | color: rgb(118, 118, 118); 21 | font-size: 12px; 22 | font-family: 'Segoe UI', 'Microsoft YaHei'; 23 | } 24 | 25 | LineEdit{ 26 | border: 1px solid black; 27 | } 28 | 29 | #desc{ 30 | font-size: 12px; 31 | font-family: 'Segoe UI', 'Microsoft YaHei'; 32 | } 33 | 34 | ScrollArea { 35 | background-color: rgb(249, 249, 249); 36 | border: none; 37 | } 38 | 39 | QWidget#ScrollWidget{ 40 | background-color: rgb(249, 249, 249); 41 | } 42 | 43 | QScrollBar { 44 | background: transparent; 45 | width: 4px; 46 | margin-top: 32px; 47 | margin-bottom: 0; 48 | padding-right: 2px; 49 | } 50 | 51 | QScrollBar::sub-line { 52 | background: transparent; 53 | } 54 | 55 | QScrollBar::add-line { 56 | background: transparent; 57 | } 58 | 59 | QScrollBar::handle { 60 | background: rgb(122, 122, 122); 61 | border: 2px solid rgb(128, 128, 128); 62 | border-radius: 1px; 63 | min-height: 32px; 64 | } 65 | 66 | QScrollBar::add-page:vertical, 67 | QScrollBar::sub-page:vertical { 68 | background: none; 69 | } -------------------------------------------------------------------------------- /res/qss/light/video_card.qss: -------------------------------------------------------------------------------- 1 | VideoCard,TextCard,UploadCard { 2 | border: 1px solid rgb(234, 234, 234); 3 | border-radius: 6px; 4 | background-color: rgb(253, 253, 253); 5 | } 6 | 7 | UploadCard { 8 | border: 1px solid rgb(177, 177, 177); 9 | border-radius: 6px; 10 | background-color: rgb(251, 251, 251); 11 | } 12 | 13 | VideoCard:hover,TextCard:hover{ 14 | background-color: rgb(251, 251, 251); 15 | border-color: rgb(177, 177, 177); 16 | } 17 | 18 | #titleLabel { 19 | color: black; 20 | font: 14px 'Segoe UI', 'Microsoft YaHei'; 21 | font-weight: bold; 22 | } 23 | 24 | #contentLabel { 25 | color: rgb(118, 118, 118); 26 | font: 12px 'Segoe UI', 'Microsoft YaHei'; 27 | } 28 | 29 | #viewTitleLabel { 30 | color: black; 31 | font: 20px "Segoe UI SemiBold", "Microsoft YaHei"; 32 | } -------------------------------------------------------------------------------- /run_pylupdate.bat: -------------------------------------------------------------------------------- 1 | D:/Python38/Scripts/pylupdate5.exe Main.py view/DownloadInterface.py view/SettingInterface.py view/LocalVideoInterface.py view/SubscribeInterface.py view/TodoListInterface.py view/UploadInterface.py common/MyWidget.py -ts res/lang/zh_CN.ts -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aye10032/YouTubeDownLoader/598af4be68b94a1f9b1b60174221adf6d38d9667/screenshot.png -------------------------------------------------------------------------------- /view/DownloadInterface.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import webbrowser 5 | 6 | from PyQt5.QtCore import Qt 7 | from PyQt5.QtGui import QGuiApplication 8 | from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel, QWidget, QSizePolicy, QHBoxLayout 9 | from qfluentwidgets import LineEdit, PushButton, ToolButton, SwitchButton, TextEdit, InfoBar, ToolTipFilter, \ 10 | ToolTipPosition 11 | from qfluentwidgets import FluentIcon as FIF 12 | from yt_dlp import YoutubeDL 13 | 14 | from common.Config import cfg, SUCCESS, WARNING 15 | from common.MyThread import UpdateMessage, Download 16 | from common.MyWidget import TableDialog 17 | from common.SignalBus import signal_bus 18 | from common.Style import StyleSheet, MyIcon 19 | 20 | 21 | class DownloadInterface(QFrame): 22 | _uploader = '' 23 | _title = '' 24 | _description = '' 25 | _upload_date = '' 26 | _format_code, _extension, _resolution, _format_note, _file_size = [], [], [], [], [] 27 | _path = '' 28 | _download = False 29 | 30 | def __init__(self, text: str, parent=None): 31 | super().__init__(parent) 32 | self.update_message_thread = None 33 | self.download_thread = None 34 | 35 | self.main_layout = QGridLayout(self) 36 | self.title_label = QLabel(self.tr('Video Download'), self) 37 | 38 | self.origin_link_label = QLabel(self.tr('Origin Link'), self) 39 | self.origin_link_input = LineEdit(self) 40 | 41 | self.auto_quality_label = QLabel(self.tr('Auto Quality'), self) 42 | self.auto_quality_btn = SwitchButton() 43 | self.quality_label = QLabel(self.tr('Quality'), self) 44 | self.quality_input = LineEdit(self) 45 | self.get_quality_btn = ToolButton(FIF.SEARCH, self) 46 | 47 | self.get_info_btn = PushButton(self.tr('Get Info'), self, FIF.MESSAGE) 48 | self.download_btn = PushButton(self.tr('Download Video'), self, FIF.DOWNLOAD) 49 | 50 | self.video_title_label = QLabel(self.tr('Video Title'), self) 51 | self.video_title_input = LineEdit(self) 52 | self.copy_title_btn = ToolButton(FIF.COPY, self) 53 | 54 | self.reprint_info_label = QLabel(self.tr('Reprinter Info'), self) 55 | self.reprint_info_input = LineEdit(self) 56 | self.copy_reprint_btn = ToolButton(FIF.COPY, self) 57 | 58 | self.video_description_input = TextEdit(self) 59 | 60 | self.save_btn = ToolButton(FIF.SAVE, self) 61 | self.play_btn = ToolButton(MyIcon.PLAY, self) 62 | self.copy_btn = ToolButton(FIF.COPY, self) 63 | self.link_btn = ToolButton(MyIcon.LINK, self) 64 | self.folder_btn = ToolButton(FIF.FOLDER, self) 65 | self.upload_btn = ToolButton(FIF.SEND, self) 66 | 67 | self.log_output = TextEdit(self) 68 | 69 | self.init_ui() 70 | self.setObjectName(text) 71 | 72 | def init_ui(self): 73 | self.main_layout.setSpacing(0) 74 | self.main_layout.setContentsMargins(20, 5, 20, 5) 75 | for i in range(9): 76 | self.main_layout.setColumnStretch(i, 1) 77 | self.main_layout.setRowStretch(i, 0) 78 | self.main_layout.setRowStretch(6, 1) 79 | self.main_layout.setRowStretch(7, 1) 80 | self.main_layout.setRowStretch(8, 1) 81 | 82 | self.title_label.setMargin(10) 83 | self.main_layout.addWidget(self.title_label, 0, 0, 1, 9, Qt.AlignCenter) 84 | 85 | widget_1 = QWidget() 86 | layout_1 = QHBoxLayout() 87 | layout_1.setContentsMargins(0, 5, 0, 5) 88 | self.origin_link_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 89 | layout_1.addWidget(self.origin_link_label, stretch=1) 90 | layout_1.addWidget(self.origin_link_input, stretch=6) 91 | widget_1.setLayout(layout_1) 92 | self.main_layout.addWidget(widget_1, 1, 0, 1, 9) 93 | 94 | widget_2 = QWidget() 95 | layout_2 = QGridLayout() 96 | layout_2.setContentsMargins(0, 0, 0, 5) 97 | self.quality_input.setText('') 98 | self.quality_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 99 | layout_2.addWidget(self.auto_quality_label, 0, 0, Qt.AlignLeft) 100 | layout_2.addWidget(self.auto_quality_btn, 0, 1, Qt.AlignLeft) 101 | layout_2.addWidget(self.quality_label, 0, 3, Qt.AlignCenter) 102 | layout_2.addWidget(self.quality_input, 0, 4, 1, 2) 103 | layout_2.addWidget(self.get_quality_btn, 0, 6) 104 | widget_2.setLayout(layout_2) 105 | self.main_layout.addWidget(widget_2, 2, 0, 1, 9) 106 | self.auto_quality_btn.setChecked(cfg.get(cfg.auto_quality)) 107 | self.auto_quality_btn.setText( 108 | self.tr('On') if self.auto_quality_btn.isChecked() else self.tr('Off')) 109 | self.quality_input.setReadOnly(cfg.get(cfg.auto_quality)) 110 | 111 | self.main_layout.addWidget(self.get_info_btn, 3, 2, 1, 2, Qt.AlignHCenter) 112 | self.main_layout.addWidget(self.download_btn, 3, 5, 1, 2, Qt.AlignHCenter) 113 | 114 | widget_3 = QWidget() 115 | layout_3 = QHBoxLayout() 116 | layout_3.setContentsMargins(0, 15, 0, 5) 117 | layout_3.addWidget(self.video_title_label, stretch=1) 118 | layout_3.addWidget(self.video_title_input, stretch=6) 119 | layout_3.addWidget(self.copy_title_btn, stretch=1) 120 | widget_3.setLayout(layout_3) 121 | self.main_layout.addWidget(widget_3, 4, 0, 1, 9) 122 | 123 | widget_4 = QWidget() 124 | layout_4 = QHBoxLayout() 125 | layout_4.setContentsMargins(0, 0, 0, 5) 126 | layout_4.addWidget(self.reprint_info_label, stretch=1) 127 | layout_4.addWidget(self.reprint_info_input, stretch=6) 128 | layout_4.addWidget(self.copy_reprint_btn, stretch=1) 129 | widget_4.setLayout(layout_4) 130 | self.main_layout.addWidget(widget_4, 5, 0, 1, 9) 131 | 132 | self.video_description_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 133 | self.main_layout.addWidget(self.video_description_input, 6, 0, 3, 9) 134 | self.video_description_input.setStyleSheet('font-size: 12px;font-family: \'Segoe UI\', \'Microsoft YaHei\';') 135 | 136 | widget_5 = QWidget() 137 | layout_5 = QHBoxLayout() 138 | layout_5.setContentsMargins(0, 5, 0, 15) 139 | layout_5.addWidget(self.save_btn, stretch=1) 140 | layout_5.addWidget(self.folder_btn, stretch=1) 141 | layout_5.addWidget(self.play_btn, stretch=1) 142 | layout_5.addWidget(self.link_btn, stretch=1) 143 | layout_5.addWidget(self.copy_btn, stretch=1) 144 | layout_5.addWidget(self.upload_btn, stretch=1) 145 | widget_5.setLayout(layout_5) 146 | self.main_layout.addWidget(widget_5, 9, 0, 1, 9) 147 | self.save_btn.setToolTip(self.tr('save download information')) 148 | self.folder_btn.setToolTip(self.tr('open download folder')) 149 | self.play_btn.setToolTip(self.tr('play download video')) 150 | self.link_btn.setToolTip(self.tr('open youtube link')) 151 | self.copy_btn.setToolTip(self.tr('copy description')) 152 | self.upload_btn.setToolTip(self.tr('turn to upload page')) 153 | self.save_btn.installEventFilter(ToolTipFilter(self.save_btn, 300, ToolTipPosition.TOP)) 154 | self.folder_btn.installEventFilter(ToolTipFilter(self.folder_btn, 300, ToolTipPosition.TOP)) 155 | self.play_btn.installEventFilter(ToolTipFilter(self.play_btn, 300, ToolTipPosition.TOP)) 156 | self.link_btn.installEventFilter(ToolTipFilter(self.link_btn, 300, ToolTipPosition.TOP)) 157 | self.copy_btn.installEventFilter(ToolTipFilter(self.copy_btn, 300, ToolTipPosition.TOP)) 158 | self.upload_btn.installEventFilter(ToolTipFilter(self.upload_btn, 300, ToolTipPosition.TOP)) 159 | 160 | self.log_output.setFixedHeight(100) 161 | self.log_output.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 162 | self.main_layout.addWidget(self.log_output, 10, 0, 2, 9) 163 | self.log_output.setStyleSheet('font-size: 12px;font-family: \'Segoe UI\', \'Microsoft YaHei\';') 164 | self.log_output.setReadOnly(True) 165 | 166 | self.setLayout(self.main_layout) 167 | 168 | self.init_text() 169 | self.set_qss() 170 | self.connect_signal() 171 | 172 | def init_text(self): 173 | self.origin_link_input.setText('') 174 | self.video_title_input.setText('【MC】【】') 175 | self.reprint_info_input.setText('转自 有能力请支持原作者') 176 | self.video_description_input.setText( 177 | f'作者:\r\n' 178 | f'发布时间:\r\n' 179 | f'搬运:{cfg.get(cfg.reprint_id)}\r\n' 180 | f'视频摘要:\r\n' 181 | f'原简介翻译:\r\n' 182 | f'存档:\r\n' 183 | f'其他外链:') 184 | 185 | def update_ui(self, path): 186 | self._path = path 187 | data_file = os.path.join(path, 'data.json') 188 | with open(data_file, 'r') as f: 189 | data_contents = json.loads(f.read()) 190 | self.origin_link_input.setText(data_contents['link']) 191 | self.video_title_input.setText(data_contents['title']) 192 | self.reprint_info_input.setText(data_contents['reprint']) 193 | self.video_description_input.setText(data_contents['description']) 194 | self._uploader = data_contents['uploader'] 195 | 196 | def set_url(self, url): 197 | self.init_text() 198 | self.origin_link_input.setText(url) 199 | 200 | def set_qss(self): 201 | self.title_label.setObjectName('Title') 202 | self.origin_link_label.setObjectName('Text') 203 | self.auto_quality_label.setObjectName('Text') 204 | self.quality_label.setObjectName('Text') 205 | self.video_title_label.setObjectName('Text') 206 | self.reprint_info_label.setObjectName('Text') 207 | 208 | StyleSheet.DOWNLOAD.apply(self) 209 | 210 | def connect_signal(self): 211 | self.auto_quality_btn.checkedChanged.connect(self.auto_quality_btn_changed) 212 | self.get_quality_btn.clicked.connect(self.on_get_quality_btn_clicked) 213 | 214 | self.get_info_btn.clicked.connect(self.start_get_info) 215 | self.download_btn.clicked.connect(self.on_download_btn_clicked) 216 | 217 | self.copy_title_btn.clicked.connect(self.copy_title) 218 | self.copy_reprint_btn.clicked.connect(self.copy_reprint) 219 | 220 | self.save_btn.clicked.connect(self.on_save_btn_clicked) 221 | self.folder_btn.clicked.connect(self.on_folder_btn_clicked) 222 | self.play_btn.clicked.connect(self.on_play_btn_clicked) 223 | self.link_btn.clicked.connect(self.on_link_btn_clicked) 224 | self.copy_btn.clicked.connect(self.on_copy_btn_clicked) 225 | self.upload_btn.clicked.connect(self.on_upload_btn_clicked) 226 | 227 | def auto_quality_btn_changed(self, is_checked: bool): 228 | if is_checked: 229 | self.auto_quality_btn.setText(self.tr('On')) 230 | else: 231 | self.auto_quality_btn.setText(self.tr('Off')) 232 | 233 | cfg.set(cfg.auto_quality, is_checked) 234 | 235 | self.quality_input.setReadOnly(is_checked) 236 | 237 | def on_get_quality_btn_clicked(self): 238 | if self.auto_quality_btn.isChecked(): 239 | self.show_finish_tooltip( 240 | self.tr('auto quality is enabled, you can start downloading the video directly'), WARNING) 241 | return 242 | 243 | if self.update_message_thread and self.update_message_thread.isRunning(): 244 | return 245 | 246 | self.update_message_thread = UpdateMessage(self.origin_link_input.text()) 247 | self.update_message_thread.log_signal.connect(self.update_log) 248 | self.update_message_thread.result_signal.connect(self.update_message) 249 | self.update_message_thread.finish_signal.connect(self.get_quality_done) 250 | self.update_message_thread.error_signal.connect(self.network_error) 251 | self.update_message_thread.start() 252 | 253 | def start_get_info(self): 254 | if self.update_message_thread and self.update_message_thread.isRunning(): 255 | return 256 | 257 | self.update_message_thread = UpdateMessage(self.origin_link_input.text()) 258 | self.update_message_thread.log_signal.connect(self.update_log) 259 | self.update_message_thread.result_signal.connect(self.update_message) 260 | self.update_message_thread.finish_signal.connect(self.get_info_done) 261 | self.update_message_thread.error_signal.connect(self.network_error) 262 | self.update_message_thread.start() 263 | 264 | def on_download_btn_clicked(self): 265 | if not self.auto_quality_btn.isChecked() and self.quality_input.text() == '': 266 | self.show_finish_tooltip(self.tr('you should choose quality first'), WARNING) 267 | return 268 | 269 | if self.video_title_input.text() == '【MC】【】': 270 | if self.update_message_thread and self.update_message_thread.isRunning(): 271 | return 272 | 273 | self.update_message_thread = UpdateMessage(self.origin_link_input.text()) 274 | self.update_message_thread.log_signal.connect(self.update_log) 275 | self.update_message_thread.result_signal.connect(self.update_message) 276 | self.update_message_thread.finish_signal.connect(self.start_download) 277 | self.update_message_thread.error_signal.connect(self.network_error) 278 | self.update_message_thread.start() 279 | else: 280 | self.start_download() 281 | 282 | def start_download(self): 283 | self._path = cfg.get(cfg.download_folder) + '/' + self.video_title_input.text(). \ 284 | replace(':', '').replace('.', '').replace('|', '').replace('\\', '').replace('/', '') \ 285 | .replace('?', '').replace('\"', '') 286 | 287 | quality = self.quality_input.text() 288 | 289 | ydl_opts = { 290 | "writethumbnail": True, 291 | 'concurrent-fragments': cfg.get(cfg.thread), 292 | 'paths': {'home': self._path}, 293 | 'output': {'default': '%(title)s.%(ext)s'}, 294 | 'writesubtitles': True, 295 | 'writeautomaticsub': True, 296 | 'subtitlesformat': 'vtt', 297 | 'subtitleslangs': ['zh-Hans', 'en'], 298 | 'postprocessors': [ 299 | { 300 | 'key': 'FFmpegSubtitlesConvertor', 301 | 'format': 'ass', 302 | }, 303 | { 304 | 'key': 'FFmpegThumbnailsConvertor', 305 | 'format': 'jpg' 306 | } 307 | ] 308 | } 309 | 310 | if cfg.get(cfg.proxy_enable): 311 | ydl_opts['proxy'] = cfg.get(cfg.proxy) 312 | ydl_opts['socket_timeout'] = 3000 313 | 314 | if cfg.get(cfg.auto_quality): 315 | ydl_opts['format'] = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]' 316 | else: 317 | ydl_opts['format'] = quality 318 | 319 | if self.download_thread and self.download_thread.isRunning(): 320 | return 321 | 322 | self.download_thread = Download(self.origin_link_input.text(), ydl_opts) 323 | self.download_thread.log_signal.connect(self.update_log) 324 | self.download_thread.finish_signal.connect(self.download_done) 325 | self.download_thread.error_signal.connect(self.network_error) 326 | self.download_thread.start() 327 | 328 | def update_message(self, info_dict): 329 | self._uploader = info_dict.get('uploader') 330 | self._title = info_dict.get('title') 331 | self._description = info_dict.get('description') 332 | 333 | if not (info_dict.get("upload_date") is None): 334 | self._upload_date = info_dict.get("upload_date", None) 335 | else: 336 | self._upload_date = '00000000' 337 | 338 | if self._upload_date[4] == '0' and self._upload_date[6] == '0': 339 | date = self._upload_date[0:4] + '年' + self._upload_date[5] + '月' + self._upload_date[7:8] + '日' 340 | elif self._upload_date[4] == '0' and not self._upload_date[6] == 0: 341 | date = self._upload_date[0:4] + '年' + self._upload_date[5] + '月' + self._upload_date[6:8] + '日' 342 | elif not self._upload_date[4] == '0' and self._upload_date[6] == '0': 343 | date = self._upload_date[0:4] + '年' + self._upload_date[4:6] + '月' + self._upload_date[7:8] + '日' 344 | else: 345 | date = self._upload_date[0:4] + '年' + self._upload_date[4:6] + '月' + self._upload_date[6:8] + '日' 346 | 347 | self.video_title_input.setText(f'【MC】{self._title}【{self._uploader}】') 348 | self.reprint_info_input.setText(f'转自{self.origin_link_input.text()} 有能力请支持原作者') 349 | self.video_description_input.setText( 350 | f'作者:{self._uploader}\r\n' 351 | f'发布时间:{date}\r\n' 352 | f'搬运:{cfg.get(cfg.reprint_id)}\r\n' 353 | f'视频摘要:\r\n' 354 | f'原简介翻译:{self._description}\r\n' 355 | f'存档:\r\n' 356 | f'其他外链:') 357 | 358 | formats = info_dict.get('formats') 359 | for f in formats: 360 | self._format_code.append(f.get('format_id')) 361 | self._extension.append(f.get('ext')) 362 | self._resolution.append(YoutubeDL.format_resolution(f)) 363 | self._format_note.append(f.get('format_note')) 364 | self._file_size.append(f.get('filesize')) 365 | 366 | def get_info_done(self): 367 | self.show_finish_tooltip(self.tr('description download complete'), SUCCESS) 368 | 369 | def download_done(self): 370 | files = os.listdir(self._path) 371 | 372 | for file in files: 373 | if file.endswith('.jpg'): 374 | old_path = os.path.join(self._path, file) 375 | new_path = os.path.join(self._path, 'cover{}'.format(os.path.splitext(file)[1])) 376 | os.rename(old_path, new_path) 377 | 378 | self.save_data() 379 | 380 | self.show_finish_tooltip(self.tr('download complete'), SUCCESS) 381 | self._download = True 382 | 383 | def get_quality_done(self): 384 | format_info = [self._format_code, self._extension, self._resolution, self._format_note, self._file_size] 385 | w = TableDialog(len(self._format_code), 5, format_info, self) 386 | w.setTitleBarVisible(False) 387 | if w.exec(): 388 | if w.audio_code != '': 389 | self.quality_input.setText(f'{w.audio_code}+{w.video_code}') 390 | else: 391 | self.quality_input.setText(w.video_code) 392 | 393 | self.show_finish_tooltip(self.tr('quality configure complete, now you can start download'), SUCCESS) 394 | else: 395 | print('Cancel button is pressed') 396 | 397 | def show_finish_tooltip(self, text, tool_type: int): 398 | """ show restart tooltip """ 399 | if tool_type == SUCCESS: 400 | InfoBar.success('', text, parent=self.window(), duration=5000) 401 | elif tool_type == WARNING: 402 | InfoBar.warning('', text, parent=self.window(), duration=5000) 403 | 404 | def copy_title(self): 405 | clipboard = QGuiApplication.clipboard() 406 | clipboard.setText(self.video_title_input.text()) 407 | self.show_finish_tooltip(self.tr('the content of the title has been copied'), SUCCESS) 408 | 409 | def copy_reprint(self): 410 | clipboard = QGuiApplication.clipboard() 411 | clipboard.setText(self.reprint_info_input.text()) 412 | self.show_finish_tooltip(self.tr('the content of the reprint has been copied'), SUCCESS) 413 | 414 | def on_save_btn_clicked(self): 415 | if self._path == '': 416 | self.show_finish_tooltip(self.tr('you haven\'t downloaded any videos yet'), WARNING) 417 | return 418 | 419 | self.save_data() 420 | 421 | self.show_finish_tooltip(self.tr('video information is saved'), SUCCESS) 422 | 423 | def save_data(self): 424 | info = { 425 | 'link': self.origin_link_input.text(), 426 | 'title': self.video_title_input.text(), 427 | 'reprint': self.reprint_info_input.text(), 428 | 'description': self.video_description_input.toPlainText(), 429 | 'uploader': self._uploader 430 | } 431 | 432 | with open(f'{self._path}/data.json', 'w') as f: 433 | json.dump(info, f) 434 | 435 | def on_folder_btn_clicked(self): 436 | if self._path == '': 437 | self.show_finish_tooltip(self.tr('you haven\'t downloaded any videos yet'), WARNING) 438 | return 439 | 440 | if os.name == 'nt': 441 | os.startfile(self._path) 442 | elif os.name == 'darwin': 443 | subprocess.Popen(['open', self._path]) 444 | else: 445 | subprocess.Popen(['xdg-open', self._path]) 446 | 447 | def on_play_btn_clicked(self): 448 | if not self._download: 449 | self.show_finish_tooltip(self.tr('you haven\'t downloaded any videos yet'), WARNING) 450 | return 451 | 452 | files = os.listdir(self._path) 453 | 454 | for file in files: 455 | if file.endswith('.mp4'): 456 | video_path = os.path.join(self._path, file) 457 | if os.name == 'nt': 458 | os.startfile(video_path) 459 | elif os.name == 'darwin': 460 | subprocess.Popen(['open', video_path]) 461 | else: 462 | subprocess.Popen(['xdg-open', video_path]) 463 | 464 | def on_link_btn_clicked(self): 465 | if self.origin_link_input.text() == '': 466 | self.show_finish_tooltip(self.tr('you haven\'t entered a video link yet'), WARNING) 467 | return 468 | 469 | webbrowser.open(self.origin_link_input.text()) 470 | 471 | def on_copy_btn_clicked(self): 472 | clipboard = QGuiApplication.clipboard() 473 | clipboard.setText(self.video_description_input.toPlainText()) 474 | self.show_finish_tooltip(self.tr('the content of the description has been copied'), SUCCESS) 475 | 476 | def on_upload_btn_clicked(self): 477 | if self._path == '': 478 | self.show_finish_tooltip(self.tr('you haven\'t downloaded any videos yet'), WARNING) 479 | return 480 | 481 | self.save_data() 482 | 483 | signal_bus.path2_upload_signal.emit(self._path) 484 | 485 | def network_error(self): 486 | self.show_finish_tooltip(self.tr('network error!'), WARNING) 487 | 488 | def update_log(self, log): 489 | self.log_output.append('[' + log.get('status') + '] ' + log.get('_default_template')) 490 | -------------------------------------------------------------------------------- /view/InfoInterface.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import Qt 2 | from PyQt5.QtWidgets import QFrame, QVBoxLayout, QWidget, QLabel 3 | from qfluentwidgets import ScrollArea, ExpandLayout 4 | 5 | from common.Config import LICENCE_PATH 6 | from common.Style import StyleSheet 7 | 8 | 9 | class InfoInterface(QFrame): 10 | def __init__(self, text: str, parent=None): 11 | super().__init__(parent=parent) 12 | self.layout = QVBoxLayout(self) 13 | self.scroll_area = ScrollArea(self) 14 | self.scroll_widget = QWidget(self) 15 | self.expand_layout = ExpandLayout(self.scroll_widget) 16 | 17 | self.title_label = QLabel(self.tr("About"), self) 18 | self.about_text = QLabel('', self.scroll_widget) 19 | 20 | self.setObjectName(text) 21 | self.init_layout() 22 | self.init_widget() 23 | 24 | def init_layout(self): 25 | self.title_label.setAlignment(Qt.AlignCenter) 26 | 27 | self.about_text.setFixedHeight(500) 28 | 29 | self.expand_layout.setSpacing(28) 30 | self.expand_layout.setContentsMargins(20, 10, 20, 0) 31 | self.expand_layout.addWidget(self.about_text) 32 | 33 | def init_widget(self): 34 | self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 35 | self.scroll_area.setViewportMargins(0, 10, 0, 20) 36 | self.scroll_area.setWidget(self.scroll_widget) 37 | self.scroll_area.setWidgetResizable(True) 38 | 39 | f = open(LICENCE_PATH, mode='r', encoding='utf8') 40 | text = f.read() 41 | self.about_text.setText(text) 42 | 43 | self.layout.addWidget(self.title_label) 44 | self.layout.addWidget(self.scroll_area) 45 | 46 | self.set_qss() 47 | 48 | def set_qss(self): 49 | self.title_label.setObjectName('Title') 50 | self.scroll_widget.setObjectName('ScrollWidget') 51 | 52 | StyleSheet.SCROLL.apply(self) 53 | -------------------------------------------------------------------------------- /view/LocalVideoInterface.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from PyQt5.QtCore import Qt 5 | from PyQt5.QtGui import QPixmap 6 | from PyQt5.QtWidgets import QFrame, QVBoxLayout, QWidget, QLabel 7 | from qfluentwidgets import ScrollArea, ExpandLayout 8 | 9 | from common.Config import cfg 10 | from common.MyWidget import VideoCard, VideoCardView 11 | from common.Style import StyleSheet 12 | 13 | 14 | class LocalVideoInterface(QFrame): 15 | 16 | def __init__(self, text: str, parent=None): 17 | super().__init__(parent=parent) 18 | self.layout = QVBoxLayout(self) 19 | self.scroll_area = ScrollArea(self) 20 | self.scroll_widget = QWidget(self) 21 | self.expand_layout = ExpandLayout(self.scroll_widget) 22 | self.video_card_view = VideoCardView('', self.scroll_widget) 23 | 24 | self.title_label = QLabel(self.tr("Download List"), self) 25 | 26 | self.setObjectName(text) 27 | self.init_layout() 28 | self.init_widget() 29 | self.connect_signal() 30 | 31 | def init_layout(self): 32 | self.title_label.setAlignment(Qt.AlignCenter) 33 | 34 | downloads = os.listdir(cfg.get(cfg.download_folder)) 35 | 36 | index = 0 37 | for video_folder in downloads: 38 | video_path = os.path.join(cfg.get(cfg.download_folder), video_folder) 39 | if os.path.isdir(video_path): 40 | data_file = os.path.join(video_path, 'data.json') 41 | if os.path.exists(data_file) and os.path.isfile(data_file): 42 | with open(data_file, 'r') as f: 43 | data_contents = json.loads(f.read()) 44 | cover_file = os.path.join(video_path, 'cover.jpg') 45 | if os.path.exists(cover_file) and os.path.isfile(cover_file): 46 | image = QPixmap(cover_file) 47 | index += 1 48 | video_card = VideoCard(image, data_contents['title'], os.path.abspath(video_path), 49 | f'video_card{index}', index) 50 | self.video_card_view.add_video_card(video_card) 51 | else: 52 | continue 53 | 54 | self.expand_layout.setSpacing(28) 55 | self.expand_layout.setContentsMargins(20, 10, 20, 0) 56 | self.expand_layout.addWidget(self.video_card_view) 57 | 58 | def init_widget(self): 59 | self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 60 | self.scroll_area.setViewportMargins(0, 10, 0, 20) 61 | self.scroll_area.setWidget(self.scroll_widget) 62 | self.scroll_area.setWidgetResizable(True) 63 | 64 | self.layout.addWidget(self.title_label) 65 | self.layout.addWidget(self.scroll_area) 66 | 67 | self.set_qss() 68 | 69 | def set_qss(self): 70 | self.title_label.setObjectName('Title') 71 | self.scroll_widget.setObjectName('ScrollWidget') 72 | 73 | StyleSheet.SCROLL.apply(self) 74 | 75 | def connect_signal(self): 76 | pass 77 | -------------------------------------------------------------------------------- /view/SettingInterface.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import Qt 2 | from PyQt5.QtWidgets import QFrame, QWidget, QVBoxLayout, QLabel, QFileDialog 3 | from qfluentwidgets import ScrollArea, ExpandLayout, SettingCardGroup, PushSettingCard, SwitchSettingCard, \ 4 | ComboBoxSettingCard, InfoBar, CustomColorSettingCard, setThemeColor, OptionsSettingCard, setTheme, Theme 5 | from qfluentwidgets import FluentIcon as FIF 6 | 7 | from common.Config import cfg 8 | from common.MyWidget import RangeSettingCard, TextDialog, DistListSettingCard 9 | from common.Style import StyleSheet, MyIcon 10 | 11 | 12 | class SettingInterface(QFrame): 13 | 14 | def __init__(self, text: str, parent=None): 15 | super().__init__(parent=parent) 16 | self.layout = QVBoxLayout(self) 17 | self.scroll_area = ScrollArea(self) 18 | self.scroll_widget = QWidget(self) 19 | self.expand_layout = ExpandLayout(self.scroll_widget) 20 | 21 | self.title_label = QLabel(self.tr("Settings"), self) 22 | 23 | self.edit_setting_group = SettingCardGroup( 24 | self.tr('Download Setting'), self.scroll_widget 25 | ) 26 | self.reprint_id_card = PushSettingCard( 27 | self.tr('Edit'), 28 | FIF.DOWNLOAD, 29 | self.tr('Reprinter ID'), 30 | cfg.get(cfg.reprint_id), 31 | self.edit_setting_group 32 | ) 33 | self.proxy_enable = SwitchSettingCard( 34 | FIF.GLOBE, 35 | self.tr("Enable Proxy"), 36 | self.tr("Whether to enable web proxy"), 37 | configItem=cfg.proxy_enable, 38 | parent=self.edit_setting_group 39 | ) 40 | self.proxy_card = PushSettingCard( 41 | self.tr('Edit'), 42 | FIF.GLOBE, 43 | self.tr('Proxy Setting'), 44 | cfg.get(cfg.proxy), 45 | self.edit_setting_group 46 | ) 47 | self.thread_card = RangeSettingCard( 48 | cfg.thread, 49 | MyIcon.NUMBER, 50 | self.tr('Number of threads'), 51 | parent=self.edit_setting_group 52 | ) 53 | self.download_folder_card = PushSettingCard( 54 | self.tr('Choose folder'), 55 | FIF.FOLDER_ADD, 56 | self.tr("Download directory"), 57 | cfg.get(cfg.download_folder), 58 | self.edit_setting_group 59 | ) 60 | 61 | self.advanced_setting_group = SettingCardGroup( 62 | self.tr('Advanced setting'), self.scroll_widget 63 | ) 64 | self.google_api_card = PushSettingCard( 65 | self.tr('Edit'), 66 | MyIcon.KEY, 67 | self.tr('Google Api Token'), 68 | str_encryption(cfg.get(cfg.api_token)), 69 | self.advanced_setting_group 70 | ) 71 | self.subscribe_channel_card = DistListSettingCard( 72 | cfg.subscribe_channels, 73 | self.tr("Subscribe Channels"), 74 | parent=self.advanced_setting_group 75 | ) 76 | self.api_server_card = PushSettingCard( 77 | self.tr('Edit'), 78 | MyIcon.SERVER, 79 | self.tr('Api Server'), 80 | cfg.get(cfg.api_server), 81 | self.advanced_setting_group 82 | ) 83 | 84 | self.system_setting_group = SettingCardGroup( 85 | self.tr('System Setting'), self.scroll_widget 86 | ) 87 | self.theme_card = OptionsSettingCard( 88 | cfg.themeMode, 89 | FIF.BRUSH, 90 | self.tr('Application theme'), 91 | self.tr("Change the appearance of your application"), 92 | texts=[ 93 | self.tr('Light'), self.tr('Dark'), 94 | self.tr('Use system setting') 95 | ], 96 | parent=self.system_setting_group 97 | ) 98 | self.theme_color_card = CustomColorSettingCard( 99 | cfg.themeColor, 100 | FIF.PALETTE, 101 | self.tr('Theme color'), 102 | self.tr('Change the theme color of you application'), 103 | self.system_setting_group 104 | ) 105 | self.language_card = ComboBoxSettingCard( 106 | cfg.language, 107 | FIF.LANGUAGE, 108 | self.tr('Language'), 109 | self.tr('Set your preferred language for UI'), 110 | texts=['简体中文', 'English', self.tr('Use system setting')], 111 | parent=self.system_setting_group 112 | ) 113 | 114 | self.setObjectName(text) 115 | self.init_layout() 116 | self.init_widget() 117 | self.connect_signal() 118 | 119 | def init_layout(self): 120 | self.title_label.setAlignment(Qt.AlignCenter) 121 | self.edit_setting_group.addSettingCard(self.reprint_id_card) 122 | self.edit_setting_group.addSettingCard(self.proxy_enable) 123 | self.edit_setting_group.addSettingCard(self.proxy_card) 124 | self.edit_setting_group.addSettingCard(self.thread_card) 125 | self.edit_setting_group.addSettingCard(self.download_folder_card) 126 | 127 | self.advanced_setting_group.addSettingCard(self.google_api_card) 128 | self.advanced_setting_group.addSettingCard(self.subscribe_channel_card) 129 | self.advanced_setting_group.addSettingCard(self.api_server_card) 130 | 131 | self.system_setting_group.addSettingCard(self.theme_card) 132 | self.system_setting_group.addSettingCard(self.theme_color_card) 133 | self.system_setting_group.addSettingCard(self.language_card) 134 | 135 | self.expand_layout.setSpacing(28) 136 | self.expand_layout.setContentsMargins(20, 10, 20, 0) 137 | self.expand_layout.addWidget(self.edit_setting_group) 138 | self.expand_layout.addWidget(self.advanced_setting_group) 139 | self.expand_layout.addWidget(self.system_setting_group) 140 | 141 | def init_widget(self): 142 | self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 143 | self.scroll_area.setViewportMargins(0, 10, 0, 20) 144 | self.scroll_area.setWidget(self.scroll_widget) 145 | self.scroll_area.setWidgetResizable(True) 146 | 147 | self.layout.addWidget(self.title_label) 148 | self.layout.addWidget(self.scroll_area) 149 | 150 | self.set_qss() 151 | 152 | def set_qss(self): 153 | self.title_label.setObjectName('Title') 154 | self.scroll_widget.setObjectName('ScrollWidget') 155 | 156 | StyleSheet.SCROLL.apply(self) 157 | 158 | def connect_signal(self): 159 | cfg.appRestartSig.connect(self.show_restart_tooltip) 160 | cfg.themeChanged.connect(self.on_theme_changed) 161 | 162 | self.reprint_id_card.clicked.connect( 163 | self.on_reprint_id_card_clicked 164 | ) 165 | self.download_folder_card.clicked.connect( 166 | self.on_download_folder_card_clicked 167 | ) 168 | self.proxy_card.clicked.connect( 169 | self.on_proxy_card_clicked 170 | ) 171 | 172 | self.google_api_card.clicked.connect( 173 | self.on_google_api_card_clicked 174 | ) 175 | self.api_server_card.clicked.connect( 176 | self.on_api_server_card_clicked 177 | ) 178 | 179 | self.theme_color_card.colorChanged.connect(setThemeColor) 180 | 181 | def show_restart_tooltip(self): 182 | """ show restart tooltip """ 183 | InfoBar.warning( 184 | '', 185 | self.tr('Configuration takes effect after restart'), 186 | parent=self.window() 187 | ) 188 | 189 | def on_theme_changed(self, theme: Theme): 190 | """ theme changed slot """ 191 | # change the theme of qfluentwidgets 192 | setTheme(theme) 193 | 194 | # chang the theme of setting interface 195 | self.set_qss() 196 | 197 | def on_reprint_id_card_clicked(self): 198 | w = TextDialog(self.tr('Nick Name'), self.tr('please input your nick name:'), cfg.get(cfg.reprint_id), self) 199 | w.setTitleBarVisible(False) 200 | if w.exec(): 201 | cfg.set(cfg.reprint_id, w.input_edit.text()) 202 | self.reprint_id_card.setContent(w.input_edit.text()) 203 | else: 204 | print('Cancel button is pressed') 205 | 206 | def on_proxy_card_clicked(self): 207 | w = TextDialog(self.tr('Proxy Setting'), self.tr('manual proxy configuration:'), cfg.get(cfg.proxy), self) 208 | w.setTitleBarVisible(False) 209 | if w.exec(): 210 | cfg.set(cfg.proxy, w.input_edit.text()) 211 | self.proxy_card.setContent(w.input_edit.text()) 212 | else: 213 | print('Cancel button is pressed') 214 | 215 | def on_google_api_card_clicked(self): 216 | w = TextDialog( 217 | self.tr('API Token Setting'), 218 | self.tr('set your google develop token:'), 219 | cfg.get(cfg.api_token), self 220 | ) 221 | w.setTitleBarVisible(False) 222 | if w.exec(): 223 | cfg.set(cfg.api_token, w.input_edit.text()) 224 | self.google_api_card.setContent(str_encryption(w.input_edit.text())) 225 | else: 226 | print('Cancel button is pressed') 227 | 228 | def on_api_server_card_clicked(self): 229 | w = TextDialog(self.tr('API Server'), self.tr('manual your server address:'), cfg.get(cfg.api_server), self) 230 | w.setTitleBarVisible(False) 231 | if w.exec(): 232 | cfg.set(cfg.api_server, w.input_edit.text()) 233 | self.api_server_card.setContent(w.input_edit.text()) 234 | else: 235 | print('Cancel button is pressed') 236 | 237 | def on_download_folder_card_clicked(self): 238 | folder = QFileDialog.getExistingDirectory( 239 | self, self.tr("Choose folder"), "./") 240 | if not folder or cfg.get(cfg.download_folder) == folder: 241 | return 242 | 243 | print(folder) 244 | cfg.set(cfg.download_folder, folder) 245 | self.download_folder_card.setContent(folder) 246 | 247 | 248 | def str_encryption(text: str): 249 | if len(text) == 0: 250 | return '' 251 | else: 252 | return text[:3] + "***" + text[-3:] 253 | -------------------------------------------------------------------------------- /view/SubscribeInterface.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from PyQt5.QtCore import Qt, QUrl 4 | from PyQt5.QtGui import QPixmap 5 | from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QNetworkProxy 6 | from PyQt5.QtWidgets import QFrame, QVBoxLayout, QWidget, QLabel 7 | from googleapiclient.discovery import build 8 | from httplib2 import ProxyInfo, socks, Http 9 | from qfluentwidgets import ScrollArea, ExpandLayout 10 | from socks import ProxyConnectionError 11 | 12 | from common.Config import cfg 13 | from common.MyWidget import VideoCardView, TextCard 14 | from common.Style import StyleSheet 15 | 16 | 17 | class SubscribeInterface(QFrame): 18 | def __init__(self, text: str, parent=None): 19 | super().__init__(parent=parent) 20 | self.layout = QVBoxLayout(self) 21 | self.scroll_area = ScrollArea(self) 22 | self.scroll_widget = QWidget(self) 23 | self.expand_layout = ExpandLayout(self.scroll_widget) 24 | 25 | self.title_label = QLabel(self.tr("Subscribe List"), self) 26 | 27 | self.setObjectName(text) 28 | self.init_layout() 29 | self.init_widget() 30 | 31 | def init_layout(self): 32 | self.title_label.setAlignment(Qt.AlignCenter) 33 | 34 | if not cfg.get(cfg.api_token) == '': 35 | channels = cfg.get(cfg.subscribe_channels) 36 | for channel in channels: 37 | video_card_view = VideoCardView(channel['name'], self.scroll_widget) 38 | channel_id = channel['channel_id'] 39 | 40 | videos = get_channel_info(channel_id) 41 | for video in videos: 42 | url = 'https://youtu.be/' + video['id']['videoId'] 43 | title = video['snippet']['title'] 44 | upload_date = str_local_time(video['snippet']['publishedAt']) 45 | video_card = TextCard(title, upload_date, url, video['id']['videoId'], video_card_view) 46 | video_card_view.add_video_card(video_card) 47 | 48 | self.expand_layout.addWidget(video_card_view) 49 | 50 | self.expand_layout.setSpacing(28) 51 | self.expand_layout.setContentsMargins(20, 10, 20, 0) 52 | 53 | def init_widget(self): 54 | self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 55 | self.scroll_area.setViewportMargins(0, 10, 0, 20) 56 | self.scroll_area.setWidget(self.scroll_widget) 57 | self.scroll_area.setWidgetResizable(True) 58 | 59 | self.layout.addWidget(self.title_label) 60 | self.layout.addWidget(self.scroll_area) 61 | 62 | self.set_qss() 63 | 64 | def set_qss(self): 65 | self.title_label.setObjectName('Title') 66 | self.scroll_widget.setObjectName('ScrollWidget') 67 | 68 | StyleSheet.SCROLL.apply(self) 69 | 70 | 71 | def get_channel_info(channel_id: str): 72 | if cfg.get(cfg.proxy_enable): 73 | ipaddress = cfg.get(cfg.proxy).split(':')[1][2:] 74 | ipport = int(cfg.get(cfg.proxy).split(':')[2]) 75 | proxy_info = ProxyInfo(socks.PROXY_TYPE_HTTP, ipaddress, ipport) 76 | http = Http(timeout=300, proxy_info=proxy_info) 77 | else: 78 | http = Http(timeout=300) 79 | 80 | try: 81 | 82 | youtube = build('youtube', 'v3', developerKey=cfg.get(cfg.api_token), static_discovery=False, http=http) 83 | 84 | result = youtube.search().list( 85 | part='snippet,id', 86 | channelId=channel_id, 87 | order='date', 88 | maxResults=8 89 | ).execute() 90 | 91 | return result['items'] 92 | except ConnectionRefusedError as e: 93 | print(e) 94 | except ProxyConnectionError as e: 95 | print(e) 96 | 97 | return [] 98 | 99 | 100 | def str_local_time(utc_time_str: str): 101 | utc_time = datetime.fromisoformat(utc_time_str.replace("Z", "+00:00")) 102 | local_time = utc_time.astimezone() 103 | 104 | return local_time.strftime('%Y年%m月%d日 %H:%M') 105 | 106 | 107 | def load_pixmap_from_url(url): 108 | manager = QNetworkAccessManager() 109 | print(f'try to download {url}') 110 | 111 | if cfg.get(cfg.proxy_enable): 112 | proxy = QNetworkProxy() 113 | proxy.setType(QNetworkProxy.HttpProxy) 114 | proxy.setHostName(cfg.get(cfg.proxy).split(':')[1][2:]) 115 | proxy.setPort(int(cfg.get(cfg.proxy).split(':')[2])) 116 | manager.setProxy(proxy) 117 | 118 | request = QNetworkRequest(QUrl(url)) 119 | 120 | reply = manager.get(request) 121 | 122 | while not reply.isFinished(): 123 | pass 124 | 125 | if reply.error() != QNetworkReply.NoError: 126 | print(f"Error loading image {url}") 127 | return None 128 | 129 | pixmap = QPixmap() 130 | pixmap.loadFromData(reply.readAll()) 131 | 132 | return pixmap 133 | -------------------------------------------------------------------------------- /view/TodoListInterface.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | from PyQt5.QtCore import Qt 5 | from PyQt5.QtWidgets import QFrame, QVBoxLayout, QWidget, QLabel 6 | from qfluentwidgets import ScrollArea, ExpandLayout 7 | from requests import request 8 | 9 | from common.Config import cfg 10 | from common.MyWidget import VideoCardView, TextCard 11 | from common.Style import StyleSheet 12 | 13 | 14 | class TodoListInterface(QFrame): 15 | def __init__(self, text: str, parent=None): 16 | super().__init__(parent=parent) 17 | self.layout = QVBoxLayout(self) 18 | self.scroll_area = ScrollArea(self) 19 | self.scroll_widget = QWidget(self) 20 | self.expand_layout = ExpandLayout(self.scroll_widget) 21 | self.video_card_view = VideoCardView('', self.scroll_widget) 22 | 23 | self.title_label = QLabel(self.tr("TODO List"), self) 24 | 25 | self.setObjectName(text) 26 | self.init_layout() 27 | self.init_widget() 28 | 29 | def init_layout(self): 30 | self.title_label.setAlignment(Qt.AlignCenter) 31 | 32 | if not cfg.get(cfg.api_server) == '': 33 | url = cfg.get(cfg.api_server) 34 | 35 | try: 36 | done_response = request("GET", url) 37 | 38 | json_data = done_response.json() 39 | 40 | for item in json_data['data']: 41 | todo_id = item['id'] 42 | description = item['description'] 43 | url = item['url'] 44 | time_stamp = item['time'] 45 | # 将时间戳转换为指定格式 46 | time_format = datetime.fromtimestamp(time_stamp / 1000).strftime('%Y年%m月%d日 %H:%M') 47 | 48 | video_card = TextCard(f'{todo_id} | {description}', time_format, url, f'todo{todo_id}', 49 | self.video_card_view) 50 | self.video_card_view.add_video_card(video_card) 51 | 52 | except ConnectionRefusedError: 53 | logging.warning('获取视频列表错误') 54 | 55 | self.expand_layout.setSpacing(28) 56 | self.expand_layout.setContentsMargins(20, 10, 20, 0) 57 | self.expand_layout.addWidget(self.video_card_view) 58 | 59 | def init_widget(self): 60 | self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 61 | self.scroll_area.setViewportMargins(0, 10, 0, 20) 62 | self.scroll_area.setWidget(self.scroll_widget) 63 | self.scroll_area.setWidgetResizable(True) 64 | 65 | self.layout.addWidget(self.title_label) 66 | self.layout.addWidget(self.scroll_area) 67 | 68 | self.set_qss() 69 | 70 | def set_qss(self): 71 | self.title_label.setObjectName('Title') 72 | self.scroll_widget.setObjectName('ScrollWidget') 73 | 74 | StyleSheet.SCROLL.apply(self) 75 | -------------------------------------------------------------------------------- /view/UploadInterface.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | 5 | from PyQt5.QtCore import Qt 6 | from PyQt5.QtWidgets import QFrame, QLabel, QVBoxLayout, QWidget, QSizePolicy, QHBoxLayout, QFileDialog 7 | from qfluentwidgets import TextEdit, ScrollArea, ExpandLayout, LineEdit, ToolButton, PrimaryPushButton, \ 8 | InfoBar 9 | from qfluentwidgets import FluentIcon as FIF 10 | 11 | from common.Config import cfg, SUCCESS, WARNING 12 | from common.MyThread import Upload 13 | from common.MyWidget import UploadCard 14 | from common.SignalBus import signal_bus 15 | from common.Style import StyleSheet 16 | 17 | 18 | class UploadInterface(QFrame): 19 | _videos = [] 20 | 21 | def __init__(self, text: str, parent=None): 22 | super().__init__(parent) 23 | self.upload_thread = None 24 | 25 | self.layout = QVBoxLayout(self) 26 | self.scroll_area = ScrollArea(self) 27 | self.scroll_widget = QWidget(self) 28 | self.expand_layout = ExpandLayout(self.scroll_widget) 29 | 30 | self.title_label = QLabel(self.tr("Upload"), self) 31 | 32 | self.video_title_label = QLabel(self.tr('Video Title'), self.scroll_widget) 33 | self.video_title_input = LineEdit(self.scroll_widget) 34 | 35 | self.cover_label = QLabel(self.tr('Cover'), self.scroll_widget) 36 | self.cover_path_input = LineEdit(self.scroll_widget) 37 | self.cover_path_btn = ToolButton(FIF.FOLDER, self.scroll_widget) 38 | 39 | self.widget_3 = QWidget(self.scroll_widget) 40 | self.video_label = QLabel(self.tr('Video'), self.scroll_widget) 41 | self.video_card_view = QWidget(self.scroll_widget) 42 | self.video_card_layout = QVBoxLayout() 43 | self.add_video_btn = ToolButton(FIF.FOLDER_ADD, self.scroll_widget) 44 | 45 | self.reprint_info_label = QLabel(self.tr('Source'), self.scroll_widget) 46 | self.reprint_info_input = LineEdit(self.scroll_widget) 47 | 48 | self.tag_label = QLabel(self.tr('Tag'), self.scroll_widget) 49 | self.tag_input = LineEdit(self.scroll_widget) 50 | 51 | self.video_description_label = QLabel(self.tr('Description'), self.scroll_widget) 52 | self.video_description_input = TextEdit(self.scroll_widget) 53 | 54 | self.dynamic_label = QLabel(self.tr('Dynamic'), self.scroll_widget) 55 | self.dynamic_input = LineEdit(self.scroll_widget) 56 | 57 | self.upload_btn = PrimaryPushButton(self.tr('Upload'), self.scroll_widget, FIF.SEND) 58 | 59 | self.log_output = TextEdit(self) 60 | 61 | self.setObjectName(text) 62 | self.init_layout() 63 | self.init_widget() 64 | self.connect_signal() 65 | 66 | def init_widget(self): 67 | self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 68 | self.scroll_area.setViewportMargins(0, 10, 0, 20) 69 | self.scroll_area.setWidget(self.scroll_widget) 70 | self.scroll_area.setWidgetResizable(True) 71 | 72 | widget_1 = QWidget(self.scroll_widget) 73 | layout_1 = QHBoxLayout() 74 | layout_1.setContentsMargins(5, 0, 0, 0) 75 | self.video_title_label.setFixedWidth(85) 76 | layout_1.addWidget(self.video_title_label, stretch=1) 77 | layout_1.addWidget(self.video_title_input, stretch=6) 78 | widget_1.setLayout(layout_1) 79 | widget_1.setFixedHeight(35) 80 | self.expand_layout.addWidget(widget_1) 81 | 82 | widget_2 = QWidget(self.scroll_widget) 83 | layout_2 = QHBoxLayout() 84 | layout_2.setContentsMargins(5, 0, 0, 0) 85 | self.cover_label.setFixedWidth(85) 86 | layout_2.addWidget(self.cover_label, stretch=1) 87 | layout_2.addWidget(self.cover_path_input, stretch=5) 88 | layout_2.addWidget(self.cover_path_btn, stretch=1) 89 | widget_2.setLayout(layout_2) 90 | widget_2.setFixedHeight(35) 91 | self.expand_layout.addWidget(widget_2) 92 | 93 | layout_3 = QHBoxLayout() 94 | layout_3.setContentsMargins(5, 0, 0, 0) 95 | self.video_label.setFixedWidth(85) 96 | layout_3.addWidget(self.video_label, stretch=1, alignment=Qt.AlignTop) 97 | layout_3.addWidget(self.video_card_view, stretch=6, alignment=Qt.AlignTop) 98 | self.video_card_layout.addWidget(self.add_video_btn, alignment=Qt.AlignRight) 99 | self.widget_3.setLayout(layout_3) 100 | self.widget_3.setFixedHeight(35) 101 | self.expand_layout.addWidget(self.widget_3) 102 | 103 | widget_4 = QWidget(self.scroll_widget) 104 | layout_4 = QHBoxLayout() 105 | layout_4.setContentsMargins(5, 0, 0, 0) 106 | self.reprint_info_label.setFixedWidth(85) 107 | layout_4.addWidget(self.reprint_info_label, stretch=1) 108 | layout_4.addWidget(self.reprint_info_input, stretch=6) 109 | widget_4.setLayout(layout_4) 110 | widget_4.setFixedHeight(35) 111 | self.expand_layout.addWidget(widget_4) 112 | 113 | widget_5 = QWidget(self.scroll_widget) 114 | layout_5 = QHBoxLayout() 115 | layout_5.setContentsMargins(5, 0, 0, 0) 116 | self.tag_label.setFixedWidth(85) 117 | layout_5.addWidget(self.tag_label, stretch=1) 118 | layout_5.addWidget(self.tag_input, stretch=6) 119 | widget_5.setLayout(layout_5) 120 | widget_5.setFixedHeight(35) 121 | self.expand_layout.addWidget(widget_5) 122 | 123 | widget_6 = QWidget(self.scroll_widget) 124 | layout_6 = QHBoxLayout() 125 | layout_6.setContentsMargins(5, 0, 0, 0) 126 | self.video_description_label.setFixedWidth(85) 127 | layout_6.addWidget(self.video_description_label, stretch=1, alignment=Qt.AlignTop) 128 | layout_6.addWidget(self.video_description_input, stretch=6, alignment=Qt.AlignTop) 129 | self.video_description_input.setStyleSheet('font-size: 12px;font-family: \'Segoe UI\', \'Microsoft YaHei\';') 130 | self.video_description_input.setFixedHeight(240) 131 | widget_6.setLayout(layout_6) 132 | widget_6.setFixedHeight(250) 133 | self.expand_layout.addWidget(widget_6) 134 | 135 | widget_7 = QWidget(self.scroll_widget) 136 | layout_7 = QHBoxLayout() 137 | layout_7.setContentsMargins(5, 0, 0, 0) 138 | self.dynamic_label.setFixedWidth(85) 139 | layout_7.addWidget(self.dynamic_label, stretch=1) 140 | layout_7.addWidget(self.dynamic_input, stretch=6) 141 | widget_7.setLayout(layout_7) 142 | widget_7.setFixedHeight(35) 143 | self.expand_layout.addWidget(widget_7) 144 | 145 | widget_8 = QWidget(self.scroll_widget) 146 | layout_8 = QHBoxLayout() 147 | layout_8.setContentsMargins(5, 0, 0, 0) 148 | layout_8.addWidget(self.upload_btn, alignment=Qt.AlignCenter) 149 | widget_8.setLayout(layout_8) 150 | widget_8.setFixedHeight(35) 151 | self.expand_layout.addWidget(widget_8) 152 | 153 | self.log_output.setFixedHeight(100) 154 | self.log_output.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 155 | self.log_output.setStyleSheet('font-size: 12px;font-family: \'Segoe UI\', \'Microsoft YaHei\';') 156 | self.log_output.setReadOnly(True) 157 | 158 | def init_layout(self): 159 | self.title_label.setAlignment(Qt.AlignCenter) 160 | 161 | self.video_card_view.setLayout(self.video_card_layout) 162 | self.video_card_layout.setSpacing(5) 163 | self.video_card_layout.setContentsMargins(0, 0, 0, 0) 164 | 165 | self.expand_layout.setSpacing(10) 166 | self.expand_layout.setContentsMargins(15, 0, 30, 0) 167 | 168 | self.layout.addWidget(self.title_label) 169 | self.layout.addWidget(self.scroll_area) 170 | self.layout.addWidget(self.log_output) 171 | 172 | self.set_qss() 173 | 174 | def init_text(self, path: str): 175 | data_file = os.path.join(path, 'data.json') 176 | with open(data_file, 'r') as f: 177 | data_contents = json.loads(f.read()) 178 | self.video_title_input.setText(data_contents['title']) 179 | self.reprint_info_input.setText(data_contents['reprint']) 180 | self.video_description_input.setText(data_contents['description']) 181 | 182 | self.cover_path_input.setText(os.path.join(path, 'cover.jpg')) 183 | uploader = data_contents['uploader'] 184 | self.tag_input.setText(f'游戏,单机游戏,MC,youtube,minecraft,{uploader}') 185 | for file in os.listdir(path): 186 | if file.endswith('.mp4') or file.endswith('.mkv'): 187 | video_path = os.path.join(path, file) 188 | self.add_video(video_path) 189 | 190 | def add_video(self, path): 191 | name = os.path.splitext(os.path.split(path)[1])[0] 192 | route_key = re.findall(r'\[(.*?)\]', name)[-1] 193 | video = { 194 | 'name': name, 195 | 'route_key': route_key, 196 | 'path': path 197 | } 198 | if video not in self._videos: 199 | self._videos.append(video) 200 | 201 | card = UploadCard(video['name'], video['path'], self.scroll_widget) 202 | card.setObjectName(video['route_key']) 203 | card.del_signal.connect(self.del_video) 204 | self.video_card_layout.addWidget(card) 205 | 206 | self.adjust_size() 207 | else: 208 | return 209 | 210 | def del_video(self, route_key): 211 | card = self.video_card_view.findChild(UploadCard, route_key, options=Qt.FindDirectChildrenOnly) 212 | if card is not None: 213 | card.deleteLater() 214 | self._videos = [video for video in self._videos if video['route_key'] != route_key] 215 | self.adjust_size() 216 | 217 | def adjust_size(self): 218 | count = len(self._videos) 219 | if count == 0: 220 | self.video_card_view.setFixedHeight(35) 221 | self.widget_3.setFixedHeight(35) 222 | else: 223 | self.video_card_view.setFixedHeight(count * 85 + 45) 224 | self.widget_3.setFixedHeight(count * 85 + 45) 225 | 226 | self.video_card_layout.update() 227 | self.video_card_view.update() 228 | self.scroll_widget.update() 229 | self.update() 230 | 231 | def set_qss(self): 232 | self.title_label.setObjectName('Title') 233 | self.scroll_widget.setObjectName('ScrollWidget') 234 | self.video_title_label.setObjectName('Text') 235 | self.cover_label.setObjectName('Text') 236 | self.video_label.setObjectName('Text') 237 | self.reprint_info_label.setObjectName('Text') 238 | self.tag_label.setObjectName('Text') 239 | self.video_description_label.setObjectName('Text') 240 | self.dynamic_label.setObjectName('Text') 241 | 242 | StyleSheet.UPLOAD.apply(self) 243 | 244 | def connect_signal(self): 245 | self.cover_path_btn.clicked.connect(self.on_cover_path_btn_clicked) 246 | self.add_video_btn.clicked.connect(self.on_add_video_btn_clicked) 247 | self.upload_btn.clicked.connect(self.on_upload_btn_clicked) 248 | signal_bus.log_signal.connect(self.log_update) 249 | 250 | def on_cover_path_btn_clicked(self): 251 | options = QFileDialog.Options() 252 | options.filter = "JPEG files (*.jpg)" 253 | file_name, _ = QFileDialog.getOpenFileName(None, "Choose Image File", cfg.get(cfg.download_folder), 254 | "Image files (*.jpg *.png *.bmp)", options=options) 255 | self.cover_path_input.setText(file_name) 256 | 257 | def on_add_video_btn_clicked(self): 258 | options = QFileDialog.Options() 259 | options.filter = "MP4 files (*.mp4)" 260 | file_name, _ = QFileDialog.getOpenFileName(None, "Choose Image File", cfg.get(cfg.download_folder), 261 | "Video files (*.mp4)", options=options) 262 | 263 | self.add_video(file_name) 264 | 265 | def on_upload_btn_clicked(self): 266 | cookie_file = os.path.join('config', 'cookies.json') 267 | if not os.path.exists(cookie_file): 268 | self.show_finish_tooltip(self.tr('no cookies found'), WARNING) 269 | return 270 | 271 | with open(cookie_file, 'r') as f: 272 | cookie_contents = json.loads(f.read()) 273 | 274 | sessdata = '' 275 | bili_jct = '' 276 | dedeuserid_ckmd5 = '' 277 | dedeuserid = '' 278 | access_token = '' 279 | 280 | if 'cookie_info' in cookie_contents: 281 | cookies = cookie_contents['cookie_info']['cookies'] 282 | for cookie in cookies: 283 | if cookie['name'] == 'SESSDATA': 284 | sessdata = cookie['value'] 285 | elif cookie['name'] == 'bili_jct': 286 | bili_jct = cookie['value'] 287 | elif cookie['name'] == 'DedeUserID__ckMd5': 288 | dedeuserid_ckmd5 = cookie['value'] 289 | elif cookie['name'] == 'DedeUserID': 290 | dedeuserid = cookie['value'] 291 | else: 292 | self.show_finish_tooltip(self.tr('no cookies found'), WARNING) 293 | 294 | if 'token_info' in cookie_contents: 295 | access_token = cookie_contents['token_info']['access_token'] 296 | else: 297 | self.show_finish_tooltip(self.tr('no cookies found'), WARNING) 298 | 299 | login_access = { 300 | 'cookies': { 301 | 'SESSDATA': sessdata, 302 | 'bili_jct': bili_jct, 303 | 'DedeUserID__ckMd5': dedeuserid_ckmd5, 304 | 'DedeUserID': dedeuserid 305 | }, 306 | 'access_token': access_token 307 | } 308 | 309 | if len(self._videos) == 0: 310 | self.show_finish_tooltip(self.tr('no videos, plead add video first!'), WARNING) 311 | video_list = [] 312 | for video_info in self._videos: 313 | card = self.video_card_view.findChild(UploadCard, video_info['route_key'], 314 | options=Qt.FindDirectChildrenOnly) 315 | title = 'part' 316 | if card is not None: 317 | title = card.title_input.text() 318 | part_info = { 319 | 'name': title, 320 | 'path': video_info['path'] 321 | } 322 | video_list.append(part_info) 323 | 324 | if self.upload_thread and self.upload_thread.isRunning(): 325 | return 326 | 327 | info = { 328 | 'title': self.video_title_input.text(), 329 | 'desc': self.video_description_input.toPlainText(), 330 | 'source': self.reprint_info_input.text(), 331 | 'tag': self.tag_input.text().split(','), 332 | 'dynamic': self.dynamic_input.text(), 333 | 'cover_path': self.cover_path_input.text() 334 | } 335 | 336 | self.upload_thread = Upload(login_access, info, video_list) 337 | self.upload_thread.finish_signal.connect(self.upload_done) 338 | self.upload_thread.start() 339 | 340 | def log_update(self, text): 341 | self.log_output.append(text) 342 | 343 | def upload_done(self): 344 | self.show_finish_tooltip('upload done!', SUCCESS) 345 | 346 | def show_finish_tooltip(self, text, tool_type: int): 347 | """ show restart tooltip """ 348 | if tool_type == SUCCESS: 349 | InfoBar.success('', text, parent=self.window(), duration=5000) 350 | elif tool_type == WARNING: 351 | InfoBar.warning('', text, parent=self.window(), duration=5000) 352 | --------------------------------------------------------------------------------