From 6bf28f116dbf5f288d1b766fff40273c8bf3f390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs?= Date: Fri, 13 Dec 2019 17:52:03 -0500 Subject: switch to redistributable package --- .gitignore | 6 +- README.md | 12 +- bin/hypervideo-gui | 12 + hypervideo_gui.py | 422 --------------------- hypervideo_gui/__init__.py | 0 hypervideo_gui/main_ui.py | 417 ++++++++++++++++++++ images/hypervideo_gui.png | Bin 19751 -> 0 bytes images/screenshot.png | Bin 0 -> 19751 bytes script.py | 12 + setup-files/gnu/hypervideo-gui.desktop | 9 + .../icons/hicolor/128x128/apps/hypervideo-gui.png | Bin 0 -> 17388 bytes .../icons/hicolor/16x16/apps/hypervideo-gui.png | Bin 0 -> 1098 bytes .../icons/hicolor/192x192/apps/hypervideo-gui.png | Bin 0 -> 29992 bytes .../icons/hicolor/22x22/apps/hypervideo-gui.png | Bin 0 -> 1316 bytes .../icons/hicolor/32x32/apps/hypervideo-gui.png | Bin 0 -> 2224 bytes .../icons/hicolor/384x384/apps/hypervideo-gui.png | Bin 0 -> 87791 bytes .../icons/hicolor/48x48/apps/hypervideo-gui.png | Bin 0 -> 3986 bytes .../icons/hicolor/64x64/apps/hypervideo-gui.png | Bin 0 -> 6169 bytes .../icons/hicolor/96x96/apps/hypervideo-gui.png | Bin 0 -> 10980 bytes setup.py | 58 ++- 20 files changed, 516 insertions(+), 432 deletions(-) create mode 100644 bin/hypervideo-gui delete mode 100644 hypervideo_gui.py create mode 100644 hypervideo_gui/__init__.py create mode 100644 hypervideo_gui/main_ui.py delete mode 100644 images/hypervideo_gui.png create mode 100644 images/screenshot.png create mode 100644 script.py create mode 100644 setup-files/gnu/hypervideo-gui.desktop create mode 100644 setup-files/gnu/icons/hicolor/128x128/apps/hypervideo-gui.png create mode 100644 setup-files/gnu/icons/hicolor/16x16/apps/hypervideo-gui.png create mode 100644 setup-files/gnu/icons/hicolor/192x192/apps/hypervideo-gui.png create mode 100644 setup-files/gnu/icons/hicolor/22x22/apps/hypervideo-gui.png create mode 100644 setup-files/gnu/icons/hicolor/32x32/apps/hypervideo-gui.png create mode 100644 setup-files/gnu/icons/hicolor/384x384/apps/hypervideo-gui.png create mode 100644 setup-files/gnu/icons/hicolor/48x48/apps/hypervideo-gui.png create mode 100644 setup-files/gnu/icons/hicolor/64x64/apps/hypervideo-gui.png create mode 100644 setup-files/gnu/icons/hicolor/96x96/apps/hypervideo-gui.png diff --git a/.gitignore b/.gitignore index c62f5d5..ebdd3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ - +# Temp files __pycache__/ -build/ +dist/ flycheck* +hypervideo_gui.egg-info *.spec *.json +MANIFEST diff --git a/README.md b/README.md index 2cc840f..c90d742 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ Simple Hypervideo Downloader GUI is intended to serve as a basic interface for downloading YouTube videos/audio that can be easily shared and used by non-programmers. -![](images/hypervideo_gui.png) +![](images/screenshot.png) ### Dependencies - PyQt5 (`pacman -S python-pyqt5`) - hypervideo (`pacman -S hypervideo`) - ffmpeg (`pacman -S ffmpeg`) +### Install + +`pacman -S hypervideo-gui` + ### Usage -1. Run `python hypervideo_gui.py` +1. Run `hypervideo-gui` in terminal 2. Enter the URL of the video you want to download into the top "URL" textbox. 3. Then enter the folder to save the video to in the "Output Folder" textbox. 4. Next choose the video format @@ -29,8 +33,8 @@ For default, download path settings are saved in file `~/.config/hypervideo_gui/hypervideo_gui_state.json` ### Developer Setup -Just set up Python3 with libraries PyQT5. -`cd` to the repository path, and then `python hypervideo_gui.py`. +Just set up Python3 with libraries PyQt5. +`cd` to the repository path, and then `python script.py`. ### Troubleshooting Make sure you have the latest version of hypervideo! Use `pacman -Sy hypervideo`in the command line. diff --git a/bin/hypervideo-gui b/bin/hypervideo-gui new file mode 100644 index 0000000..e9c2fcf --- /dev/null +++ b/bin/hypervideo-gui @@ -0,0 +1,12 @@ +#!/usr/bin/python + +import sys + +from hypervideo_gui.main_ui import * + +# Exec APP +if __name__ == '__main__': + APP = QApplication(sys.argv) + APP.setStyle('Fusion') + ex = App() + sys.exit(APP.exec_()) diff --git a/hypervideo_gui.py b/hypervideo_gui.py deleted file mode 100644 index c668d27..0000000 --- a/hypervideo_gui.py +++ /dev/null @@ -1,422 +0,0 @@ -''' Hypervideo GUI ''' - -import sys -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("

