|
|
#!/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()
|