def __init__(self, parent=None, name: str = '', description: str = '', allow_empty=False): super(Dialog, self).__init__(parent) self.option = QLineEdit() self.allow_empty = allow_empty self.label = QLabel(color_text('Insert option:', 'limegreen')) self.name_label = QLabel(color_text(name + ':', 'limegreen')) self.tooltip = QLabel(description) self.ok_button = QPushButton('Ok', self) self.ok_button.setFixedSize(self.ok_button.sizeHint()) self.ok_button.setDisabled(not allow_empty) self.ok_button.clicked.connect(self.accept) self.cancel_button = QPushButton('Cancel', self) self.cancel_button.setFixedSize(self.cancel_button.sizeHint()) self.cancel_button.clicked.connect(self.reject) layout = QGridLayout(self) layout.addWidget(self.name_label, 0, 0, 1, 3) layout.addWidget(self.tooltip, 1, 0, 1, 3) layout.addWidget(self.label, 2, 0, 1, 3) layout.addWidget(self.option, 3, 0, 1, 3) layout.setColumnStretch(0, 1) layout.setColumnStretch(1, 0) layout.setColumnStretch(2, 0) layout.addWidget(self.ok_button, 4, 1) layout.addWidget(self.cancel_button, 4, 2) self.option.textChanged.connect(self.input_check) self.setFixedHeight(self.sizeHint().height()) self.setFixedWidth(self.sizeHint().width()) self.option.setFocus()
def stat_update(self): def show_infolabel(): nonlocal self if not self.info_label_in_layout: self.info_label.show() self.info_label_in_layout = True if self._open_window is not None: self._open_window.setText('\n'.join(self.process.debug_log)) self.status_box.setText(color_text(self.process.status, color='lawngreen')) self.progress.setText(color_text(self.process.progress, color='lawngreen')) self.eta.setText(self.process.eta) self.speed.setText(self.process.speed) self.filesize.setText(self.process.filesize) self.playlist.setText(self.process.playlist) self.progress.setStyleSheet(f'background: {"#484848" if self.process.progress else "#303030"}') self.eta.setStyleSheet(f'background: {"#484848" if self.process.eta else "#303030"}') self.speed.setStyleSheet(f'background: {"#484848" if self.process.speed else "#303030"}') self.filesize.setStyleSheet(f'background: {"#484848" if self.process.filesize else "#303030"}') self.playlist.setStyleSheet(f'background: {"#484848" if self.process.playlist else "#303030"}') if self.process.status == 'ERROR': show_infolabel() self.status_box.setText(color_text(self.process.status)) self.info_label.setText(f'{self.process.name if self.process.name else "Process"}' f' failed with message:\n{self.process.info.replace("ERROR:", "").lstrip()}') elif self.process.status == 'Aborted': self.status_box.setText(color_text(self.process.status)) show_infolabel() name = ' | ' + self.process.name if self.process.name else '' self.info_label.setText(self.process.info + name) elif self.process.info or self._debug or self.process.name: # Shows the info label if there is debug info, or if any other field has info if self._debug and not any((self.process.info, self.process.name)): if self.process.program_log: show_infolabel() else: show_infolabel() content = [] if self.process.name: content.append(self.process.name.strip()) if self.process.info: content.append(self.process.info.replace("[download] ", "")) if self._debug: content += ['<br>Debug info:<br>'] + list(self.process.program_log) self.info_label.setText('<br>'.join(content)) self.adjust()
def print_process_output(self, text): scrollbar = self.tab1.textbrowser.verticalScrollBar() place = scrollbar.sliderPosition() if place == scrollbar.maximum(): keep_position = False else: keep_position = True # get the last line of QTextEdit self.tab1.textbrowser.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor) self.tab1.textbrowser.moveCursor(QTextCursor.StartOfLine, QTextCursor.MoveAnchor) self.tab1.textbrowser.moveCursor(QTextCursor.End, QTextCursor.KeepAnchor) last_line = self.tab1.textbrowser.textCursor().selectedText() # Check if a percentage has already been placed. if "%" in last_line and 'ETA' in last_line and "%" in text: self.tab1.textbrowser.textCursor().removeSelectedText() self.tab1.textbrowser.textCursor().deletePreviousChar() # Last line of text self.tab1.textbrowser.append( color_text(text.split("[download]")[-1][1:], color='lawngreen', weight='bold', sections=(0, 5))) if '100%' in text: self.tab1.textbrowser.append('') else: if ("%" in text and 'ETA' in text) or '100% of ' in text: # Last line of text self.tab1.textbrowser.append( color_text(text.split("[download]")[-1][1:], color='lawngreen', weight='bold', sections=(0, 5))) elif '[download]' in text: self.tab1.textbrowser.append(''.join( [text.replace('[download] ', ''), '\n'])) else: self.tab1.textbrowser.append(''.join([text, '\n'])) # Prevents some leftover highlighted text on errors and such. self.tab1.textbrowser.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor) # Ensures slider position is kept when not at bottom, and stays at bottom with new text when there. if keep_position: scrollbar.setSliderPosition(place) else: scrollbar.setSliderPosition(scrollbar.maximum())
def dir_info(self): # TODO: Print this info to GUI. file_dir = os.path.dirname(os.path.abspath(__file__)).replace('\\', '/') debug = [color_text('Youtube-dl.exe path: ') + self.youtube_dl_path, color_text('ffmpeg.exe path: ') + self.ffmpeg_path, color_text('Filedir: ') + file_dir, color_text('Workdir: ') + self.file_handler.work_dir, color_text('Youtube-dl working directory: ') + self.program_workdir] debug += [color_text('\nIcon paths:'), *self.icon_list] debug += [color_text('\nChecking if icons are in place:', 'darkorange', 'bold')] for i in self.icon_list: if i is not None: if self.file_handler.is_file(str(i)): try: debug.append(f'Found: {os.path.split(i)[1]}') except IndexError: debug.append(f'Found: {i}') if self.icon_list.count(None): debug.append(color_text(f'Missing {self.icon_list.count(None)} icon file(s)!')) # RichText does not support both the use of \n and <br> at the same time. Use <br> debug_info = '<br>'.join([text.replace('\n', '<br>') for text in debug if text is not None]) mock_download = MockDownload(info=debug_info) self.add_download_to_gui(mock_download) self.tab_widget.setCurrentIndex(0)
def dir_info(self): file_dir = os.path.dirname(os.path.abspath(__file__)).replace( '\\', '/') debug = [ color_text('\nYoutube-dl.exe path:'), self.youtube_dl_path, color_text('\nffmpeg.exe path:'), self.ffmpeg_path, color_text('Filedir:'), file_dir, color_text('Workdir:'), self.file_handler.work_dir, color_text('Youtube-dl working directory:'), self.program_workdir, color_text('\nIcon paths:'), *self.icon_list ] for i in debug: self.tab1.textbrowser.append(str(i)) self.tab1.textbrowser.append( color_text('\nChecking if icons are in place:', 'darkorange', 'bold')) for i in self.icon_list: if i is not None: if self.file_handler.is_file(str(i)): try: self.tab1.textbrowser.append(''.join( ['Found: ', os.path.split(i)[1]])) except IndexError: self.tab1.textbrowser.append(''.join(['Found: ', i])) else: self.tab1.textbrowser.append(''.join(['Missing in:', i])) self.tab_widget.setCurrentIndex(0)
def restart_current_download(self): # TODO: Trigger this make trigger for restarting download! if self.active_download is not None and self.active_download.state( ) == QProcess.Running: self.active_download.kill() self.output.emit( color_text('Restarting download!', weight='normal')) self.active_download.start() else: self.output.emit('No active download to restart!')
def program_state_changed(self, new_state): if new_state == QProcess.NotRunning: self.active_download.disconnect() self.output.emit('\nDone\n') self.queue_handler(process_finished=True) elif new_state == QProcess.Running: self.output.emit( color_text('Starting...\n', 'lawngreen', 'normal', sections=(0, 8))) return
def _single_queue_handler(self, process_finished=False): # TODO: Add parallel downloads! if not self.RUNNING: self.clearOutput.emit() if not self.RUNNING or process_finished: # TODO: Detect crash when redistributable C++ is not present, if possible # if process_finished: # error_code = self.active_download.exitCode() # if error_code: # self.output.emit(color_text(f'Youtube-dl closed with error code {error_code}! ' # 'Is the required C++ distributable installed?')) # self.error_count += 1 if self._queue: download = self._queue.popleft() self.updateQueue.emit(f'Items in queue: {len(self._queue):3}') self.active_download = download try: download.start_dl() self.RUNNING = True self.stateChanged.emit() except TypeError as e: self.error_count += 1 self.output.emit(color_text(f'FAILED with error {e}')) return self.queue_handler(process_finished=True) else: self.active_download = None self.RUNNING = False self.stateChanged.emit() error_report = 0 if not self.error_count else color_text( str(self.error_count), "darkorange", "bold") self.output.emit(f'Error count: {error_report}.') self.error_count = 0 self.updateQueue.emit(f'Items in queue: {len(self._queue):3}')
def __init__(self, process: Download, slot, debug=False, parent=None, url=None): super(ProcessListItem, self).__init__(parent=parent) self.process = process self.slot = slot self.url = url self.process.getOutput.connect(self.stat_update) self.line = QHBoxLayout() self.setFocusPolicy(Qt.NoFocus) # self.setStyleSheet() self._open_window = None self.status_box = QLabel(color_text(self.process.status, color='lawngreen')) self.status_box.setContextMenuPolicy(Qt.CustomContextMenu) self.status_box.customContextMenuRequested.connect(self.open_info_menu) self.progress = QLabel(parent=self) self.progress.setAlignment(Qt.AlignCenter) self.eta = QLabel('', parent=self) self.eta.setAlignment(Qt.AlignCenter) self.speed = QLabel(parent=self) self.speed.setAlignment(Qt.AlignCenter) self.filesize = QLabel(parent=self) self.filesize.setAlignment(Qt.AlignCenter) self.playlist = QLabel(parent=self) self.playlist.setAlignment(Qt.AlignCenter) font_size_pixels = FONT_CONSOLAS.pixelSize() self.status_box.setBaseSize(20 * font_size_pixels, self.sizeHint().height()) self.progress.setFixedWidth(5 * font_size_pixels) self.eta.setFixedWidth(4 * font_size_pixels) self.speed.setFixedWidth(6 * font_size_pixels) self.filesize.setFixedWidth(6 * font_size_pixels) self.playlist.setFixedWidth(6 * font_size_pixels) self.line.addWidget(self.progress, 0) self.line.addWidget(self.eta, 0) self.line.addWidget(self.speed, 0) self.line.addWidget(self.filesize, 0) self.line.addWidget(self.playlist, 0) self.line2 = QHBoxLayout() self.line2.addWidget(self.status_box, 0) self.line2.addStretch(1) self.line2.addLayout(self.line, 1) self.info_label_in_layout = False self.info_label = QLabel('', parent=self) if url is not None: self.info_label.setToolTip(f'URL: {url} (Right-click to copy)') self.info_label.setWordWrap(True) self.info_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.info_label.hide() self.info_label.setContextMenuPolicy(Qt.CustomContextMenu) self.info_label.customContextMenuRequested.connect(self.open_file_menu) self.vline = QVBoxLayout() self.vline.addLayout(self.line2, 0) self.vline.addWidget(self.info_label, 1) self.setLayout(self.vline) self.progress.setStyleSheet(f'background: {"#484848" if self.process.progress else "#303030"}') self.eta.setStyleSheet(f'background: {"#484848" if self.process.eta else "#303030"}') self.speed.setStyleSheet(f'background: {"#484848" if self.process.speed else "#303030"}') self.filesize.setStyleSheet(f'background: {"#484848" if self.process.filesize else "#303030"}') self.playlist.setStyleSheet(f'background: {"#484848" if self.process.playlist else "#303030"}') self._debug = debug
def build_gui(self): """Generates the GUI elements, and hooks everything up.""" # Indicates if license is shown. (For license tab) # TODO: Move to license tab? self.license_shown = False # Sorts the parameters, so that favorite ones are added to the favorite widget. favorites = { i: self.settings[i] for i in self.settings.get_favorites() } options = { k: v for k, v in self.settings.parameters.items() if k not in favorites } # Main widget. This will be the ones that holds everything. # Create top level tab widget system for the UI. self.tab_widget = QTabWidget(self) self.onclose.connect(self.confirm) self.sendClose.connect(self.closeE) # Connecting stuff for tab 1. # Start buttons starts download self.tab1 = MainTab(self.settings, self) self.tab1.start_btn.clicked.connect(self.queue_dl) # Stop button kills the process, aka youtube-dl. self.tab1.stop_btn.clicked.connect(self.stop_download) # Close button closes the window/process. self.tab1.close_btn.clicked.connect(self.close) # When the check button is checked or unchecked, calls function checked. self.tab1.checkbox.stateChanged.connect(self.allow_start) # Connects actions to text changes and adds action to when you press Enter. self.tab1.lineedit.textChanged.connect(self.allow_start) # Queue downloading self.tab1.lineedit.returnPressed.connect(self.tab1.start_btn.click) # Change profile self.tab1.profile_dropdown.currentTextChanged.connect( self.load_profile) # Delete profile self.tab1.profile_dropdown.deleteItem.connect(self.delete_profile) # Tab 2 self.tab2 = ParameterTab(options, favorites, self.settings, self) # Adds the tab2 layout to the widget. # Connection stuff tab 2. self.tab2.open_folder_action.triggered.connect(self.open_folder) self.tab2.copy_action.triggered.connect(self.copy_to_cliboard) self.tab2.options.itemChanged.connect(self.parameter_updater) self.tab2.options.move_request.connect(self.move_item) self.tab2.options.itemRemoved.connect(self.item_removed) self.tab2.options.addOption.connect(self.add_option) self.tab2.favorites.itemChanged.connect(self.parameter_updater) self.tab2.favorites.move_request.connect(self.move_item) self.tab2.favorites.itemRemoved.connect(self.item_removed) self.tab2.favorites.addOption.connect(self.add_option) self.tab2.browse_btn.clicked.connect(self.savefile_dialog) self.tab2.save_profile_btn.clicked.connect(self.save_profile) # Tab 3. # Tab creation. self.tab3 = TextTab(parent=self) # Connecting stuff tab 3. # When loadbutton is clicked, launch load textfile. self.tab3.loadButton.clicked.connect(self.load_text_from_file) # When savebutton clicked, save text to document. self.tab3.saveButton.clicked.connect(self.save_text_to_file) # Tab 4 # Button to browse for .txt file to download files. self.tab4 = AboutTab(self.settings, parent=self) ## Connecting stuff tab 4. # Starts self.update_youtube_dl, locate_program_path checks for updates. self.tab4.update_btn.clicked.connect(self.update_youtube_dl) self.tab4.dirinfo_btn.clicked.connect(self.dir_info) self.tab4.reset_btn.clicked.connect(self.reset_settings) self.tab4.license_btn.clicked.connect(self.read_license) self.tab4.location_btn.clicked.connect(self.textfile_dialog) # Future tab creation here! Currently 4 tabs # TODO: Move stylesheet applying to method, make color picking dialog to customize in realtime if self.settings.user_options['use_win_accent']: try: color = get_win_accent_color() bg_color = f""" QMainWindow {{ background-color: {color}; }} QTabBar {{ background-color: {color}; }}""" except (OSError, PermissionError): bg_color = '' else: bg_color = '' self.style_with_options = bg_color + f""" QCheckBox::indicator:unchecked {{ image: url({self.unchecked_icon}); }} QCheckBox::indicator:checked {{ image: url({self.checked_icon}); }} QComboBox::down-arrow {{ border-image: url({self.down_arrow_icon}); height: {self.tab1.profile_dropdown.iconSize().height()}px; width: {self.tab1.profile_dropdown.iconSize().width()}px; }} QComboBox::down-arrow::on {{ image: url({self.down_arrow_icon_clicked}); height: {self.tab1.profile_dropdown.iconSize().height()}px; width: {self.tab1.profile_dropdown.iconSize().width()}px; }} QTreeWidget::indicator:checked {{ image: url({self.checked_icon}); }} QTreeWidget::indicator:unchecked {{ image: url({self.unchecked_icon}); }} QTreeWidget::branch {{ image: none; border-image: none; }} QTreeWidget::branch:has-siblings:!adjoins-item {{ image: none; border-image: none; }} QTreeWidget::branch:has-siblings:adjoins-item {{ border-image: none; image: none; }} QTreeWidget::branch:!has-children:!has-siblings:adjoins-item {{ border-image: none; image: none; }} """ # Configuration main widget. # Adds tabs to the tab widget, and names the tabs. self.tab_widget.addTab(self.tab1, 'Main') self.tab_widget.addTab(self.tab2, 'Param') self.tab_widget.addTab(self.tab3, 'List') self.tab_widget.addTab(self.tab4, 'About') # Sets the styling for the GUI, everything from buttons to anything. ## self.setStyleSheet(get_stylesheet() + self.style_with_options) # Set window title. self.setWindowTitle('Grabber') self.setWindowIcon(self.windowIcon) # Set base size. self.setMinimumWidth(340) self.setMinimumHeight(200) if self.settings.user_options['select_on_focus']: self.gotfocus.connect(self.window_focus_event) else: self.tab1.lineedit.setFocus() # Other functionality. self.shortcut = QShortcut(QKeySequence("Ctrl+S"), self.tab3.textedit) self.shortcut.activated.connect(self.tab3.saveButton.click) # TODO: Hook up to button, or change output to somewhere the user can get it. # self.trigger_queue_print = QShortcut(QKeySequence('Ctrl+P'), self) # self.trigger_queue_print.activated.connect(self.print_queue) # Check for youtube if self.youtube_dl_path is None: self.tab4.update_btn.setDisabled(True) self.tab1.textbrowser.append( color_text( '\nNo youtube-dl.exe found! Add to path, ' 'or make sure it\'s in the same folder as this program. ' 'Then close and reopen this program.', 'darkorange', 'bold')) # Sets the download items tooltips to the full file path. self.download_name_handler() # Ensures widets are in correct state at startup and when tab1.lineedit is changed. self.allow_start() # Shows the main window. self.setCentralWidget(self.tab_widget) self.show() # self.main_tab.show() # Old method. # Connect after show!! self.resizedByUser.connect(self.resize_contents) # To make sure the window is updated on first enter # if resized before tab2 is shown, i'll be blank. self.downloader.output.connect(self.print_process_output) self.downloader.stateChanged.connect(self.allow_start) self.downloader.clearOutput.connect(self.tab1.textbrowser.clear) self.downloader.updateQueue.connect( lambda text: self.tab1.queue_label.setText(text)) self.tab_widget.currentChanged.connect(self.resize_contents)
def queue_dl(self): command = [] if self.tab1.checkbox.isChecked(): if self.tab4.txt_lineedit.text() == '': self.alert_message('Error!', 'No textfile selected!', '') self.tab1.textbrowser.append( 'No textfile selected...\n\nNo download queued!') return txt = self.settings.user_options['multidl_txt'] command += ['-a', f'{txt}'] else: txt = self.tab1.lineedit.text() command.append(f'{txt}') # for i in range(len(command)): # command[i] = command[i].format(txt=txt)' file_name_format = '%(title)s.%(ext)s' for parameter, options in self.settings.parameters.items(): if parameter == 'Download location': if options['state']: add = format_in_list( options['command'], self.settings.get_active_setting(parameter) + file_name_format) command += add else: command += ['-o', self.local_dl_path + file_name_format] elif parameter == 'Keep archive': if options['state']: add = format_in_list( options['command'], os.path.join( os.getcwd(), self.settings.get_active_setting(parameter))) command += add elif parameter == 'Username': if options['state']: option = self.settings.get_active_setting(parameter) if option in self._temp: _password = self._temp[option] else: dialog = Dialog( self, 'Password', f'Input you password for the account "{option}".', allow_empty=True, password=True) if dialog.exec_() == QDialog.Accepted: self._temp[ option] = _password = dialog.option.text() else: self.tab1.textbrowser.append( color_text('ERROR: No password was entered.', sections=(0, 6))) return add = format_in_list(options['command'], option) add += ['--password', _password] command += add else: if options['state']: if self.settings.get_active_setting(parameter): option = self.settings.get_active_setting(parameter) else: option = '' add = format_in_list(options['command'], option) command += add # Sets encoding to utf-8, allowing better character support in output stream. command += ['--encoding', 'utf-8'] if self.ffmpeg_path is not None: command += ['--ffmpeg-location', self.ffmpeg_path] download = Download(self.program_workdir, self.youtube_dl_path, command, self) self.tab1.start_btn.setDisabled(True) self.downloader.queue_dl(download)