aboutsummaryrefslogtreecommitdiffstats
path: root/hypervideo_gui/main_ui.py
diff options
context:
space:
mode:
Diffstat (limited to 'hypervideo_gui/main_ui.py')
-rw-r--r--hypervideo_gui/main_ui.py417
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)