sya

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

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_())