├── .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 |
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 |
--------------------------------------------------------------------------------
/res/icons/key_white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/link_black.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/link_white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aye10032/YouTubeDownLoader/598af4be68b94a1f9b1b60174221adf6d38d9667/res/icons/logo.ico
--------------------------------------------------------------------------------
/res/icons/number_black.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/number_white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/play_black.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/play_white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/server_black.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/server_white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------