sya

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

commit 81ce7e5f9bf29c8d9a5a8a87fc7c0efc9191006d
parent 6b10a06a74456e2b067e503492658c21614f50bb
Author: gearsix <gearsix@tuta.io>
Date:   Wed, 14 Dec 2022 12:37:08 +0000

Merge branch 'develop' of notabug.org:gearsix/sya into develop

Diffstat:
MHELP.md | 81+++++++++++++++++++++++++++++++++++++------------------------------------------
MREADME.md | 2+-
MTODO.txt | 5++---
Mscreenshot.PNG | 0
Msya-pyqt.py | 556+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msya.py | 32+++++++++++++++++++-------------
6 files changed, 365 insertions(+), 311 deletions(-)

diff --git a/HELP.md b/HELP.md @@ -3,63 +3,58 @@ **sya - split youtube audio**, downloads, converts & splits audio from youtube videos into multiple audio tracks. + ## Overview -To work sya requires some manual work: **tracklist** information. For more details on this, see **Tracklists** below.<br/> +To work sya requires some extra work: **tracklist** information. For more details on this, see **Tracklists** below.<br/> The rest of the options can be configured but are provided with defaults. Here's an overview of the options: -- **Tracklist** - the text file containing tracklist information +- **URL** - the URL of the video to fetch audio from +- **Tracklist** - tracklist information (see below) - **Format** - set the format to convert the audio to - **Quality** - set the audio quality to download in, for reference 5 is equal to *128k* -- **Keep unsplit file** - keep the downloaded audio file (before it gets split up) +- **Keep un-split file** - keep the downloaded audio file (before it gets split up) - **Output** - the directory to download the audio track to and split it into multiple tracks -The resulting files can be found at the *Output:* filepath on your system. +The resulting files can be found at the *Output* path on your system. -If you've found a bug or want to suggest improvements, email: `gearsix@tuta.io` -## Tracklists +## Tracklist -A tracklist is just a text file some where on your system. -It should contains: +A tracklist is just some text that provides the *title* and *timestamp* of the tracks. -- A *youtube URL* to download the audio from, this should be on the first line. -- Timestamps and titles for each track to split, there should be one timestamp and one title per-track (this can usually be found in the youtube video description or a top comment). +There should be one timestamp and one title per-track (this can usually be found in the video description or a top comment). **Example** -Below you can see the contents of an example playlist.<br/> -Try saving it to a text file on your computer and as a test if you like. - - https://www.youtube.com/watch?v=LbjcaMAhJRQ - Sneaky Snitch (0:00) - Fluffing a Duck (2:16) - Cipher (3:24) - Scheming Weasel (7:15) - Carefree (8:44) - Thatched Villagers (12:09) - Monkeys Spinning Monkeys (16:15) - Wallpaper (18:20) - Pixel Peeker Polka (21:59) - Killing Time (25:21) - Hitman (28:46) - The Cannery (32:07) - Cut and Run (35:09) - Life of Riley (38:44) - Quirky Dog (42:39) - The Complex (45:08) - Hyperfun (49:35) - Black Vortex (53:29) - Rock on Chicago (56:19) - Volatile Reaction (57:58) - On the Ground (1:00:44) - Wagon Wheel (electronic) (1:03:23) - Call to Adventure (1:08:26) - Hustle (1:12:33) - Cupids Revenge (1:14:34) - Dirt Rhodes (1:16:20) - Rhinoceros (1:18:20) - Who Likes to Party (1:21:43) - Spazzmatica Polka (1:26:01) + Sneaky Snitch (0:00) + Fluffing a Duck (2:16) + Cipher (3:24) + Scheming Weasel (7:15) + Carefree (8:44) + Thatched Villagers (12:09) + Monkeys Spinning Monkeys (16:15) + Wallpaper (18:20) + Pixel Peeker Polka (21:59) + Killing Time (25:21) + Hitman (28:46) + The Cannery (32:07) + Cut and Run (35:09) + Life of Riley (38:44) + Quirky Dog (42:39) + The Complex (45:08) + Hyperfun (49:35) + Black Vortex (53:29) + Rock on Chicago (56:19) + Volatile Reaction (57:58) + On the Ground (1:00:44) + Wagon Wheel (electronic) (1:03:23) + Call to Adventure (1:08:26) + Hustle (1:12:33) + Cupids Revenge (1:14:34) + Dirt Rhodes (1:16:20) + Rhinoceros (1:18:20) + Who Likes to Party (1:21:43) + Spazzmatica Polka (1:26:01) diff --git a/README.md b/README.md @@ -100,7 +100,7 @@ To **uninstall**, just remove all files recorded to *./install.txt*. Some people don't like the cli and I wanted to play with PyQt, so sya-pyqt wraps a nice GUI around the *sya.py* runtime. -![screenshot](screenshot.png "sya-pyqt on Windows) +![screenshot](./screenshot.PNG "sya-pyqt on Windows") ### Development diff --git a/TODO.txt b/TODO.txt @@ -1,2 +1 @@ -- QSpacerItem should be dynamic, not static size -- On exit, SyaGuiLogStream gets delted (runtime error?) -\ No newline at end of file +- Add all SyaGui elements under single layout +\ No newline at end of file diff --git a/screenshot.PNG b/screenshot.PNG Binary files differ. diff --git a/sya-pyqt.py b/sya-pyqt.py @@ -3,8 +3,7 @@ # std import os import sys -import subprocess -import shutil +import tempfile # sya import sya # pip @@ -16,7 +15,7 @@ import PyQt5.QtGui as qtgui def resource_path(relative_path): try: base_path = sys._MEIPASS - except Exception: + except AttributeError: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) @@ -29,11 +28,56 @@ def center_widget(widget): round(sg.height() / 2) - round(wg.height() / 2)) +def new_combobox(parent, items, default_item, fn_update): + combobox = qtwidg.QComboBox(parent) + for i in items: + combobox.addItem(i) + if default_item in items: + combobox.setCurrentIndex(items.index(default_item)) + combobox.activated[str].connect(fn_update) + + layout = qtwidg.QHBoxLayout() + layout.addWidget(combobox) + + return layout + + +def new_linedit(parent, fn_update, default_value=''): + line_edit = qtwidg.QLineEdit(parent) + line_edit.setText(default_value) + line_edit.textChanged.connect(fn_update) + return line_edit + + +def new_btn(parent, fn_select, icon=''): + btn_icon = qtgui.QIcon(resource_path('{}.png'.format(icon))) + btn = qtwidg.QPushButton(btn_icon, '', parent) + btn.clicked.connect(fn_select) + return btn + + +def new_filepicker(parent, fn_select, fn_update, default_value='', icon=''): + layout = qtwidg.QHBoxLayout() + lineedit = new_linedit(parent, fn_update, default_value) + layout.addWidget(lineedit) + layout.addWidget(new_btn(parent, fn_select, icon)) + return layout, lineedit + + +def generate_tracklist(url, tracklist): + fd, fpath = tempfile.mkstemp() + with open(fd, 'w') as f: + f.write(url) + f.write('\n') + f.writelines(t.encode('utf-8').decode('ascii', 'ignore') for t in tracklist) + return fpath + + class SyaGuiThread(qtcore.QThread): - def __init__(self, fn, args=None): + def __init__(self, fn, fn_args=None): super().__init__() self.fn = fn - self.args = args + self.args = fn_args def run(self): if self.args is None: @@ -49,296 +93,306 @@ class SyaGuiLogStream(qtcore.QObject): self.txt.emit(str(txt)) -def sya_gui_combobox(parent, label, items, default_item, fn_update): - label = qtwidg.QLabel(label, parent) +class SyaGuiOptions(qtwidg.QWidget): + def __init__(self, init_values): + super().__init__() - combobox = qtwidg.QComboBox(parent) - for i in items: - combobox.addItem(i) - if default_item in items: - combobox.setCurrentIndex(items.index(default_item)) - combobox.activated[str].connect(fn_update) + url = '' + tracklist = '' + output = '' + if os.path.exists(init_values.tracklist): + url, tracklist = sya.load_tracklist(init_values.tracklist) + if init_values.output == '' and init_values.tracklist != '': + output = os.path.join(os.getcwd(), os.path.splitext(os.path.basename(init_values.tracklist))[0]) + + self.labels = { + 'url': 'URL:', + 'tracklist': 'Tracklist:', + 'format': 'Format:', + 'quality': 'Quality:', + 'keep': 'Keep un-split file', + 'output': 'Output:' } + self.values = { + 'url': url, + 'tracklist': '\n'.join(tracklist), + 'format': init_values.format, + 'quality': init_values.quality, + 'keep': init_values.keep, + 'output': output } - layout = qtwidg.QHBoxLayout() - layout.addWidget(label) - layout.addWidget(combobox) + self.availableFormats = ['mp3', 'wav', 'ogg', 'aac'] + self.availableQualities = ['0 (better)', '1', '2', '3', '4', '5', '6', '7', '8', '9 (worse)'] - return layout + self._layout = qtwidg.QGridLayout() + self.url = self._init_url() + self.tracklist = self._init_tracklist() + self.format = self._init_format() + self.quality = self._init_quality() + self._init_spacer() + self.keep = self._init_keep() + self.output = self._init_output() + self.exit = self._init_exit() + self.help = self._init_help() + self.ok = self._init_ok() + self.setLayout(self._layout) + + self.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) + self.setWindowTitle('sya (split youtube audio)') + self.setFixedSize(int(self.width() / 1.5), self.minimumHeight()) + + def _init_url(self): + label = self.labels['url'] + self._layout.addWidget(qtwidg.QLabel(label, self), 0, 0) + line_edit = new_linedit(self, self.set_url, self.values['url']) + self._layout.addWidget(line_edit, 0, 1, 1, 3) + return line_edit + + def _init_tracklist(self): + label = self.labels['tracklist'] + self._layout.addWidget(qtwidg.QLabel(label, self), 1, 0, qtcore.Qt.AlignmentFlag.AlignTop) + text_edit = qtwidg.QPlainTextEdit(self) + text_edit.setPlainText(self.values['tracklist']) + text_edit.textChanged.connect(self.set_tracklist) + self._layout.addWidget(text_edit, 1, 1, 1, 3) + return text_edit + + def _init_format(self): + label = self.labels['format'] + self._layout.addWidget(qtwidg.QLabel(label, self), 2, 0) + combo_box = new_combobox(self, self.availableFormats, self.values['format'], self.set_format) + self._layout.addLayout(combo_box, 2, 1) + return combo_box + + def _init_quality(self): + label = self.labels['quality'] + self._layout.addWidget(qtwidg.QLabel(label, self), 3, 0) + combo_box = new_combobox(self, self.availableQualities, self.values['quality'], self.set_quality) + self._layout.addLayout(combo_box, 3, 1) + return combo_box + + def _init_spacer(self): + size_policy = qtwidg.QSizePolicy.Expanding + spacer = qtwidg.QSpacerItem(int(self.width() / 4), 0, size_policy, size_policy) + self._layout.addItem(spacer) + + def _init_keep(self): + label = self.labels['keep'] + checkbox = qtwidg.QCheckBox(label, self) + if self.values['keep']: + checkbox.setChecked(True) + self._layout.addWidget(checkbox, 2, 3, 2, 1) + checkbox.toggled.connect(self.toggle_keep) + return checkbox + + def _init_output(self): + label = self.labels['output'] + self._layout.addWidget(qtwidg.QLabel(label, self), 4, 0) + layout, lineedit = new_filepicker(self, self.select_output, self.set_output, self.values['output'], 'folder') + self._layout.addLayout(layout, 4, 1, 1, 3) + return lineedit + + def _init_exit(self): + btn = qtwidg.QPushButton('Exit') + self._layout.addWidget(btn, 5, 0) + return btn + def _init_help(self): + btn = qtwidg.QPushButton('Help') + self._layout.addWidget(btn, 5, 1) + return btn -def sya_gui_filepicker(parent, label, fn_select, fn_update, default_value='', icon=''): - label = qtwidg.QLabel(label, parent) + def _init_ok(self): + btn = qtwidg.QPushButton('OK') + self._layout.addWidget(btn, 5, 3) + return btn - lineEdit = qtwidg.QLineEdit(parent) - lineEdit.setText(default_value) - lineEdit.textChanged.connect(fn_update) + # callbacks + def set_url(self, text): + self.values['url'] = text + self.update_ok() - btnIcon = qtgui.QIcon(resource_path('{}.png'.format(icon))) - btn = qtwidg.QPushButton(btnIcon, '', parent) - btn.clicked.connect(fn_select) + def set_tracklist(self): + self.values['tracklist'] = self.tracklist.toPlainText() + self.update_ok() - layout = qtwidg.QHBoxLayout() - layout.addWidget(label) - layout.addWidget(lineEdit) - layout.addWidget(btn) + def select_output(self): + dialog = qtwidg.QFileDialog() + dialog.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) + file = dialog.getExistingDirectory(self, 'Select directory', os.path.expanduser('~'), + qtwidg.QFileDialog.DontUseNativeDialog) + if len(file) > 0: + self.set_output(file) - return layout, lineEdit + def set_output(self, text): + self.values['output'] = text + if self.output.text() != self.values['output']: + self.output.setText(self.values['output']) + self.update_ok() + def set_format(self, option): + if option in self.availableFormats: + self.values['format'] = option + self.update_ok() -class SyaGui(qtwidg.QMainWindow): - def __init__(self, fn_sya, fn_sya_args): + def set_quality(self, option): + if option in self.availableQualities: + self.values['quality'] = option + self.update_ok() + + def toggle_keep(self): + self.values['keep'] = not self.values['keep'] + self.update_ok() + + def update_ok(self): + self.ok.setEnabled(len(self.values['url']) > 0 and len(self.values['tracklist']) > 0 and + len(self.values['output']) > 0) + + +class SyaGuiHelp(qtwidg.QTextEdit): + def __init__(self, parent, options): super().__init__() + self.setParent(parent) + self.setWindowFlag(qtcore.Qt.WindowType.Dialog) + self.options = options + self.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) + self.setWindowTitle('sya help') + with open(resource_path("HELP.md")) as f: + self.setMarkdown(f.read()) + self.resize(500, 500) + self.setReadOnly(True) + + def show(self): + self.move(self.options.x() - self.options.width() - 100, self.options.y() - self.options.height()) + self.options.help.setEnabled(False) + super().show() + + def hide(self, sig=''): + self.options.help.setEnabled(True) + +class SyaGuiLogger(qtwidg.QWidget): + def __init__(self, parent): + super().__init__() + self.setParent(parent) + self.setWindowFlag(qtcore.Qt.WindowType.Dialog) + self._layout = qtwidg.QGridLayout() + self.textbox = self._init_textbox() + self.cancel = self._init_cancel() + self.warning = self._init_warning() + self.done = self._init_done() + self.setLayout(self._layout) + + self.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) + self.resize(800, 400) + + def _init_textbox(self): + textbox = qtwidg.QPlainTextEdit() + textbox.setReadOnly(True) + textbox.setLineWrapMode(qtwidg.QPlainTextEdit.NoWrap) + self._layout.addWidget(textbox, 1, 0, 1, 5) + return textbox + + def _init_cancel(self): + btn = qtwidg.QPushButton('Cancel') + self._layout.addWidget(btn, 2, 0) + return btn + + def _init_warning(self): + label = qtwidg.QLabel('This might take a while. You can click "Done" when it\'s finished.') + self._layout.addWidget(label, 2, 1, 1, 2) + return label + + def _init_done(self): + btn = qtwidg.QPushButton('Done') + btn.setEnabled(False) + self._layout.addWidget(btn, 2, 4) + return btn + + def hide(self, sig=''): + self.textbox.clear() + super().hide() + + def log(self, message): + self.textbox.moveCursor(qtgui.QTextCursor.End) + self.textbox.textCursor().insertText(message) + self.textbox.ensureCursorVisible() + + +class SyaGui(): + def __init__(self, fn_sya, fn_sya_args): self.fnSya = fn_sya self.fnSyaArgs = fn_sya_args + self.main_t = SyaGuiThread(self.fnSya, self.fnSyaArgs) + self.running = 0 - self.availableFormats = ['mp3', 'wav', 'ogg', 'aac'] - self.availableQualities = ['0 (better)', '1', '2', '3', '4', '5', '6', '7', '8', '9 (worse)'] + self.options = SyaGuiOptions(self.fnSyaArgs) + self.help = SyaGuiHelp(self.options, self.options) + self.logger = SyaGuiLogger(self.options) + self._init_hooks() - self._init_options_value() - self._init_options() - self._init_help() - self._init_logger() - + self.options.show() + + def _init_hooks(self): self.options.closeEvent = self.quit - self.optionsHelp.clicked.connect(self.show_help) - self.optionsOk.clicked.connect(self.main) - self.help.closeEvent = self.hide_help - self.loggerCancel.clicked.connect(self.cancel) - self.loggerDone.clicked.connect(self.done) + self.options.exit.clicked.connect(self.options.close) + self.options.help.clicked.connect(self.help.show) + self.options.ok.clicked.connect(self.main) - sys.stdout = SyaGuiLogStream(txt=self.log) - self.running = 0 + self.help.closeEvent = self.help.hide - # Runtime Methods - def log(self, msg): - self.loggerTextbox.moveCursor(qtgui.QTextCursor.End) - self.loggerTextbox.textCursor().insertText(msg) - self.loggerTextbox.ensureCursorVisible() + self.logger.cancel.clicked.connect(self.cancel) + self.logger.done.clicked.connect(self.finish) + sys.stdout = SyaGuiLogStream(txt=self.logger.log) + + def quit(self, event): + sys.stdout = sys.__stdout__ + while self.running > 0: + self.cancel() + self.help.close() + self.logger.close() + self.options.close() def cancel(self): if self.running > 0: self.main_t.terminate() self.main_t.wait() self.running -= 1 + self.options.ok.setEnabled(True) self.logger.hide() - self.loggerTextbox.clear() - - def quit(self, event): - sys.stdout = sys.__stdout__ - if self.running > 0: - self.cancel() - self.options.close() - self.logger.close() - self.close() - def done(self): - self.set_tracklist('') - self.set_output('') - self.optionsOk.setEnabled(True) + def finish(self): + self.options.url.setText('') + self.options.tracklist.setPlainText('') + self.options.output.setText('') + self.options.ok.setEnabled(True) self.logger.hide() - self.loggerTextbox.clear() - - def show_help(self): - x = self.options.x() - self.options.width() - 100 - y = self.options.y() - self.options.height() - self.help.move(x, y) - self.help.show() - self.optionsHelp.setEnabled(False) - - def hide_help(self, signal): - self.help.hide() - self.optionsHelp.setEnabled(True) - def preMain(self): + def pre_main(self): x = self.options.x() + self.options.width() + 50 y = self.options.y() - self.options.height() self.logger.move(x, y) self.logger.setWindowTitle('sya {}'.format(self.fnSyaArgs.output)) - self.optionsOk.setEnabled(False) - self.loggerDone.setEnabled(False) + self.options.ok.setEnabled(False) + self.logger.done.setEnabled(False) - def postMain(self): - self.loggerDone.setEnabled(True) + def post_main(self): + self.logger.done.setEnabled(True) def main(self): - self.fnSyaArgs.tracklist = self.optionsValue[self.tracklistLabel] - self.fnSyaArgs.format = self.optionsValue[self.formatLabel] - self.fnSyaArgs.quality = self.optionsValue[self.qualityLabel] - self.fnSyaArgs.keep = self.optionsValue[self.keepLabel] - self.fnSyaArgs.output = self.optionsValue[self.outputLabel] + self.fnSyaArgs.tracklist = generate_tracklist(self.options.values['url'], self.options.values['tracklist']) + self.fnSyaArgs.format = self.options.values['format'] + self.fnSyaArgs.quality = self.options.values['quality'] + self.fnSyaArgs.keep = self.options.values['keep'] + self.fnSyaArgs.output = self.options.values['output'] - self.main_t = SyaGuiThread(self.fnSya, self.fnSyaArgs) - self.main_t.started.connect(self.preMain) - self.main_t.finished.connect(self.postMain) + self.main_t.started.connect(self.pre_main) + self.main_t.finished.connect(self.post_main) self.logger.show() self.running += 1 self.main_t.start() - # optionsValue - def _init_options_value(self): - self.tracklistLabel = 'Tracklist:' - self.formatLabel = 'Format:' - self.qualityLabel = 'Quality:' - self.keepLabel = 'Keep unsplit file' - self.outputLabel = 'Output:' - self.optionsValue = { - self.tracklistLabel: self.fnSyaArgs.tracklist, - self.formatLabel: self.fnSyaArgs.format, - self.qualityLabel: self.fnSyaArgs.quality, - self.keepLabel: self.fnSyaArgs.keep, - self.outputLabel: self.fnSyaArgs.output - } - - # options - def _init_options(self): - self.options = qtwidg.QWidget() - self.optionsOk = qtwidg.QPushButton('OK') - self.optionsHelp = qtwidg.QPushButton('Help') - - layout = qtwidg.QGridLayout() - layout.addLayout(self._init_options_tracklist(), 0, 0, 1, 3) - layout.addLayout(self._init_options_format(), 1, 0) - layout.addLayout(self._init_options_quality(), 2, 0) - layout.addItem(qtwidg.QSpacerItem(int(self.options.width()/4), 0, qtwidg.QSizePolicy.Expanding, qtwidg.QSizePolicy.Expanding)) - layout.addWidget(self._init_options_keep(), 1, 2, 2, 1) - layout.addLayout(self._init_options_output(), 3, 0, 1, 3) - layout.addWidget(self.optionsHelp, 4, 0) - layout.addWidget(self.optionsOk, 4, 2) - - self.options.setLayout(layout) - self.options.setWindowTitle('sya (split youtube audio)') - self.options.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) - - self.update_options_ok() - self.options.show() - def _init_options_tracklist(self): - label = self.tracklistLabel - layout, self.optionsTracklist = sya_gui_filepicker(self.options, label, self.select_tracklist, self.set_tracklist, self.optionsValue[label], 'file') - return layout - - def _init_options_format(self): - label = self.formatLabel - self.optionsFormat = sya_gui_combobox(self.options, label, self.availableFormats, self.optionsValue[label], self.set_format) - return self.optionsFormat - - def _init_options_quality(self): - label = self.qualityLabel - self.optionsQuality = sya_gui_combobox(self.options, label, self.availableQualities, self.optionsValue[label], self.set_quality) - return self.optionsQuality - - def _init_options_output(self): - label = self.outputLabel - layout, self.optionsOutput = sya_gui_filepicker(self.options, label, self.select_output, self.set_output, self.optionsValue[label], 'folder') - if self.optionsValue[self.tracklistLabel] != None and self.optionsValue[self.tracklistLabel] != '': - self.set_output(os.path.splitext(self.optionsValue[self.tracklistLabel])[0]) - return layout - - def _init_options_keep(self): - label = self.keepLabel - self.optionsKeep = qtwidg.QCheckBox(label, self.options) - if self.optionsValue[label]: - self.optionsKeep.setChecked(True) - self.optionsKeep.toggled.connect(self.toggle_keep) - return self.optionsKeep - - # Options Callbacks - def select_tracklist(self): - dialog = qtwidg.QFileDialog() - dialog.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) - file = dialog.getOpenFileName(self.options, 'Select a tracklist', os.path.expanduser('~'), "Text file (*.txt)", None, qtwidg.QFileDialog.DontUseNativeDialog) - if len(file) > 0: - self.set_tracklist(file[0]) - - def set_tracklist(self, text): - self.optionsValue[self.tracklistLabel] = text - self.optionsTracklist.setText(text) - self.set_output(os.path.splitext(text)[0]) - self.update_options_ok() - - def select_output(self): - dialog = qtwidg.QFileDialog() - dialog.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) - file = dialog.getExistingDirectory(self.options, 'Select directory', os.path.expanduser('~'), qtwidg.QFileDialog.DontUseNativeDialog) - if len(file) > 0: - self.set_output(file[0]) - - def set_output(self, text): - self.optionsValue[self.outputLabel] = text - self.optionsOutput.setText(text) - self.update_options_ok() - - def set_format(self, option): - if option not in self.availableFormats: - return - self.optionsValue[self.formatLabel] = option - self.update_options_ok() - - def set_quality(self, option): - if option not in self.availableQualities: - return - self.optionsValue[self.qualityLabel] = option - self.update_options_ok() - - def toggle_keep(self): - self.optionsValue[self.keepLabel] = not self.optionsValue[self.keepLabel] - self.update_options_ok() - - def update_options_ok(self): - tracklist = self.optionsValue[self.tracklistLabel] - output = self.optionsValue[self.outputLabel] - if os.path.exists(tracklist) and len(output) > 0: - self.optionsOk.setEnabled(True) - else: - self.optionsOk.setEnabled(False) - - # Help Widget - def _init_help(self): - self.help = qtwidg.QTextEdit() - self.help.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) - self.help.setWindowTitle('sya help') - with open(resource_path("HELP.md")) as f: - self.help.setMarkdown(f.read()) - self.help.resize(500, 500) - self.help.setReadOnly(True) - return - - # Logger Widget - def _init_logger(self): - layout = qtwidg.QGridLayout() - layout.addWidget(self._init_logger_textbox(), 1, 0, 1, 5) - layout.addWidget(self._init_logger_cancel(), 2, 0) - layout.addWidget(self._init_logger_warning(), 2, 1, 1, 2) - layout.addWidget(self._init_logger_done(), 2, 4) - - self.logger = qtwidg.QWidget() - self.logger.setLayout(layout) - self.logger.setWindowIcon(qtgui.QIcon(resource_path('sya.png'))) - self.logger.resize(800, 400) - - def _init_logger_textbox(self): - self.loggerTextbox = qtwidg.QPlainTextEdit() - self.loggerTextbox.setReadOnly(True) - self.loggerTextbox.setLineWrapMode(qtwidg.QPlainTextEdit.NoWrap) - return self.loggerTextbox - - def _init_logger_cancel(self): - self.loggerCancel = qtwidg.QPushButton('Cancel') - return self.loggerCancel - - @staticmethod - def _init_logger_warning(): - return qtwidg.QLabel('This might take a while. You can click "Done" when it\'s finished.') - - def _init_logger_done(self): - self.loggerDone = qtwidg.QPushButton('Done') - self.loggerDone.setEnabled(False) - return self.loggerDone - - -# Main if __name__ == '__main__': app = qtwidg.QApplication(sys.argv) diff --git a/sya.py b/sya.py @@ -9,15 +9,18 @@ import sys Version = 'v1.0.1' +Shell = True if sys.platform == 'win32' else False + UnsafeFilenameChars = re.compile('[/\\?%*:|\"<>\x7F\x00-\x1F]') TrackNum = re.compile('(?:\d+.? ?-? ?)') -Timestamp = re.compile('(?:[\t ]+?)?[\[\(]+?((\d+[:.])+(\d+))[\]\)]?(?:[\t ]+)?') +Timestamp = re.compile('(?: - )?(?:[\t ]+)?(?:[\[\(]+)?((\d+[:.])+(\d+))(?:[\]\)])?(?:[\t ]+)?(?: - )?') class TracklistItem: def __init__(self, timestamp, title): self.timestamp = timestamp self.title = title + # utilities def error_exit(msg): print('exit failure "{}"'.format(msg)) @@ -26,7 +29,7 @@ def error_exit(msg): def check_bin(*binaries): for b in binaries: try: - subprocess.call([b], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, shell=False) + subprocess.call([b], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, shell=Shell) except: error_exit('failed to execute {}'.format(b)) @@ -40,20 +43,23 @@ def get_audio(youtubedl, url, outdir, format='mp3', quality='320K', keep=True, f if keep == True: cmd.append('-k') cmd.append(url) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=Shell) for line in p.stdout.readlines(): print(' {}'.format(line.decode('utf-8', errors='ignore').strip())) return '{}.{}'.format(fname, format) def load_tracklist(path): tracklist = [] + url = '' tracklist_file = open(path, mode = 'r') - for t in tracklist_file.readlines(): + for i, t in enumerate(tracklist_file.readlines()): t = t.strip('\n\t ') - if len(t) > 0: + if i == 0: + url = t + else: tracklist.append(t) tracklist_file.close() - return tracklist + return url, tracklist def parse_tracks(tracklist): tracks = [] @@ -95,7 +101,7 @@ def read_tracklen(ffmpeg, track_fpath): cmd = [ffmpeg, '-v', 'quiet', '-stats', '-i', track_fpath, '-f', 'null', '-'] length = '00:00' try: - ret = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False) + ret = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=Shell) length = str(ret).split('\\r') # some nasty string manip. to extract length (printed to stderr) if sys.platform == 'win32': @@ -110,7 +116,7 @@ def read_tracklen(ffmpeg, track_fpath): def split_tracks(ffmpeg, audio_fpath, audio_len, tracks, format='mp3', outpath='out'): print('Splitting...') for i, t in enumerate(tracks): - outfile = '{}/{} - {}.{}'.format(outpath, str(i+1).zfill(2), t.title.strip(' - '), format) + outfile = '{}{}{} - {}.{}'.format(outpath, os.path.sep, str(i+1).zfill(2), t.title.strip(' - '), format) end = audio_len if i < len(tracks)-1: end = tracks[i+1].timestamp @@ -118,7 +124,7 @@ def split_tracks(ffmpeg, audio_fpath, audio_len, tracks, format='mp3', outpath=' cmd = ['ffmpeg', '-nostdin', '-y', '-loglevel', 'error', '-i', audio_fpath, '-ss', t.timestamp, '-to', end, '-acodec', 'copy', outfile] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=Shell) for line in p.stdout.readlines(): print(' {}'.format(line.decode('utf-8', errors='ignore').strip())) return @@ -129,7 +135,7 @@ def parse_args(): description='download & split audio tracks long youtube videos') # arguments parser.add_argument('tracklist', metavar='TRACKLIST', nargs='?', - help='tracklist to split audio by') + help='tracklist of title and timestamp information to split audio by') # options parser.add_argument('-o', '--output', metavar='PATH', type=str, nargs='?', dest='output', @@ -164,15 +170,15 @@ def sya(args): if args.output == None: args.output = os.path.splitext(args.tracklist)[0] - tracklist = load_tracklist(args.tracklist) + url, tracklist = load_tracklist(args.tracklist) - audio_fpath = get_audio(args.youtubedl, tracklist[0], args.output, + audio_fpath = get_audio(args.youtubedl, url, args.output, args.format, args.quality, args.keep, args.ffmpeg) if os.path.exists(audio_fpath) == False: error_exit('download failed, aborting') - tracks = parse_tracks(tracklist[1:]) + tracks = parse_tracks(tracklist) missing = missing_times(tracks) if len(missing) > 0: