sya

split youtube audio tracks, with an optional pyqt gui
git clone git://src.gearsix.net/syasya.zip
Log | Files | Refs | Atom | README

sya-pyqt.py (raw) (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_())