diff options
Diffstat (limited to 'flycheck_hypervideo_gui.py')
-rw-r--r-- | flycheck_hypervideo_gui.py | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/flycheck_hypervideo_gui.py b/flycheck_hypervideo_gui.py new file mode 100644 index 0000000..5cc9c52 --- /dev/null +++ b/flycheck_hypervideo_gui.py @@ -0,0 +1,359 @@ +import sys +import os +import re + +from PyQt5.QtWidgets import * +from PyQt5.QtGui import QIcon +from PyQt5.QtCore import ( + pyqtSlot, +) + +import threading +import hypervideo +import json + +__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__ + +if not GUI_STATE_JSON_FILE: + f = open(GUI_STATE_JSON_FILE, "a+") + f.close() + + +class App(QMainWindow): + """ + Simple applicaction for download using hypervideo + """ + _VALID_URL = r"""^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$""" + + def __init__(self): + super().__init__() + + gui_state = self.loadGUIState() # load GUI state from file, load default state if save file not found + self.download_folder_list = gui_state['DownloadFolderList'] + self.default_video_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.initUI() + + def on_button_clicked(self): + 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) + # msg.setInformativeText("Version: %s" % __version__) + + x = msg.exec_() + + def initUI(self): + 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 + mainMenu = self.menuBar() + fileMenu = mainMenu.addMenu('File') + helpMenu = mainMenu.addMenu('Help') + + # Exit button + exitButton = QAction('Exit', self) + exitButton.setShortcut('Ctrl+Q') + exitButton.setStatusTip('Exit application') + exitButton.triggered.connect(self.close) + + # About button + aboutButton = QAction('About', self) + aboutButton.triggered.connect(self.on_button_clicked) + + # Adding buttons to Menu + helpMenu.addAction(aboutButton) + fileMenu.addAction(exitButton) + + # Create URL entry buttons and entry textbox + urlEntryLabel = QLabel('Enter URL:') + self.urlEntryText = QLineEdit() + self.urlEntryText.setPlaceholderText('https://invidio.us/watch?v=8SdPLG-_wtA') + # set default video to download + # self.urlEntryText.setText('https://invidio.us/watch?v=8SdPLG-_wtA') + # set up callback to update video formats when URL is changed + self.urlEntryText.textChanged.connect(self.resetVideoFormats) + + # create output folder button and entry textbox + outputFolderButton = QPushButton('Select Output Folder') + outputFolderButton.setToolTip('Select output folder') + outputFolderButton.clicked.connect(self.updateOutputFolder) + self.outputEntryCombobox = QComboBox() + self.outputEntryCombobox.setEditable(True) + + for item in self.download_folder_list: + self.outputEntryCombobox.addItem(item) # set default output folder to be downloads folder + # self.outputEntryCombobox.editTextChanged[str].connect(self.downloadTextChanged) + + # add combobox for video download format and detect formats button + detectFormatsLabel = QLabel('Download Format:') + + self.videoFormatCombobox = QComboBox() + self.populateVideoFormatCombobox(self.default_video_formats_menu_items) # set default values for format select combobox + self.videoFormatCombobox.activated[str].connect(self.videoFormatChange) + + # add download button + downloadButton = QPushButton('Download') + downloadButton.clicked.connect(self.downloadVideo_callback) + + # create grid layout + layout = QGridLayout() + + # add widgets to the layout + layout.addWidget(urlEntryLabel, 1, 0) + layout.addWidget(self.urlEntryText, 1, 1) + layout.addWidget(outputFolderButton, 2, 0) + layout.addWidget(self.outputEntryCombobox, 2, 1) + layout.addWidget(detectFormatsLabel, 3, 0) + layout.addWidget(self.videoFormatCombobox, 3, 1) + layout.addWidget(downloadButton, 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 url_entry(self): + ''' Return URL EntryText''' + url = self.urlEntryText.text() + url = url.strip() + return url + + def downloadVideo_callback(self): + ''' Callback for the "Download Video" button + ''' + + def downloadVideo_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.urlEntryText.text()]) + + self.statusBar().showMessage('Downloading Video... Done!', msecs=0) + + # make sure a valid output directory was entered + if not os.path.isdir(self.outputEntryCombobox.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.addItemToDownloadsCombobox(self.outputEntryCombobox.currentText()) + + # set output path/format + outtmpl = os.path.join(self.outputEntryCombobox.currentText(), r'%(title)s.%(ext)s') + + # create the youtube downloader options based on video format combobox selection + if self.videoFormatCombobox.currentText() == self.default_video_formats_menu_items[0]: + # download best video quality + ydl_opts = { + 'format': 'bestvideo+bestaudio/best', + 'outtmpl': outtmpl, + } + elif self.videoFormatCombobox.currentText() == self.default_video_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.videoFormatCombobox.currentText()[0:self.videoFormatCombobox.currentText().find('-')-1] + # set output path/format + outformat = os.path.join(self.outputEntryCombobox.currentText(), r'%(title)s.f%(format_id)s.%(ext)s') + ydl_opts = { + 'format': video_format, + 'outtmpl': outformat, + } + + # download the video in daemon thread + if re.match(self._VALID_URL, str(self.url_entry())) is None: + self.statusBar().showMessage('Please add a URL...') # update status bar + else: + self.statusBar().showMessage('Downloading Video...') # update status bar + thread = threading.Thread(target=downloadVideo_thread_helper, args=(self, ydl_opts, )) + thread.daemon = True + thread.start() + + def updateVideoFormats(self): + '''Grabs the list of available video formats in background thread and populates + video format combobox with results when complete. + ''' + + def getVideoFormats_thread_helper(self, url): + ''' Grabs the available video formats. Intended to be run as background thread. + ''' + self.options = { + 'format': 'best', + 'noplaylist': True, # only download single song, not playlist + } + + with hypervideo.YoutubeDL(self.options) as ydl: + meta = ydl.extract_info(url, download=False) + formats = meta.get('formats', [meta]) + + # add formats to combobox + item_list = self.default_video_formats_menu_items[0:2] + item_list.extend([f['format_id'] + ' - ' + f['ext'] for f in formats]) + self.populateVideoFormatCombobox(item_list) + self.statusBar().showMessage('Finished Downloading Video Formats') + self.videoFormatCombobox.setCurrentIndex(0) + + # check if is valid url + # should probably be reworked to be compatible with non-YouTube websites + if re.match(self._VALID_URL, str(self.url_entry())) is None: + self.populateVideoFormatCombobox(self.default_video_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=getVideoFormats_thread_helper, args=(self, str(self.url_entry()), )) + thread.daemon = True + thread.start() + + def videoFormatChange(self, text): + if text == self.default_video_formats_menu_items[2]: + # detect video formats was selected + + # update statusbar to let user know something is happening + self.statusBar().showMessage('Loading available formats...') + # update video formats + self.updateVideoFormats() + + def populateVideoFormatCombobox(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.videoFormatCombobox.clear() + for label in labels: + self.videoFormatCombobox.addItem(label) + + def resetVideoFormats(self): + idx = self.videoFormatCombobox.currentIndex() + + self.populateVideoFormatCombobox(self.default_video_formats_menu_items) + + # preserve combobox index if possible + if idx > 1: + self.videoFormatCombobox.setCurrentIndex(0) + else: + self.videoFormatCombobox.setCurrentIndex(idx) + + @pyqtSlot() + def updateOutputFolder(self): + ''' Callback for "Update Output Folder" button. Allows user to select + output directory via standard UI. + ''' + file = str(QFileDialog.getExistingDirectory(self, "Select Directory")) + + if len(file) > 0: + self.addItemToDownloadsCombobox(file) + else: + self.statusBar().showMessage('Select a folder!') + + def downloadTextChanged(self, text): + # function not used right now + if text not in self.download_folder_list and os.path.isdir(text): + self.addItemToDownloadsCombobox(text) + + def addItemToDownloadsCombobox(self, text): + 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.outputEntryCombobox.clear() + for item in self.download_folder_list: + self.outputEntryCombobox.addItem(item) + + self.outputEntryCombobox.setCurrentIndex(0) # reset index - just in case + + def saveGUIState(self): + save_dict = {'DownloadFolderList': self.download_folder_list, } + + with open(GUI_STATE_JSON_FILE, 'w') as file: + json.dump(save_dict, file) + + def loadGUIState(self): + 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): + '''This function gets called when the user closes the GUI. + It saves the GUI state to the json file + GUI_STATE_JSON_FILE. + ''' + self.saveGUIState() + + 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) + +if __name__ == '__main__': + app = QApplication(sys.argv) + app.setStyle('Fusion') + ex = App() + sys.exit(app.exec_()) |