Written with Python3 and PyQt5
" - "Version: %s
License: %s

" % (__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) - -if __name__ == '__main__': - APP = QApplication(sys.argv) - APP.setStyle('Fusion') - ex = App() - sys.exit(APP.exec_()) diff --git a/hypervideo_gui/__init__.py b/hypervideo_gui/__init__.py new file mode 100644 index 0000000..e69de29 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("

Written with Python3 and PyQt5
" + "Version: %s
License: %s

" % (__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) diff --git a/images/hypervideo_gui.png b/images/hypervideo_gui.png deleted file mode 100644 index 7a8e558..0000000 Binary files a/images/hypervideo_gui.png and /dev/null differ diff --git a/images/screenshot.png b/images/screenshot.png new file mode 100644 index 0000000..7a8e558 Binary files /dev/null and b/images/screenshot.png differ diff --git a/script.py b/script.py new file mode 100644 index 0000000..9bc382f --- /dev/null +++ b/script.py @@ -0,0 +1,12 @@ +""" Hypervideo-GUI Script """ + +import sys + +from hypervideo_gui.main_ui import * + +# Exec APP +if __name__ == '__main__': + APP = QApplication(sys.argv) + APP.setStyle('Fusion') + ex = App() + sys.exit(APP.exec_()) diff --git a/setup-files/gnu/hypervideo-gui.desktop b/setup-files/gnu/hypervideo-gui.desktop new file mode 100644 index 0000000..8d067d9 --- /dev/null +++ b/setup-files/gnu/hypervideo-gui.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Hypervideo GUI +Comment=Simple Hypervideo Downloader GUI +Comment[es]=Interfaz gráfica para Hypervideo +Icon=hypervideo-gui +Exec=hypervideo-gui %F +Terminal=false +Categories=AudioVideo diff --git a/setup-files/gnu/icons/hicolor/128x128/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/128x128/apps/hypervideo-gui.png new file mode 100644 index 0000000..bbde900 Binary files /dev/null and b/setup-files/gnu/icons/hicolor/128x128/apps/hypervideo-gui.png differ diff --git a/setup-files/gnu/icons/hicolor/16x16/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/16x16/apps/hypervideo-gui.png new file mode 100644 index 0000000..6faa223 Binary files /dev/null and b/setup-files/gnu/icons/hicolor/16x16/apps/hypervideo-gui.png differ diff --git a/setup-files/gnu/icons/hicolor/192x192/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/192x192/apps/hypervideo-gui.png new file mode 100644 index 0000000..ffc7f07 Binary files /dev/null and b/setup-files/gnu/icons/hicolor/192x192/apps/hypervideo-gui.png differ diff --git a/setup-files/gnu/icons/hicolor/22x22/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/22x22/apps/hypervideo-gui.png new file mode 100644 index 0000000..121e09e Binary files /dev/null and b/setup-files/gnu/icons/hicolor/22x22/apps/hypervideo-gui.png differ diff --git a/setup-files/gnu/icons/hicolor/32x32/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/32x32/apps/hypervideo-gui.png new file mode 100644 index 0000000..782aeb6 Binary files /dev/null and b/setup-files/gnu/icons/hicolor/32x32/apps/hypervideo-gui.png differ diff --git a/setup-files/gnu/icons/hicolor/384x384/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/384x384/apps/hypervideo-gui.png new file mode 100644 index 0000000..196dc2a Binary files /dev/null and b/setup-files/gnu/icons/hicolor/384x384/apps/hypervideo-gui.png differ diff --git a/setup-files/gnu/icons/hicolor/48x48/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/48x48/apps/hypervideo-gui.png new file mode 100644 index 0000000..6ee0f0f Binary files /dev/null and b/setup-files/gnu/icons/hicolor/48x48/apps/hypervideo-gui.png differ diff --git a/setup-files/gnu/icons/hicolor/64x64/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/64x64/apps/hypervideo-gui.png new file mode 100644 index 0000000..dcb995c Binary files /dev/null and b/setup-files/gnu/icons/hicolor/64x64/apps/hypervideo-gui.png differ diff --git a/setup-files/gnu/icons/hicolor/96x96/apps/hypervideo-gui.png b/setup-files/gnu/icons/hicolor/96x96/apps/hypervideo-gui.png new file mode 100644 index 0000000..328483f Binary files /dev/null and b/setup-files/gnu/icons/hicolor/96x96/apps/hypervideo-gui.png differ diff --git a/setup.py b/setup.py index 14f8fc2..2fa9a2c 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,57 @@ -# coding: utf-8 +""" Setup """ +import os +import sys -from distutils.core import setup +try: + from setuptools import setup, Command + setuptools_available = True +except ImportError: + from distutils.core import setup, Command + setuptools_available = False -from hypervideo_gui import __version__, __license__ +from hypervideo_gui.main_ui import ( + __version__, + __license__, +) + +script_exec = os.path.join('bin', 'hypervideo-gui') + + +# Begin - Desktop file and Icon +if sys.platform.startswith("linux"): + platform = 'gnu' +else: + sys.stderr.write("Unknown platform: %s" % sys.platform) + +ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) +SETUP_DIR = os.path.join(ROOT_DIR, 'setup-files', platform) + + +def icon_data_files(): + ''' Loop icon files ''' + sizes = [16, 22, 32, 48, 64, 96, 128, 192, 384] + data_icons_files = [] + for size in sizes: + icon_dir = os.path.join("icons", "hicolor", "%sx%s" % (size, size), "apps") + source = os.path.join(SETUP_DIR, icon_dir, "hypervideo-gui.png") + dest = os.path.join("/usr/share/", icon_dir) + data_icons_files.append((dest, [source])) + return data_icons_files + + +def application_data_files(): + ''' Desktop file ''' + return [ + ('/usr/share/applications', + [os.path.join(SETUP_DIR, 'hypervideo-gui.desktop')]), + ] + + +def data_files(): + ''' Return desktop file and icons ''' + return application_data_files() + icon_data_files() +# setup( author="Jesús E.", @@ -15,5 +63,7 @@ setup( url="https://libregit.org/heckyel/hypervideo-gui", packages=[ 'hypervideo_gui', - ] + ], + scripts=[script_exec], + data_files=data_files(), ) -- cgit v1.2.3