sya-pyqt.py (13509B)
1 #!/usr/bin/env python3 2 3 # std 4 import os 5 import sys 6 import tempfile 7 # sya 8 import sya 9 # pip 10 import PyQt5.QtCore as qtcore 11 import PyQt5.QtWidgets as qtwidg 12 import PyQt5.QtGui as qtgui 13 14 15 def resource_path(relative_path): 16 try: 17 base_path = sys._MEIPASS 18 except AttributeError: 19 base_path = os.path.abspath(".") 20 return os.path.join(base_path, relative_path) 21 22 23 def center_widget(widget): 24 sg = qtwidg.QDesktopWidget().screenGeometry() 25 wg = widget.geometry() 26 return qtcore.QPoint( 27 round(sg.width() / 2) - round(wg.width() / 2), 28 round(sg.height() / 2) - round(wg.height() / 2)) 29 30 31 def new_combobox(parent, items, default_item, fn_update): 32 combobox = qtwidg.QComboBox(parent) 33 for i in items: 34 combobox.addItem(i) 35 if default_item in items: 36 combobox.setCurrentIndex(items.index(default_item)) 37 combobox.activated[str].connect(fn_update) 38 39 layout = qtwidg.QHBoxLayout() 40 layout.addWidget(combobox) 41 42 return layout 43 44 45 def new_linedit(parent, fn_update, default_value=''): 46 line_edit = qtwidg.QLineEdit(parent) 47 line_edit.setText(default_value) 48 line_edit.textChanged.connect(fn_update) 49 return line_edit 50 51 52 def new_btn(parent, fn_select, icon=''): 53 btn_icon = qtgui.QIcon(resource_path('{}.png'.format(icon))) 54 btn = qtwidg.QPushButton(btn_icon, '', parent) 55 btn.clicked.connect(fn_select) 56 return btn 57 58 59 def new_filepicker(parent, fn_select, fn_update, default_value='', icon=''): 60 layout = qtwidg.QHBoxLayout() 61 lineedit = new_linedit(parent, fn_update, default_value) 62 layout.addWidget(lineedit) 63 layout.addWidget(new_btn(parent, fn_select, icon)) 64 return layout, lineedit 65 66 67 def generate_tracklist(url, tracklist): 68 fd, fpath = tempfile.mkstemp() 69 with open(fd, 'w') as f: 70 f.write(url) 71 f.write('\n') 72 f.writelines(t.encode('utf-8').decode('ascii', 'ignore') for t in tracklist) 73 return fpath 74 75 76 class SyaGuiThread(qtcore.QThread): 77 def __init__(self, fn, fn_args=None): 78 super().__init__() 79 self.fn = fn 80 self.args = fn_args 81 82 def run(self): 83 if self.args is None: 84 self.fn() 85 else: 86 self.fn(self.args) 87 88 89 class SyaGuiLogStream(qtcore.QObject): 90 txt = qtcore.pyqtSignal(str) 91 92 def write(self, txt): 93 self.txt.emit(str(txt)) 94 95 96 class SyaGuiOptions(qtwidg.QWidget): 97 def __init__(self, init_values): 98 super().__init__() 99 100 url = '' 101 tracklist = '' 102 output = '' 103 if os.path.exists(init_values.tracklist): 104 url, tracklist = sya.load_tracklist(init_values.tracklist) 105 if init_values.output == '' and init_values.tracklist != '': 106 output = os.path.join(os.getcwd(), os.path.splitext(os.path.basename(init_values.tracklist))[0]) 107 108 self.labels = { 109 'url': 'URL:', 110 'tracklist': 'Tracklist:', 111 'format': 'Format:', 112 'quality': 'Quality:', 113 'keep': 'Keep un-split file', 114 'output': 'Output:' } 115 self.values = { 116 'url': url, 117 'tracklist': '\n'.join(tracklist), 118 'format': init_values.format, 119 'quality': init_values.quality, 120 'keep': init_values.keep, 121 'output': output } 122 123 self.availableFormats = ['mp3', 'wav', 'ogg', 'aac'] 124 self.availableQualities = ['0 (better)', '1', '2', '3', '4', '5', '6', '7', '8', '9 (worse)'] 125 126 self._layout = qtwidg.QGridLayout() 127 self.url = self._init_url() 128 self.tracklist = self._init_tracklist() 129 self.format = self._init_format() 130 self.quality = self._init_quality() 131 self._init_spacer() 132 self.keep = self._init_keep() 133 self.output = self._init_output() 134 self.exit = self._init_exit() 135 self.help = self._init_help() 136 self.ok = self._init_ok() 137 self.setLayout(self._layout) 138 139 self.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) 140 self.setWindowTitle('sya (split youtube audio)') 141 self.setFixedSize(int(self.width() / 1.5), self.minimumHeight()) 142 143 def _init_url(self): 144 label = self.labels['url'] 145 self._layout.addWidget(qtwidg.QLabel(label, self), 0, 0) 146 line_edit = new_linedit(self, self.set_url, self.values['url']) 147 self._layout.addWidget(line_edit, 0, 1, 1, 3) 148 return line_edit 149 150 def _init_tracklist(self): 151 label = self.labels['tracklist'] 152 self._layout.addWidget(qtwidg.QLabel(label, self), 1, 0, qtcore.Qt.AlignmentFlag.AlignTop) 153 text_edit = qtwidg.QPlainTextEdit(self) 154 text_edit.setPlainText(self.values['tracklist']) 155 text_edit.textChanged.connect(self.set_tracklist) 156 self._layout.addWidget(text_edit, 1, 1, 1, 3) 157 return text_edit 158 159 def _init_format(self): 160 label = self.labels['format'] 161 self._layout.addWidget(qtwidg.QLabel(label, self), 2, 0) 162 combo_box = new_combobox(self, self.availableFormats, self.values['format'], self.set_format) 163 self._layout.addLayout(combo_box, 2, 1) 164 return combo_box 165 166 def _init_quality(self): 167 label = self.labels['quality'] 168 self._layout.addWidget(qtwidg.QLabel(label, self), 3, 0) 169 combo_box = new_combobox(self, self.availableQualities, self.values['quality'], self.set_quality) 170 self._layout.addLayout(combo_box, 3, 1) 171 return combo_box 172 173 def _init_spacer(self): 174 size_policy = qtwidg.QSizePolicy.Expanding 175 spacer = qtwidg.QSpacerItem(int(self.width() / 4), 0, size_policy, size_policy) 176 self._layout.addItem(spacer) 177 178 def _init_keep(self): 179 label = self.labels['keep'] 180 checkbox = qtwidg.QCheckBox(label, self) 181 if self.values['keep']: 182 checkbox.setChecked(True) 183 self._layout.addWidget(checkbox, 2, 3, 2, 1) 184 checkbox.toggled.connect(self.toggle_keep) 185 return checkbox 186 187 def _init_output(self): 188 label = self.labels['output'] 189 self._layout.addWidget(qtwidg.QLabel(label, self), 4, 0) 190 layout, lineedit = new_filepicker(self, self.select_output, self.set_output, self.values['output'], 'folder') 191 self._layout.addLayout(layout, 4, 1, 1, 3) 192 return lineedit 193 194 def _init_exit(self): 195 btn = qtwidg.QPushButton('Exit') 196 self._layout.addWidget(btn, 5, 0) 197 return btn 198 199 def _init_help(self): 200 btn = qtwidg.QPushButton('Help') 201 self._layout.addWidget(btn, 5, 1) 202 return btn 203 204 def _init_ok(self): 205 btn = qtwidg.QPushButton('OK') 206 self._layout.addWidget(btn, 5, 3) 207 return btn 208 209 # callbacks 210 def set_url(self, text): 211 self.values['url'] = text 212 self.update_ok() 213 214 def set_tracklist(self): 215 self.values['tracklist'] = self.tracklist.toPlainText() 216 self.update_ok() 217 218 def select_output(self): 219 dialog = qtwidg.QFileDialog() 220 dialog.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) 221 file = dialog.getExistingDirectory(self, 'Select directory', os.path.expanduser('~'), 222 qtwidg.QFileDialog.DontUseNativeDialog) 223 if len(file) > 0: 224 self.set_output(file) 225 226 def set_output(self, text): 227 self.values['output'] = text 228 if self.output.text() != self.values['output']: 229 self.output.setText(self.values['output']) 230 self.update_ok() 231 232 def set_format(self, option): 233 if option in self.availableFormats: 234 self.values['format'] = option 235 self.update_ok() 236 237 def set_quality(self, option): 238 if option in self.availableQualities: 239 self.values['quality'] = option 240 self.update_ok() 241 242 def toggle_keep(self): 243 self.values['keep'] = not self.values['keep'] 244 self.update_ok() 245 246 def update_ok(self): 247 self.ok.setEnabled(len(self.values['url']) > 0 and len(self.values['tracklist']) > 0 and 248 len(self.values['output']) > 0) 249 250 251 class SyaGuiHelp(qtwidg.QTextEdit): 252 def __init__(self, parent, options): 253 super().__init__() 254 self.setParent(parent) 255 self.setWindowFlag(qtcore.Qt.WindowType.Dialog) 256 self.options = options 257 self.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) 258 self.setWindowTitle('sya help') 259 with open(resource_path("HELP.md")) as f: 260 self.setMarkdown(f.read()) 261 self.resize(500, 500) 262 self.setReadOnly(True) 263 264 def show(self): 265 self.move(self.options.x() - self.options.width() - 100, self.options.y() - self.options.height()) 266 self.options.help.setEnabled(False) 267 super().show() 268 269 def hide(self, sig=''): 270 self.options.help.setEnabled(True) 271 272 273 class SyaGuiLogger(qtwidg.QWidget): 274 def __init__(self, parent): 275 super().__init__() 276 self.setParent(parent) 277 self.setWindowFlag(qtcore.Qt.WindowType.Dialog) 278 self._layout = qtwidg.QGridLayout() 279 self.textbox = self._init_textbox() 280 self.cancel = self._init_cancel() 281 self.warning = self._init_warning() 282 self.done = self._init_done() 283 self.setLayout(self._layout) 284 285 self.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) 286 self.resize(800, 400) 287 288 def _init_textbox(self): 289 textbox = qtwidg.QPlainTextEdit() 290 textbox.setReadOnly(True) 291 textbox.setLineWrapMode(qtwidg.QPlainTextEdit.NoWrap) 292 self._layout.addWidget(textbox, 1, 0, 1, 5) 293 return textbox 294 295 def _init_cancel(self): 296 btn = qtwidg.QPushButton('Cancel') 297 self._layout.addWidget(btn, 2, 0) 298 return btn 299 300 def _init_warning(self): 301 label = qtwidg.QLabel('This might take a while. You can click "Done" when it\'s finished.') 302 self._layout.addWidget(label, 2, 1, 1, 2) 303 return label 304 305 def _init_done(self): 306 btn = qtwidg.QPushButton('Done') 307 btn.setEnabled(False) 308 self._layout.addWidget(btn, 2, 4) 309 return btn 310 311 def hide(self, sig=''): 312 self.textbox.clear() 313 super().hide() 314 315 def log(self, message): 316 self.textbox.moveCursor(qtgui.QTextCursor.End) 317 self.textbox.textCursor().insertText(message) 318 self.textbox.ensureCursorVisible() 319 320 321 class SyaGui(): 322 def __init__(self, fn_sya, fn_sya_args): 323 self.fnSya = fn_sya 324 self.fnSyaArgs = fn_sya_args 325 self.main_t = SyaGuiThread(self.fnSya, self.fnSyaArgs) 326 self.running = 0 327 328 self.options = SyaGuiOptions(self.fnSyaArgs) 329 self.help = SyaGuiHelp(self.options, self.options) 330 self.logger = SyaGuiLogger(self.options) 331 self._init_hooks() 332 333 self.options.show() 334 335 def _init_hooks(self): 336 self.options.closeEvent = self.quit 337 self.options.exit.clicked.connect(self.options.close) 338 self.options.help.clicked.connect(self.help.show) 339 self.options.ok.clicked.connect(self.main) 340 341 self.help.closeEvent = self.help.hide 342 343 self.logger.cancel.clicked.connect(self.cancel) 344 self.logger.done.clicked.connect(self.finish) 345 sys.stdout = SyaGuiLogStream(txt=self.logger.log) 346 347 def quit(self, event): 348 sys.stdout = sys.__stdout__ 349 while self.running > 0: 350 self.cancel() 351 self.help.close() 352 self.logger.close() 353 self.options.close() 354 355 def cancel(self): 356 if self.running > 0: 357 self.main_t.terminate() 358 self.main_t.wait() 359 self.running -= 1 360 self.options.ok.setEnabled(True) 361 self.logger.hide() 362 363 def finish(self): 364 self.options.url.setText('') 365 self.options.tracklist.setPlainText('') 366 self.options.output.setText('') 367 self.options.ok.setEnabled(True) 368 self.logger.hide() 369 370 def pre_main(self): 371 x = self.options.x() + self.options.width() + 50 372 y = self.options.y() - self.options.height() 373 self.logger.move(x, y) 374 self.logger.setWindowTitle('sya {}'.format(self.fnSyaArgs.output)) 375 self.options.ok.setEnabled(False) 376 self.logger.done.setEnabled(False) 377 378 def post_main(self): 379 self.logger.done.setEnabled(True) 380 381 def main(self): 382 tracklist = generate_tracklist(self.options.values['url'], self.options.values['tracklist']) 383 self.fnSyaArgs.tracklist = [tracklist] # sya expects a list here 384 self.fnSyaArgs.format = self.options.values['format'] 385 self.fnSyaArgs.quality = self.options.values['quality'] 386 self.fnSyaArgs.keep = self.options.values['keep'] 387 self.fnSyaArgs.output = self.options.values['output'] 388 389 self.main_t.started.connect(self.pre_main) 390 self.main_t.finished.connect(self.post_main) 391 392 self.logger.show() 393 self.running += 1 394 self.main_t.start() 395 396 397 if __name__ == '__main__': 398 app = qtwidg.QApplication(sys.argv) 399 400 args = sya.parse_args() 401 if args.tracklist is None or len(args.tracklist) == 0: 402 args.tracklist = '' 403 else: 404 args.tracklist = args.tracklist[0] 405 if args.output is None: 406 args.output = '' 407 if args.youtubedl is None: 408 args.youtubedl = resource_path('yt-dlp') if sys.platform != 'win32' else resource_path('yt-dlp.exe') 409 if args.ffmpeg is None: 410 args.ffmpeg = resource_path('ffmpeg') if sys.platform != 'win32' else resource_path('ffmpeg.exe') 411 gui = SyaGui(sya.sya, args) 412 413 sys.exit(app.exec_())