#!/usr/bin/env python2 # -*- coding: utf-8 -*- """Youtubedlg module responsible for the main app window. """ from __future__ import unicode_literals import os import gettext import wx from wx.lib.pubsub import setuparg1 #NOTE Should remove deprecated from wx.lib.pubsub import pub as Publisher from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin from .parsers import OptionsParser from .optionsframe import ( OptionsFrame, LogGUI ) from .updatemanager import ( UPDATE_PUB_TOPIC, UpdateThread ) from .downloadmanager import ( MANAGER_PUB_TOPIC, WORKER_PUB_TOPIC, DownloadManager, DownloadList, DownloadItem ) from .utils import ( get_pixmaps_dir, build_command, get_icon_file, shutdown_sys, remove_file, open_file, get_time ) from .widgets import CustomComboBox from .formats import ( DEFAULT_FORMATS, VIDEO_FORMATS, AUDIO_FORMATS, FORMATS ) from .info import ( __descriptionfull__, __licensefull__, __projecturl__, __appname__, __author__ ) from .version import __version__ class MainFrame(wx.Frame): """Main window class. This class is responsible for creating the main app window and binding the events. Attributes: FRAMES_MIN_SIZE (tuple): Tuple that contains the minumum width, height of the frame. Labels area (strings): Strings for the widgets labels. STATUSLIST_COLUMNS (dict): Python dictionary which holds informations about the wxListCtrl columns. For more informations read the comments above the STATUSLIST_COLUMNS declaration. Args: opt_manager (optionsmanager.OptionsManager): Object responsible for handling the settings. log_manager (logmanager.LogManager): Object responsible for handling the log stuff. parent (wx.Window): Frame parent. """ FRAMES_MIN_SIZE = (560, 360) # Labels area URLS_LABEL = _("Enter URLs below") UPDATE_LABEL = _("Update") OPTIONS_LABEL = _("Options") STOP_LABEL = _("Stop") INFO_LABEL = _("Info") WELCOME_MSG = _("Welcome") WARNING_LABEL = _("Warning") ADD_LABEL = _("Add") DOWNLOAD_LIST_LABEL = _("Download list") DELETE_LABEL = _("Delete") PLAY_LABEL = _("Play") UP_LABEL = _("Up") DOWN_LABEL = _("Down") RELOAD_LABEL = _("Reload") PAUSE_LABEL = _("Pause") START_LABEL = _("Start") ABOUT_LABEL = _("About") VIEWLOG_LABEL = _("View Log") SUCC_REPORT_MSG = _("Successfully downloaded {0} URL(s) in {1} " "day(s) {2} hour(s) {3} minute(s) {4} second(s)") DL_COMPLETED_MSG = _("Downloads completed") URL_REPORT_MSG = _("Total Progress: {0:.1f}% | Queued ({1}) Paused ({2}) Active ({3}) Completed ({4}) Error ({5})") CLOSING_MSG = _("Stopping downloads") CLOSED_MSG = _("Downloads stopped") PROVIDE_URL_MSG = _("You need to provide at least one URL") DOWNLOAD_STARTED = _("Downloads started") CHOOSE_DIRECTORY = _("Choose Directory") DOWNLOAD_ACTIVE = _("Download in progress. Please wait for all downloads to complete") UPDATE_ACTIVE = _("Update already in progress") UPDATING_MSG = _("Downloading latest youtube-dl. Please wait...") UPDATE_ERR_MSG = _("Youtube-dl download failed [{0}]") UPDATE_SUCC_MSG = _("Successfully downloaded youtube-dl") OPEN_DIR_ERR = _("Unable to open directory: '{dir}'. " "The specified path does not exist") SHUTDOWN_ERR = _("Error while shutting down. " "Make sure you typed the correct password") SHUTDOWN_MSG = _("Shutting down system") VIDEO_LABEL = _("Title") EXTENSION_LABEL = _("Extension") SIZE_LABEL = _("Size") PERCENT_LABEL = _("Percent") ETA_LABEL = _("ETA") SPEED_LABEL = _("Speed") STATUS_LABEL = _("Status") ################################# # STATUSLIST_COLUMNS # # Dictionary which contains the columns for the wxListCtrl widget. # Each key represents a column and holds informations about itself. # Structure informations: # column_key: (column_number, column_label, minimum_width, is_resizable) # STATUSLIST_COLUMNS = { 'filename': (0, VIDEO_LABEL, 150, True), 'extension': (1, EXTENSION_LABEL, 60, False), 'filesize': (2, SIZE_LABEL, 80, False), 'percent': (3, PERCENT_LABEL, 65, False), 'eta': (4, ETA_LABEL, 45, False), 'speed': (5, SPEED_LABEL, 90, False), 'status': (6, STATUS_LABEL, 160, False) } def __init__(self, opt_manager, log_manager, parent=None): super(MainFrame, self).__init__(parent, wx.ID_ANY, __appname__, size=opt_manager.options["main_win_size"]) self.opt_manager = opt_manager self.log_manager = log_manager self.download_manager = None self.update_thread = None self.app_icon = None #REFACTOR Get and set on __init__.py self._download_list = DownloadList() # Set up youtube-dl options parser self._options_parser = OptionsParser() # Get the pixmaps directory self._pixmaps_path = get_pixmaps_dir() # Set the Timer self._app_timer = wx.Timer(self) # Set the app icon app_icon_path = get_icon_file() if app_icon_path is not None: self.app_icon = wx.Icon(app_icon_path, wx.BITMAP_TYPE_PNG) self.SetIcon(self.app_icon) bitmap_data = ( ("down", "arrow_down_32px.png"), ("up", "arrow_up_32px.png"), ("play", "camera_32px.png"), ("start", "cloud_download_32px.png"), ("delete", "delete_32px.png"), ("folder", "folder_32px.png"), ("pause", "pause_32px.png"), ("resume", "play_arrow_32px.png"), ("reload", "reload_32px.png"), ("settings", "settings_20px.png"), ("stop", "stop_32px.png") ) self._bitmaps = {} for item in bitmap_data: target, name = item self._bitmaps[target] = wx.Bitmap(os.path.join(self._pixmaps_path, name)) # Set the data for all the wx.Button items # name, label, size, event_handler buttons_data = ( ("delete", self.DELETE_LABEL, (-1, -1), self._on_delete, wx.BitmapButton), ("play", self.PLAY_LABEL, (-1, -1), self._on_play, wx.BitmapButton), ("up", self.UP_LABEL, (-1, -1), self._on_arrow_up, wx.BitmapButton), ("down", self.DOWN_LABEL, (-1, -1), self._on_arrow_down, wx.BitmapButton), ("reload", self.RELOAD_LABEL, (-1, -1), self._on_reload, wx.BitmapButton), ("pause", self.PAUSE_LABEL, (-1, -1), self._on_pause, wx.BitmapButton), ("start", self.START_LABEL, (-1, -1), self._on_start, wx.BitmapButton), ("savepath", "...", (35, -1), self._on_savepath, wx.Button), ("add", self.ADD_LABEL, (-1, -1), self._on_add, wx.Button) ) # Set the data for the settings menu item # label, event_handler settings_menu_data = ( (self.OPTIONS_LABEL, self._on_options), (self.UPDATE_LABEL, self._on_update), (self.VIEWLOG_LABEL, self._on_viewlog), (self.ABOUT_LABEL, self._on_about) ) statuslist_menu_data = ( (_("Get URL"), self._on_geturl), (_("Get command"), self._on_getcmd), (_("Open destination"), self._on_open_dest), (_("Re-enter"), self._on_reenter) ) # Create options frame self._options_frame = OptionsFrame(self) # Create frame components self._panel = wx.Panel(self) self._url_text = self._create_statictext(self.URLS_LABEL) #REFACTOR Move to buttons_data self._settings_button = self._create_bitmap_button(self._bitmaps["settings"], (30, 30), self._on_settings) self._url_list = self._create_textctrl(wx.TE_MULTILINE | wx.TE_DONTWRAP, self._on_urllist_edit) self._folder_icon = self._create_static_bitmap(self._bitmaps["folder"], self._on_open_path) self._path_combobox = ExtComboBox(self._panel, 5, style=wx.CB_READONLY) self._videoformat_combobox = CustomComboBox(self._panel, style=wx.CB_READONLY) self._download_text = self._create_statictext(self.DOWNLOAD_LIST_LABEL) self._status_list = ListCtrl(self.STATUSLIST_COLUMNS, parent=self._panel, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES) # Dictionary to store all the buttons self._buttons = {} for item in buttons_data: name, label, size, evt_handler, parent = item button = parent(self._panel, size=size) if parent == wx.Button: button.SetLabel(label) elif parent == wx.BitmapButton: button.SetToolTip(wx.ToolTip(label)) if name in self._bitmaps: button.SetBitmap(self._bitmaps[name], wx.TOP) if evt_handler is not None: button.Bind(wx.EVT_BUTTON, evt_handler) self._buttons[name] = button self._status_bar = self.CreateStatusBar() # Create extra components self._settings_menu = self._create_menu_item(settings_menu_data) self._statuslist_menu = self._create_menu_item(statuslist_menu_data) # Overwrite the menu hover event to avoid changing the statusbar self.Bind(wx.EVT_MENU_HIGHLIGHT, lambda event: None) # Bind extra events self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self._on_statuslist_right_click, self._status_list) self.Bind(wx.EVT_TEXT, self._update_savepath, self._path_combobox) self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._update_pause_button, self._status_list) self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self._update_pause_button, self._status_list) self.Bind(wx.EVT_CLOSE, self._on_close) self.Bind(wx.EVT_TIMER, self._on_timer, self._app_timer) self._videoformat_combobox.Bind(wx.EVT_COMBOBOX, self._update_videoformat) # Set threads wxCallAfter handlers self._set_publisher(self._update_handler, UPDATE_PUB_TOPIC) self._set_publisher(self._download_worker_handler, WORKER_PUB_TOPIC) self._set_publisher(self._download_manager_handler, MANAGER_PUB_TOPIC) # Set up extra stuff self.Center() self.SetMinSize(self.FRAMES_MIN_SIZE) self._status_bar_write(self.WELCOME_MSG) self._update_videoformat_combobox() self._path_combobox.LoadMultiple(self.opt_manager.options["save_path_dirs"]) self._path_combobox.SetValue(self.opt_manager.options["save_path"]) self._set_layout() self._url_list.SetFocus() def _create_menu_item(self, items): menu = wx.Menu() for item in items: label, evt_handler = item menu_item = menu.Append(-1, label) menu.Bind(wx.EVT_MENU, evt_handler, menu_item) return menu def _on_statuslist_right_click(self, event): selected = event.GetIndex() if selected != -1: self._status_list.deselect_all() self._status_list.Select(selected, on=1) self.PopupMenu(self._statuslist_menu) def _on_reenter(self, event): selected = self._status_list.get_selected() if selected != -1: object_id = self._status_list.GetItemData(selected) download_item = self._download_list.get_item(object_id) if download_item.stage != "Active": self._status_list.remove_row(selected) self._download_list.remove(object_id) options = self._options_parser.parse(self.opt_manager.options) download_item = DownloadItem(download_item.url, options) download_item.path = self.opt_manager.options["save_path"] if not self._download_list.has_item(download_item.object_id): self._status_list.bind_item(download_item) self._download_list.insert(download_item) def reset(self): self._update_videoformat_combobox() self._path_combobox.LoadMultiple(self.opt_manager.options["save_path_dirs"]) self._path_combobox.SetValue(self.opt_manager.options["save_path"]) def _on_open_dest(self, event): selected = self._status_list.get_selected() if selected != -1: object_id = self._status_list.GetItemData(selected) download_item = self._download_list.get_item(object_id) if download_item.path: open_file(download_item.path) def _on_open_path(self, event): open_file(self._path_combobox.GetValue()) def _on_geturl(self, event): selected = self._status_list.get_selected() if selected != -1: object_id = self._status_list.GetItemData(selected) download_item = self._download_list.get_item(object_id) url = download_item.url if not wx.TheClipboard.IsOpened(): clipdata = wx.TextDataObject() clipdata.SetText(url) wx.TheClipboard.Open() wx.TheClipboard.SetData(clipdata) wx.TheClipboard.Close() def _on_getcmd(self, event): selected = self._status_list.get_selected() if selected != -1: object_id = self._status_list.GetItemData(selected) download_item = self._download_list.get_item(object_id) cmd = build_command(download_item.options, download_item.url) if not wx.TheClipboard.IsOpened(): clipdata = wx.TextDataObject() clipdata.SetText(cmd) wx.TheClipboard.Open() wx.TheClipboard.SetData(clipdata) wx.TheClipboard.Close() def _on_timer(self, event): total_percentage = 0.0 queued = paused = active = completed = error = 0 for item in self._download_list.get_items(): if item.stage == "Queued": queued += 1 if item.stage == "Paused": paused += 1 if item.stage == "Active": active += 1 total_percentage += float(item.progress_stats["percent"].split('%')[0]) if item.stage == "Completed": completed += 1 if item.stage == "Error": error += 1 # REFACTOR Store percentage as float in the DownloadItem? # REFACTOR DownloadList keep track for each item stage? items_count = active + completed + error + queued total_percentage += completed * 100.0 + error * 100.0 if items_count: total_percentage /= items_count msg = self.URL_REPORT_MSG.format(total_percentage, queued, paused, active, completed, error) if self.update_thread is None: # Dont overwrite the update messages self._status_bar_write(msg) def _update_pause_button(self, event): selected_rows = self._status_list.get_all_selected() label = _("Pause") bitmap = self._bitmaps["pause"] for row in selected_rows: object_id = self._status_list.GetItemData(row) download_item = self._download_list.get_item(object_id) if download_item.stage == "Paused": # If we find one or more items in Paused # state set the button functionality to resume label = _("Resume") bitmap = self._bitmaps["resume"] break self._buttons["pause"].SetLabel(label) self._buttons["pause"].SetToolTip(wx.ToolTip(label)) self._buttons["pause"].SetBitmap(bitmap, wx.TOP) def _update_videoformat_combobox(self): self._videoformat_combobox.Clear() self._videoformat_combobox.add_items(list(DEFAULT_FORMATS.values()), False) vformats = [] for vformat in self.opt_manager.options["selected_video_formats"]: vformats.append(FORMATS[vformat]) aformats = [] for aformat in self.opt_manager.options["selected_audio_formats"]: aformats.append(FORMATS[aformat]) if vformats: self._videoformat_combobox.add_header(_("Video")) self._videoformat_combobox.add_items(vformats) if aformats: self._videoformat_combobox.add_header(_("Audio")) self._videoformat_combobox.add_items(aformats) current_index = self._videoformat_combobox.FindString(FORMATS[self.opt_manager.options["selected_format"]]) if current_index == wx.NOT_FOUND: self._videoformat_combobox.SetSelection(0) else: self._videoformat_combobox.SetSelection(current_index) self._update_videoformat(None) def _update_videoformat(self, event): self.opt_manager.options["selected_format"] = selected_format = FORMATS[self._videoformat_combobox.GetValue()] if selected_format in VIDEO_FORMATS: self.opt_manager.options["video_format"] = selected_format self.opt_manager.options["audio_format"] = "" #NOTE Set to default value, check parsers.py elif selected_format in AUDIO_FORMATS: self.opt_manager.options["video_format"] = DEFAULT_FORMATS[_("default")] self.opt_manager.options["audio_format"] = selected_format else: self.opt_manager.options["video_format"] = DEFAULT_FORMATS[_("default")] self.opt_manager.options["audio_format"] = "" def _update_savepath(self, event): self.opt_manager.options["save_path"] = self._path_combobox.GetValue() def _on_delete(self, event): index = self._status_list.get_next_selected() if index == -1: dlg = ButtonsChoiceDialog(self, [_("Remove all"), _("Remove completed")], _("No items selected. Please pick an action"), _("Delete")) ret_code = dlg.ShowModal() dlg.Destroy() #REFACTOR Maybe add this functionality directly to DownloadList? if ret_code == 1: for ditem in self._download_list.get_items(): if ditem.stage != "Active": self._status_list.remove_row(self._download_list.index(ditem.object_id)) self._download_list.remove(ditem.object_id) if ret_code == 2: for ditem in self._download_list.get_items(): if ditem.stage == "Completed": self._status_list.remove_row(self._download_list.index(ditem.object_id)) self._download_list.remove(ditem.object_id) else: if self.opt_manager.options["confirm_deletion"]: dlg = wx.MessageDialog(self, _("Are you sure you want to remove selected items?"), _("Delete"), wx.YES_NO | wx.ICON_QUESTION) result = dlg.ShowModal() == wx.ID_YES dlg.Destroy() else: result = True if result: while index >= 0: object_id = self._status_list.GetItemData(index) selected_download_item = self._download_list.get_item(object_id) if selected_download_item.stage == "Active": self._create_popup(_("Item is active, cannot remove"), self.WARNING_LABEL, wx.OK | wx.ICON_EXCLAMATION) else: #if selected_download_item.stage == "Completed": #dlg = wx.MessageDialog(self, "Do you want to remove the files associated with this item?", "Remove files", wx.YES_NO | wx.ICON_QUESTION) #result = dlg.ShowModal() == wx.ID_YES #dlg.Destroy() #if result: #for cur_file in selected_download_item.get_files(): #remove_file(cur_file) self._status_list.remove_row(index) self._download_list.remove(object_id) index -= 1 index = self._status_list.get_next_selected(index) self._update_pause_button(None) def _on_play(self, event): selected_rows = self._status_list.get_all_selected() if selected_rows: for selected_row in selected_rows: object_id = self._status_list.GetItemData(selected_row) selected_download_item = self._download_list.get_item(object_id) if selected_download_item.stage == "Completed": if selected_download_item.filenames: filename = selected_download_item.get_files()[-1] open_file(filename) else: self._create_popup(_("Item is not completed"), self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION) def _on_arrow_up(self, event): index = self._status_list.get_next_selected() if index != -1: while index >= 0: object_id = self._status_list.GetItemData(index) download_item = self._download_list.get_item(object_id) new_index = index - 1 if new_index < 0: new_index = 0 if not self._status_list.IsSelected(new_index): self._download_list.move_up(object_id) self._status_list.move_item_up(index) self._status_list._update_from_item(new_index, download_item) index = self._status_list.get_next_selected(index) def _on_arrow_down(self, event): index = self._status_list.get_next_selected(reverse=True) if index != -1: while index >= 0: object_id = self._status_list.GetItemData(index) download_item = self._download_list.get_item(object_id) new_index = index + 1 if new_index >= self._status_list.GetItemCount(): new_index = self._status_list.GetItemCount() - 1 if not self._status_list.IsSelected(new_index): self._download_list.move_down(object_id) self._status_list.move_item_down(index) self._status_list._update_from_item(new_index, download_item) index = self._status_list.get_next_selected(index, True) def _on_reload(self, event): selected_rows = self._status_list.get_all_selected() if not selected_rows: for index, item in enumerate(self._download_list.get_items()): if item.stage in ("Paused", "Completed", "Error"): # Store the old savepath because reset is going to remove it savepath = item.path item.reset() item.path = savepath self._status_list._update_from_item(index, item) else: for selected_row in selected_rows: object_id = self._status_list.GetItemData(selected_row) item = self._download_list.get_item(object_id) if item.stage in ("Paused", "Completed", "Error"): # Store the old savepath because reset is going to remove it savepath = item.path item.reset() item.path = savepath self._status_list._update_from_item(selected_row, item) self._update_pause_button(None) def _on_pause(self, event): selected_rows = self._status_list.get_all_selected() if selected_rows: #REFACTOR Use DoubleStageButton for this and check stage if self._buttons["pause"].GetLabel() == _("Pause"): new_state = "Paused" else: new_state = "Queued" for selected_row in selected_rows: object_id = self._status_list.GetItemData(selected_row) download_item = self._download_list.get_item(object_id) if download_item.stage == "Queued" or download_item.stage == "Paused": self._download_list.change_stage(object_id, new_state) self._status_list._update_from_item(selected_row, download_item) self._update_pause_button(None) def _on_start(self, event): if self.download_manager is None: if self.update_thread is not None and self.update_thread.is_alive(): self._create_popup(_("Update in progress. Please wait for the update to complete"), self.WARNING_LABEL, wx.OK | wx.ICON_EXCLAMATION) else: self._start_download() else: self.download_manager.stop_downloads() def _on_savepath(self, event): dlg = wx.DirDialog(self, self.CHOOSE_DIRECTORY, self._path_combobox.GetStringSelection()) if dlg.ShowModal() == wx.ID_OK: path = dlg.GetPath() self._path_combobox.Append(path) self._path_combobox.SetValue(path) self._update_savepath(None) dlg.Destroy() def _on_add(self, event): urls = self._get_urls() if not urls: self._create_popup(self.PROVIDE_URL_MSG, self.WARNING_LABEL, wx.OK | wx.ICON_EXCLAMATION) else: self._url_list.Clear() options = self._options_parser.parse(self.opt_manager.options) for url in urls: download_item = DownloadItem(url, options) download_item.path = self.opt_manager.options["save_path"] if not self._download_list.has_item(download_item.object_id): self._status_list.bind_item(download_item) self._download_list.insert(download_item) def _on_settings(self, event): event_object_pos = event.EventObject.GetPosition() event_object_height = event.EventObject.GetSize()[1] event_object_pos = (event_object_pos[0], event_object_pos[1] + event_object_height) self.PopupMenu(self._settings_menu, event_object_pos) def _on_viewlog(self, event): if self.log_manager is None: self._create_popup(_("Logging is disabled"), self.WARNING_LABEL, wx.OK | wx.ICON_EXCLAMATION) else: log_window = LogGUI(self) log_window.load(self.log_manager.log_file) log_window.Show() def _on_about(self, event): info = wx.AboutDialogInfo() if self.app_icon is not None: info.SetIcon(self.app_icon) info.SetName(__appname__) info.SetVersion(__version__) info.SetDescription(__descriptionfull__) info.SetWebSite(__projecturl__) info.SetLicense(__licensefull__) info.AddDeveloper(__author__) wx.AboutBox(info) def _set_publisher(self, handler, topic): """Sets a handler for the given topic. Args: handler (function): Can be any function with one parameter the message that the caller sends. topic (string): Can be any string that identifies the caller. You can bind multiple handlers on the same topic or multiple topics on the same handler. """ Publisher.subscribe(handler, topic) def _create_statictext(self, label): return wx.StaticText(self._panel, label=label) def _create_bitmap_button(self, icon, size=(-1, -1), handler=None): button = wx.BitmapButton(self._panel, bitmap=icon, size=size, style=wx.NO_BORDER) if handler is not None: button.Bind(wx.EVT_BUTTON, handler) return button def _create_static_bitmap(self, icon, event_handler=None): static_bitmap = wx.StaticBitmap(self._panel, bitmap=icon) if event_handler is not None: static_bitmap.Bind(wx.EVT_LEFT_DCLICK, event_handler) return static_bitmap def _create_textctrl(self, style=None, event_handler=None): if style is None: textctrl = wx.TextCtrl(self._panel) else: textctrl = wx.TextCtrl(self._panel, style=style) if event_handler is not None: textctrl.Bind(wx.EVT_TEXT_PASTE, event_handler) textctrl.Bind(wx.EVT_MIDDLE_DOWN, event_handler) if os.name == 'nt': # Enable CTRL+A on Windows def win_ctrla_eventhandler(event): if event.GetKeyCode() == wx.WXK_CONTROL_A: event.GetEventObject().SelectAll() event.Skip() textctrl.Bind(wx.EVT_CHAR, win_ctrla_eventhandler) return textctrl def _create_popup(self, text, title, style): wx.MessageBox(text, title, style) def _set_layout(self): """Sets the layout of the main window. """ main_sizer = wx.BoxSizer() panel_sizer = wx.BoxSizer(wx.VERTICAL) top_sizer = wx.BoxSizer(wx.HORIZONTAL) top_sizer.Add(self._url_text, 0, wx.ALIGN_BOTTOM | wx.BOTTOM, 5) top_sizer.AddSpacer((-1, -1), 1) top_sizer.Add(self._settings_button) panel_sizer.Add(top_sizer, 0, wx.EXPAND) panel_sizer.Add(self._url_list, 1, wx.EXPAND) mid_sizer = wx.BoxSizer(wx.HORIZONTAL) mid_sizer.Add(self._folder_icon) mid_sizer.AddSpacer((3, -1)) mid_sizer.Add(self._path_combobox, 2, wx.ALIGN_CENTER_VERTICAL) mid_sizer.AddSpacer((5, -1)) mid_sizer.Add(self._buttons["savepath"], flag=wx.ALIGN_CENTER_VERTICAL) mid_sizer.AddSpacer((10, -1), 1) mid_sizer.Add(self._videoformat_combobox, 1, wx.ALIGN_CENTER_VERTICAL) mid_sizer.AddSpacer((5, -1)) mid_sizer.Add(self._buttons["add"], flag=wx.ALIGN_CENTER_VERTICAL) panel_sizer.Add(mid_sizer, 0, wx.EXPAND | wx.ALL, 10) panel_sizer.Add(self._download_text, 0, wx.BOTTOM, 5) panel_sizer.Add(self._status_list, 2, wx.EXPAND) bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) bottom_sizer.Add(self._buttons["delete"]) bottom_sizer.AddSpacer((5, -1)) bottom_sizer.Add(self._buttons["play"]) bottom_sizer.AddSpacer((5, -1)) bottom_sizer.Add(self._buttons["up"]) bottom_sizer.AddSpacer((5, -1)) bottom_sizer.Add(self._buttons["down"]) bottom_sizer.AddSpacer((5, -1)) bottom_sizer.Add(self._buttons["reload"]) bottom_sizer.AddSpacer((5, -1)) bottom_sizer.Add(self._buttons["pause"]) bottom_sizer.AddSpacer((10, -1), 1) bottom_sizer.Add(self._buttons["start"]) panel_sizer.Add(bottom_sizer, 0, wx.EXPAND | wx.TOP, 5) main_sizer.Add(panel_sizer, 1, wx.ALL | wx.EXPAND, 10) self._panel.SetSizer(main_sizer) self._panel.Layout() def _update_youtubedl(self): """Update youtube-dl binary to the latest version. """ if self.download_manager is not None and self.download_manager.is_alive(): self._create_popup(self.DOWNLOAD_ACTIVE, self.WARNING_LABEL, wx.OK | wx.ICON_EXCLAMATION) elif self.update_thread is not None and self.update_thread.is_alive(): self._create_popup(self.UPDATE_ACTIVE, self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION) else: self.update_thread = UpdateThread(self.opt_manager.options['youtubedl_path']) def _status_bar_write(self, msg): """Display msg in the status bar. """ self._status_bar.SetStatusText(msg) def _reset_widgets(self): """Resets GUI widgets after update or download process. """ self._buttons["start"].SetLabel(_("Start")) self._buttons["start"].SetToolTip(wx.ToolTip(_("Start"))) self._buttons["start"].SetBitmap(self._bitmaps["start"], wx.TOP) def _print_stats(self): """Display download stats in the status bar. """ suc_downloads = self.download_manager.successful dtime = get_time(self.download_manager.time_it_took) msg = self.SUCC_REPORT_MSG.format(suc_downloads, dtime['days'], dtime['hours'], dtime['minutes'], dtime['seconds']) self._status_bar_write(msg) def _after_download(self): """Run tasks after download process has been completed. Note: Here you can add any tasks you want to run after the download process has been completed. """ if self.opt_manager.options['shutdown']: dlg = ShutdownDialog(self, 60, _("Shutting down in {0} second(s)"), _("Shutdown")) result = dlg.ShowModal() == wx.ID_OK dlg.Destroy() if result: self.opt_manager.save_to_file() success = shutdown_sys(self.opt_manager.options['sudo_password']) if success: self._status_bar_write(self.SHUTDOWN_MSG) else: self._status_bar_write(self.SHUTDOWN_ERR) else: if self.opt_manager.options["show_completion_popup"]: self._create_popup(self.DL_COMPLETED_MSG, self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION) def _download_worker_handler(self, msg): """downloadmanager.Worker thread handler. Handles messages from the Worker thread. Args: See downloadmanager.Worker _talk_to_gui() method. """ signal, data = msg.data download_item = self._download_list.get_item(data["index"]) download_item.update_stats(data) row = self._download_list.index(data["index"]) self._status_list._update_from_item(row, download_item) def _download_manager_handler(self, msg): """downloadmanager.DownloadManager thread handler. Handles messages from the DownloadManager thread. Args: See downloadmanager.DownloadManager _talk_to_gui() method. """ data = msg.data if data == 'finished': self._print_stats() self._reset_widgets() self.download_manager = None self._app_timer.Stop() self._after_download() elif data == 'closed': self._status_bar_write(self.CLOSED_MSG) self._reset_widgets() self.download_manager = None self._app_timer.Stop() elif data == 'closing': self._status_bar_write(self.CLOSING_MSG) elif data == 'report_active': pass #NOTE Remove from here and downloadmanager #since now we have the wx.Timer to check progress def _update_handler(self, msg): """updatemanager.UpdateThread thread handler. Handles messages from the UpdateThread thread. Args: See updatemanager.UpdateThread _talk_to_gui() method. """ data = msg.data if data[0] == 'download': self._status_bar_write(self.UPDATING_MSG) elif data[0] == 'error': self._status_bar_write(self.UPDATE_ERR_MSG.format(data[1])) elif data[0] == 'correct': self._status_bar_write(self.UPDATE_SUCC_MSG) else: self._reset_widgets() self.update_thread = None def _get_urls(self): """Returns urls list. """ return [line for line in self._url_list.GetValue().split('\n') if line] def _start_download(self): if self._status_list.is_empty(): self._create_popup(_("No items to download"), self.WARNING_LABEL, wx.OK | wx.ICON_EXCLAMATION) else: self._app_timer.Start(100) self.download_manager = DownloadManager(self, self._download_list, self.opt_manager, self.log_manager) self._status_bar_write(self.DOWNLOAD_STARTED) self._buttons["start"].SetLabel(self.STOP_LABEL) self._buttons["start"].SetToolTip(wx.ToolTip(self.STOP_LABEL)) self._buttons["start"].SetBitmap(self._bitmaps["stop"], wx.TOP) def _paste_from_clipboard(self): """Paste the content of the clipboard to the self._url_list widget. It also adds a new line at the end of the data if not exist. """ if not wx.TheClipboard.IsOpened(): if wx.TheClipboard.Open(): if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)): data = wx.TextDataObject() wx.TheClipboard.GetData(data) data = data.GetText() if data[-1] != '\n': data += '\n' self._url_list.WriteText(data) wx.TheClipboard.Close() def _on_urllist_edit(self, event): """Event handler of the self._url_list widget. This method is triggered when the users pastes text into the URLs list either by using CTRL+V or by using the middle click of the mouse. """ if event.GetEventType() == wx.EVT_TEXT_PASTE.typeId: self._paste_from_clipboard() else: wx.TheClipboard.UsePrimarySelection(True) self._paste_from_clipboard() wx.TheClipboard.UsePrimarySelection(False) def _on_update(self, event): """Event handler of the self._update_btn widget. This method is used when the update button is pressed to start the update process. Note: Currently there is not way to stop the update process. """ if self.opt_manager.options["disable_update"]: self._create_popup(_("Updates are disabled for your system. Please use the system's package manager to update youtube-dl."), self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION) else: self._update_youtubedl() def _on_options(self, event): """Event handler of the self._options_btn widget. This method is used when the options button is pressed to show the options window. """ self._options_frame.load_all_options() self._options_frame.Show() def _on_close(self, event): """Event handler for the wx.EVT_CLOSE event. This method is used when the user tries to close the program to save the options and make sure that the download & update processes are not running. """ if self.opt_manager.options["confirm_exit"]: dlg = wx.MessageDialog(self, _("Are you sure you want to exit?"), _("Exit"), wx.YES_NO | wx.ICON_QUESTION) result = dlg.ShowModal() == wx.ID_YES dlg.Destroy() else: result = True if result: self.close() def close(self): if self.download_manager is not None: self.download_manager.stop_downloads() self.download_manager.join() if self.update_thread is not None: self.update_thread.join() # Store main-options frame size self.opt_manager.options['main_win_size'] = self.GetSize() self.opt_manager.options['opts_win_size'] = self._options_frame.GetSize() self.opt_manager.options["save_path_dirs"] = self._path_combobox.GetStrings() self._options_frame.save_all_options() self.opt_manager.save_to_file() self.Destroy() class ListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin): """Custom ListCtrl widget. Args: columns (dict): See MainFrame class STATUSLIST_COLUMNS attribute. """ def __init__(self, columns, *args, **kwargs): super(ListCtrl, self).__init__(*args, **kwargs) ListCtrlAutoWidthMixin.__init__(self) self.columns = columns self._list_index = 0 self._url_list = set() self._set_columns() def remove_row(self, row_number): self.DeleteItem(row_number) self._list_index -= 1 def move_item_up(self, row_number): self._move_item(row_number, row_number - 1) def move_item_down(self, row_number): self._move_item(row_number, row_number + 1) def _move_item(self, cur_row, new_row): self.Freeze() item = self.GetItem(cur_row) self.DeleteItem(cur_row) item.SetId(new_row) self.InsertItem(item) self.Select(new_row) self.Thaw() def has_url(self, url): """Returns True if the url is aleady in the ListCtrl else False. Args: url (string): URL string. """ return url in self._url_list def bind_item(self, download_item): self.InsertStringItem(self._list_index, download_item.url) self.SetItemData(self._list_index, download_item.object_id) self._update_from_item(self._list_index, download_item) self._list_index += 1 def _update_from_item(self, row, download_item): progress_stats = download_item.progress_stats for key in self.columns: column = self.columns[key][0] if key == "status" and progress_stats["playlist_index"]: # Not the best place but we build the playlist status here status = "{0} {1}/{2}".format(progress_stats["status"], progress_stats["playlist_index"], progress_stats["playlist_size"]) self.SetStringItem(row, column, status) else: self.SetStringItem(row, column, progress_stats[key]) def clear(self): """Clear the ListCtrl widget & reset self._list_index and self._url_list. """ self.DeleteAllItems() self._list_index = 0 self._url_list = set() def is_empty(self): """Returns True if the list is empty else False. """ return self._list_index == 0 def get_selected(self): return self.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) def get_all_selected(self): return [index for index in xrange(self._list_index) if self.IsSelected(index)] def deselect_all(self): for index in xrange(self._list_index): self.Select(index, on=0) def get_next_selected(self, start=-1, reverse=False): if start == -1: start = self._list_index - 1 if reverse else 0 else: # start from next item if reverse: start -= 1 else: start += 1 end = -1 if reverse else self._list_index step = -1 if reverse else 1 for index in xrange(start, end, step): if self.IsSelected(index): return index return -1 def _set_columns(self): """Initializes ListCtrl columns. See MainFrame STATUSLIST_COLUMNS attribute for more info. """ for column_item in sorted(self.columns.values()): self.InsertColumn(column_item[0], column_item[1], width=wx.LIST_AUTOSIZE_USEHEADER) # If the column width obtained from wxLIST_AUTOSIZE_USEHEADER # is smaller than the minimum allowed column width # then set the column width to the minumum allowed size if self.GetColumnWidth(column_item[0]) < column_item[2]: self.SetColumnWidth(column_item[0], column_item[2]) # Set auto-resize if enabled if column_item[3]: self.setResizeColumn(column_item[0]) # REFACTOR Extra widgets below should move to other module with widgets class ExtComboBox(wx.ComboBox): def __init__(self, parent, max_items=-1, *args, **kwargs): super(ExtComboBox, self).__init__(parent, *args, **kwargs) assert max_items > 0 or max_items == -1 self.max_items = max_items def Append(self, new_value): if self.FindString(new_value) == wx.NOT_FOUND: super(ExtComboBox, self).Append(new_value) if self.max_items != -1 and self.GetCount() > self.max_items: self.SetItems(self.GetStrings()[1:]) def SetValue(self, new_value): if self.FindString(new_value) == wx.NOT_FOUND: self.Append(new_value) self.SetSelection(self.FindString(new_value)) def LoadMultiple(self, items_list): for item in items_list: self.Append(item) class DoubleStageButton(wx.Button): def __init__(self, parent, labels, bitmaps, bitmap_pos=wx.TOP, *args, **kwargs): super(DoubleStageButton, self).__init__(parent, *args, **kwargs) assert isinstance(labels, tuple) and isinstance(bitmaps, tuple) assert len(labels) == 2 assert len(bitmaps) == 0 or len(bitmaps) == 2 self.labels = labels self.bitmaps = bitmaps self.bitmap_pos = bitmap_pos self._stage = 0 self._set_layout() def _set_layout(self): self.SetLabel(self.labels[self._stage]) if len(self.bitmaps): self.SetBitmap(self.bitmaps[self._stage], self.bitmap_pos) def change_stage(self): self._stage = 0 if self._stage else 1 self._set_layout() def set_stage(self, new_stage): assert new_stage == 0 or new_stage == 1 self._stage = new_stage self._set_layout() class ButtonsChoiceDialog(wx.Dialog): if os.name == "nt": STYLE = wx.DEFAULT_DIALOG_STYLE else: STYLE = wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX BORDER = 10 def __init__(self, parent, choices, message, *args, **kwargs): super(ButtonsChoiceDialog, self).__init__(parent, wx.ID_ANY, *args, style=self.STYLE, **kwargs) buttons = [] # Create components panel = wx.Panel(self) info_bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX) info_icon = wx.StaticBitmap(panel, wx.ID_ANY, info_bmp) msg_text = wx.StaticText(panel, wx.ID_ANY, message) buttons.append(wx.Button(panel, wx.ID_CANCEL, _("Cancel"))) for index, label in enumerate(choices): buttons.append(wx.Button(panel, index + 1, label)) # Get the maximum button width & height max_width = max_height = -1 for button in buttons: button_width, button_height = button.GetSize() if button_width > max_width: max_width = button_width if button_height > max_height: max_height = button_height max_width += 10 # Set buttons width & bind events for button in buttons: if button != buttons[0]: button.SetMinSize((max_width, max_height)) else: # On Close button change only the height button.SetMinSize((-1, max_height)) button.Bind(wx.EVT_BUTTON, self._on_close) # Set sizers vertical_sizer = wx.BoxSizer(wx.VERTICAL) message_sizer = wx.BoxSizer(wx.HORIZONTAL) message_sizer.Add(info_icon) message_sizer.AddSpacer((10, 10)) message_sizer.Add(msg_text, flag=wx.EXPAND) vertical_sizer.Add(message_sizer, 1, wx.ALL, border=self.BORDER) buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) for button in buttons[1:]: buttons_sizer.Add(button) buttons_sizer.AddSpacer((5, -1)) buttons_sizer.AddSpacer((-1, -1), 1) buttons_sizer.Add(buttons[0], flag=wx.ALIGN_RIGHT) vertical_sizer.Add(buttons_sizer, flag=wx.EXPAND | wx.ALL, border=self.BORDER) panel.SetSizer(vertical_sizer) width, height = panel.GetBestSize() self.SetSize((width, height * 1.3)) self.Center() def _on_close(self, event): self.EndModal(event.GetEventObject().GetId()) class ButtonsGroup(object): WIDTH = 0 HEIGHT = 1 def __init__(self, buttons_list=None, squared=False): if buttons_list is None: self._buttons_list = [] else: self._buttons_list = buttons_list self._squared = squared def set_size(self, size): assert len(size) == 2 width, height = size if width == -1: for button in self._buttons_list: cur_width = button.GetSize()[self.WIDTH] if cur_width > width: width = cur_width if height == -1: for button in self._buttons_list: cur_height = button.GetSize()[self.HEIGHT] if cur_height > height: height = cur_height if self._squared: width = height = (width if width > height else height) for button in self._buttons_list: button.SetMinSize((width, height)) def create_sizer(self, orient=wx.HORIZONTAL, space=-1): box_sizer = wx.BoxSizer(orient) for button in self._buttons_list: box_sizer.Add(button) if space != -1: box_sizer.AddSpacer((space, space)) return box_sizer def bind_event(self, event, event_handler): for button in self._buttons_list: button.Bind(event, event_handler) def disable_all(self): for button in self._buttons_list: button.Enable(False) def enable_all(self): for button in self._buttons_list: button.Enable(True) def add(self, button): self._buttons_list.append(button) class ShutdownDialog(wx.Dialog): if os.name == "nt": STYLE = wx.DEFAULT_DIALOG_STYLE else: STYLE = wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX TIMER_INTERVAL = 1000 # milliseconds BORDER = 10 def __init__(self, parent, timeout, message, *args, **kwargs): super(ShutdownDialog, self).__init__(parent, wx.ID_ANY, *args, style=self.STYLE, **kwargs) assert timeout > 0 self.timeout = timeout self.message = message # Create components panel = wx.Panel(self) info_bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX) info_icon = wx.StaticBitmap(panel, wx.ID_ANY, info_bmp) self.msg_text = msg_text = wx.StaticText(panel, wx.ID_ANY, self._get_message()) ok_button = wx.Button(panel, wx.ID_OK, _("OK")) cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel")) # Set layout vertical_sizer = wx.BoxSizer(wx.VERTICAL) message_sizer = wx.BoxSizer(wx.HORIZONTAL) message_sizer.Add(info_icon) message_sizer.AddSpacer((10, 10)) message_sizer.Add(msg_text, flag=wx.EXPAND) vertical_sizer.Add(message_sizer, 1, wx.ALL, border=self.BORDER) buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) buttons_sizer.Add(ok_button) buttons_sizer.AddSpacer((5, -1)) buttons_sizer.Add(cancel_button) vertical_sizer.Add(buttons_sizer, flag=wx.ALIGN_RIGHT | wx.ALL, border=self.BORDER) panel.SetSizer(vertical_sizer) width, height = panel.GetBestSize() self.SetSize((width * 1.3, height * 1.3)) self.Center() # Set up timer self.timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self._on_timer, self.timer) self.timer.Start(self.TIMER_INTERVAL) def _get_message(self): return self.message.format(self.timeout) def _on_timer(self, event): self.timeout -= 1 self.msg_text.SetLabel(self._get_message()) if self.timeout <= 0: self.EndModal(wx.ID_OK) def Destroy(self): self.timer.Stop() return super(ShutdownDialog, self).Destroy()