diff options
Diffstat (limited to 'hypervideo_gui/main_ui.py')
-rw-r--r-- | hypervideo_gui/main_ui.py | 417 |
1 files changed, 417 insertions, 0 deletions
diff --git a/hypervideo_gui/main_ui.py b/hypervideo_gui/main_ui.py new file mode 100644 index 0000000..ab217a1 --- /dev/null +++ b/hypervideo_gui/main_ui.py @@ -0,0 +1,417 @@ +#!/usr/bin/python + +''' Hypervideo GUI ''' + +import os +import re +import threading +import json +import time + +import hypervideo + +from PyQt5.QtWidgets import ( + QAction, + QApplication, + QComboBox, + QFileDialog, + QGridLayout, + QLabel, + QLineEdit, + QMainWindow, + QMessageBox, + QPushButton, + QWidget, +) + +from PyQt5.QtCore import ( + pyqtSlot, +) + +__version__ = '0.1' +__license__ = "GPL-3" + +__path__ = os.environ['HOME'] +__pathBASE__ = r'%s/.config/hypervideo_gui/' % __path__ +if not os.path.exists(__pathBASE__): + os.makedirs(__pathBASE__) + +GUI_STATE_JSON_FILE = '%s/.config/hypervideo_gui/hypervideo_gui_state.json' % __path__ + + +class App(QMainWindow): + """ + Simple applicaction for download using hypervideo + """ + _VALID_URL = r"""^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$""" + + def __init__(self): + super().__init__() + + # load GUI state from file, load default state if save file not found + gui_state = self.load_gui_state() + self.download_folder_list = gui_state['DownloadFolderList'] + self.default_formats_menu_items = ['Video/Audio - Best Quality', + 'Audio Only - Best Quality', + 'Detect All Available Formats'] + + # initialize window dimensions + self.left = 100 + self.top = 100 + self.width = 640 + self.height = 200 + self.setFixedSize(self.width, self.height) + + self._init_ui() + + def _init_ui(self): + ''' Initial UI ''' + self.setWindowTitle('Simple Hypervideo Downloader GUI') + self.setGeometry(self.left, self.top, self.width, self.height) + # Initialize status bar + self.statusBar().showMessage('Welcome to Simple Hypervideo Downloader GUI!') + + # Menu + main_menu = self.menuBar() + file_menu = main_menu.addMenu('File') + help_menu = main_menu.addMenu('Help') + + # Exit button + exit_button = QAction('Exit', self) + exit_button.setShortcut('Ctrl+Q') + exit_button.setStatusTip('Exit application') + exit_button.triggered.connect(self.close) + + # About button + about_button = QAction('About', self) + about_button.triggered.connect(self.on_button_clicked) + + # Adding buttons to Menu + help_menu.addAction(about_button) + file_menu.addAction(exit_button) + + # Create URL entry buttons and entry textbox + url_entry_label = QLabel('Enter URL:') + self.url_entry_text = QLineEdit() + self.url_entry_text.setPlaceholderText('https://invidio.us/watch?v=8SdPLG-_wtA') + # set up callback to update video formats when URL is changed + self.url_entry_text.textChanged.connect(self.reset_video_formats) + + # create output folder button and entry textbox + output_folder_button = QPushButton('Select Output Folder') + output_folder_button.setToolTip('Select output folder') + output_folder_button.clicked.connect(self.update_output_folder) + self.output_entry_combobox = QComboBox() + self.output_entry_combobox.setEditable(True) + + for item in self.download_folder_list: + # set default output folder to be downloads folder + self.output_entry_combobox.addItem(item) + # self.output_entry_combobox.editTextChanged[str].connect(self.download_text_changed) + + # add combobox for video download format and detect formats button + detect_formats_label = QLabel('Download Format:') + + self.video_format_combobox = QComboBox() + # set default values for format select combobox + self.populate_video_format_combobox(self.default_formats_menu_items) + self.video_format_combobox.activated[str].connect(self.video_format_change) + + # add download button + download_button = QPushButton('Download') + download_button.clicked.connect(self.download_video_callback) + + # create grid layout + layout = QGridLayout() + + # add widgets to the layout + layout.addWidget(url_entry_label, 1, 0) + layout.addWidget(self.url_entry_text, 1, 1) + layout.addWidget(output_folder_button, 2, 0) + layout.addWidget(self.output_entry_combobox, 2, 1) + layout.addWidget(detect_formats_label, 3, 0) + layout.addWidget(self.video_format_combobox, 3, 1) + layout.addWidget(download_button, 5, 0) + + # add grid layout as central widget for main window + main_widget = QWidget() + main_widget.setLayout(layout) + self.setCentralWidget(main_widget) + + self.show() + + def on_button_clicked(self): + """ button about """ + msg = QMessageBox() + + msg.setWindowTitle('About us') + msg.setText("<p align='center'>Written with Python3 and PyQt5<br>" + "Version: %s <br> License: %s </p>" % (__version__, __license__)) + msg.setIcon(QMessageBox.Information) + self.show() + + msg.exec_() + + def url_catch(self): + ''' Return URL EntryText''' + url = self.url_entry_text.text() + url = str(url.strip()) + return url + + def url_check_valid(self): + ''' Check valid URL ''' + if re.match(self._VALID_URL, self.url_catch()) is None: + check = False + else: + check = True + return check + + def download_video_callback(self): + ''' Callback for the "Download Video" button + ''' + + def download_video_thread_helper(self, ydl_opts): + '''Download the video. Meant to be called in a background daemon thread + ''' + with hypervideo.YoutubeDL(ydl_opts) as ydl: + ydl.download([self.url_catch()]) + + self.statusBar().showMessage('Downloading Video... Done!', msecs=0) + + # make sure a valid output directory was entered + if not os.path.isdir(self.output_entry_combobox.currentText()): + self.statusBar().showMessage('Invalid download directory!') + return + + # make sure the Download Folder List combobox is populated with the latest entry + # this covers the case where the user uses the edittext portion of the combobox + self.add_item_to_downloads_combobox(self.output_entry_combobox.currentText()) + + # set output path/format + outtmpl = os.path.join(self.output_entry_combobox.currentText(), r'%(title)s.%(ext)s') + + # create the youtube downloader options based on video format combobox selection + if self.video_format_combobox.currentText() == self.default_formats_menu_items[0]: + # download best video quality + ydl_opts = { + 'format': 'bestvideo+bestaudio/best', + 'outtmpl': outtmpl, + } + elif self.video_format_combobox.currentText() == self.default_formats_menu_items[1]: + # for downloading best audio and converting to mp3 + ydl_opts = { + 'format': 'bestaudio/best', + 'outtmpl': outtmpl, + 'postprocessors': [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192', # not actually best quality... + }], + } + else: + # grab video format from the dropdown string: + # ie. "135 - some video metadata here" -> "135" + video_format = self.video_format_combobox.currentText()[0:self.video_format_combobox.currentText().find('-')-1] + # set output path/format + outformat = os.path.join(self.output_entry_combobox.currentText(), + r'%(title)s.f%(format_id)s.%(ext)s') + ydl_opts = { + 'format': video_format, + 'outtmpl': outformat, + } + + # download the video in daemon thread + if self.url_check_valid() is False: + self.statusBar().showMessage('Please add a URL...') + else: + self.statusBar().showMessage('Downloading Video...') + thread = threading.Thread(target=download_video_thread_helper, args=(self, ydl_opts, )) + thread.daemon = True + thread.start() + + def update_video_formats(self): + '''Grabs the list of available video formats in background thread and populates + video format combobox with results when complete. + ''' + + def get_video_formats_thread_helper(self, url): + ''' Grabs the available video formats. Intended to be run as background thread. + ''' + self.options = { + 'socket_timeout': 30, + 'format': 'best', + 'noplaylist': True, # only download single song, not playlist + } + + if self.url_check_valid() is True: + try: + with hypervideo.YoutubeDL(self.options) as ydl: + meta = ydl.extract_info(url, download=False) + formats = meta.get('formats', [meta]) + except TypeError: + self.statusBar().showMessage('Problem downloading %s' % url) + return None + + # Search formats + if formats is None: + self.statusBar().showMessage('Formats not found') + else: + item_list = self.default_formats_menu_items[0:2] + # ForLoop search formats + for quality in formats: + format_id = quality.get('format_id', '') + ext = quality.get('ext', '') + acodec = quality.get('acodec', '') + vcodec = quality.get('vcodec', '') + item_list.append('%s - %s (audio: %s) (video: %s)' % + (format_id, ext, acodec, vcodec)) + # Replace value empty in audio or video + format_list = [w.replace('(audio: )', '(audio: none)') + .replace('(video: )', '(video: none)') + for w in item_list] + # Add formats in combobox + self.populate_video_format_combobox(format_list) + self.video_format_combobox.setCurrentIndex(0) + time.sleep(.300) + self.statusBar().showMessage('Finished Downloading Video Formats', msecs=0) + + # check if is valid url + # should probably be reworked to be compatible with non-YouTube websites + if self.url_check_valid() is False: + self.populate_video_format_combobox(self.default_formats_menu_items) + return + else: + # valid url - fetch the video formats in background daemon thread + self.statusBar().showMessage('Downloading Video Formats') + thread = threading.Thread(target=get_video_formats_thread_helper, + args=(self, self.url_catch(), )) + thread.daemon = True + thread.start() + + def video_format_change(self, text): + ''' Video Format Change ''' + if text == self.default_formats_menu_items[2]: + # detect video formats was selected + # update statusbar to let user know something is happening + if self.url_check_valid() is False: + self.statusBar().showMessage('Please add a URL...') + else: + # update video formats + self.update_video_formats() + + def populate_video_format_combobox(self, labels): + '''Populate the video format combobox with video formats. Clear the previous labels. + labels {list} -- list of strings representing the video format combobox options + ''' + self.video_format_combobox.clear() + for label in labels: + self.video_format_combobox.addItem(label) + + def reset_video_formats(self): + ''' Clean video formast ''' + idx = self.video_format_combobox.currentIndex() + + self.populate_video_format_combobox(self.default_formats_menu_items) + + # preserve combobox index if possible + if idx > 1: + self.video_format_combobox.setCurrentIndex(0) + else: + self.video_format_combobox.setCurrentIndex(idx) + + @pyqtSlot() + def update_output_folder(self): + ''' Callback for "Update Output Folder" button. Allows user to select + output directory via standard UI. + https://stackoverflow.com/questions/43121340/why-is-the-use-of-lensequence-in-condition-values-considered-incorrect-by-pyli + ''' + file = str(QFileDialog.getExistingDirectory(self, "Select Directory")) + + if file: + self.add_item_to_downloads_combobox(file) + else: + self.statusBar().showMessage('Select a folder!') + + def download_text_changed(self, text): + ''' download text changed ''' + # function not used right now + if text not in self.download_folder_list and os.path.isdir(text): + self.add_item_to_downloads_combobox(text) + + def add_item_to_downloads_combobox(self, text): + ''' Add item to download combobox ''' + if text not in self.download_folder_list: + # if it's not in the list, add it to the list + self.download_folder_list = [text]+self.download_folder_list + else: + # if it's in the list already, move it to the top of the list + self.download_folder_list = [text]+[folder for folder in self.download_folder_list if not folder == text] + + # maximum download folder history of 5 + if len(self.download_folder_list) > 5: + self.download_folder_list = self.download_folder_list[0:6] + + # update the combobox + self.output_entry_combobox.clear() + for item in self.download_folder_list: + self.output_entry_combobox.addItem(item) + + self.output_entry_combobox.setCurrentIndex(0) # reset index - just in case + + def save_gui_state(self): + ''' Save GUI State ''' + save_dict = {'DownloadFolderList': self.download_folder_list, } + + if not GUI_STATE_JSON_FILE: + newf = open(GUI_STATE_JSON_FILE, "a+") + newf.close() + + with open(GUI_STATE_JSON_FILE, 'w') as file: + json.dump(save_dict, file) + + def load_gui_state(self): + ''' Load GUI state ''' + if os.path.isfile(GUI_STATE_JSON_FILE): + with open(GUI_STATE_JSON_FILE, 'r') as file: + save_data = json.load(file) + + if isinstance(save_data['DownloadFolderList'], str): + # convert to list + save_data['DownloadFolderList'] = [save_data['DownloadFolderList']] + + else: + save_data = {'DownloadFolderList': [get_default_download_path()], } + + return save_data + + def closeEvent(self, event): + '''Protected Function for PyQt5 + gets called when the user closes the GUI. + It saves the GUI state to the json file + GUI_STATE_JSON_FILE. + ''' + self.save_gui_state() + + close = QMessageBox() + close.setIcon(QMessageBox.Question) + close.setWindowTitle('Exit') + close.setText('You sure?') + close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) + close = close.exec() + + if close == QMessageBox.Yes: + event.accept() + else: + event.ignore() + + +def get_default_download_path(): + """Returns the path for the "Downloads" folder in GNU + Used as default directory for videos to be saved to. + #From: https://stackoverflow.com/questions/35851281/python-finding-the-users-downloads-folder + """ + if os.name == 'posix': + dpath = '/tmp/hypervideo-gui/' + if not os.path.exists(dpath): + os.makedirs(dpath) + return os.path.join(dpath) |