You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
424 lines
13 KiB
424 lines
13 KiB
#!/usr/bin/env python2
|
|
|
|
''' Contains code for main app frame & custom ListCtrl. '''
|
|
|
|
import os.path
|
|
|
|
import wx
|
|
from wx.lib.pubsub import setuparg1
|
|
from wx.lib.pubsub import pub as Publisher
|
|
|
|
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
|
|
|
|
from .optionsframe import OptionsFrame
|
|
from .updthread import UpdateThread
|
|
from .dlthread import DownloadManager
|
|
|
|
from .utils import (
|
|
YOUTUBEDL_BIN,
|
|
get_config_path,
|
|
get_icon_file,
|
|
shutdown_sys,
|
|
get_time,
|
|
open_dir
|
|
)
|
|
from .data import (
|
|
__appname__
|
|
)
|
|
|
|
|
|
class MainFrame(wx.Frame):
|
|
|
|
''' Youtube-dlG main frame. '''
|
|
|
|
FRAME_SIZE = (700, 490)
|
|
TEXTCTRL_SIZE = (-1, -1)
|
|
BUTTONS_SIZE = (90, 30)
|
|
BUTTONS_SPACE = 80
|
|
SIZE_20 = 20
|
|
SIZE_10 = 10
|
|
SIZE_5 = 5
|
|
|
|
URLS_LABEL = "URLs"
|
|
DOWNLOAD_LABEL = "Download"
|
|
UPDATE_LABEL = "Update"
|
|
OPTIONS_LABEL = "Options"
|
|
ERROR_LABEL = "Error"
|
|
STOP_LABEL = "Stop"
|
|
INFO_LABEL = "Info"
|
|
WELCOME_MSG = "Welcome"
|
|
SUCC_REPORT_MSG = ("Successfully downloaded {0} url(s) in {1} "
|
|
"day(s) {2} hour(s) {3} minute(s) {4} second(s)")
|
|
DL_COMPLETED_MSG = "Download completed"
|
|
URL_REPORT_MSG = "Downloading {0} url(s)"
|
|
CLOSING_MSG = "Stopping downloads"
|
|
CLOSED_MSG = "Downloads stopped"
|
|
PROVIDE_URL_MSG = "You need to provide at least one url"
|
|
DOWNLOAD_STARTED = "Download started"
|
|
|
|
VIDEO_LABEL = "Video"
|
|
SIZE_LABEL = "Size"
|
|
PERCENT_LABEL = "Percent"
|
|
ETA_LABEL = "ETA"
|
|
SPEED_LABEL = "Speed"
|
|
STATUS_LABEL = "Status"
|
|
|
|
STATUSLIST_COLUMNS = (
|
|
('filename', 0, VIDEO_LABEL, 150, True),
|
|
('filesize', 1, SIZE_LABEL, 80, False),
|
|
('percent', 2, PERCENT_LABEL, 65, False),
|
|
('eta', 3, ETA_LABEL, 45, False),
|
|
('speed', 4, SPEED_LABEL, 90, False),
|
|
('status', 5, STATUS_LABEL, 160, False)
|
|
)
|
|
|
|
def __init__(self, opt_manager, log_manager, parent=None):
|
|
wx.Frame.__init__(self, parent, title=__appname__, size=self.FRAME_SIZE)
|
|
self.opt_manager = opt_manager
|
|
self.log_manager = log_manager
|
|
self.download_manager = None
|
|
self.update_thread = None
|
|
self.app_icon = get_icon_file()
|
|
|
|
if self.app_icon is not None:
|
|
self.app_icon = wx.Icon(self.app_icon, wx.BITMAP_TYPE_PNG)
|
|
self.SetIcon(self.app_icon)
|
|
|
|
# Create options frame
|
|
self._options_frame = OptionsFrame(self)
|
|
|
|
# Create components
|
|
self._panel = wx.Panel(self)
|
|
|
|
self._url_text = self._create_statictext(self.URLS_LABEL)
|
|
self._url_list = self._create_textctrl(wx.TE_MULTILINE | wx.TE_DONTWRAP, self._on_urllist_edit)
|
|
|
|
self._download_btn = self._create_button(self.DOWNLOAD_LABEL, self._on_download)
|
|
self._update_btn = self._create_button(self.UPDATE_LABEL, self._on_update)
|
|
self._options_btn = self._create_button(self.OPTIONS_LABEL, self._on_options)
|
|
|
|
self._status_list = ListCtrl(self.STATUSLIST_COLUMNS,
|
|
parent=self._panel,
|
|
style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
|
|
|
|
self._status_bar = self._create_statictext(self.WELCOME_MSG)
|
|
|
|
# Bind extra events
|
|
self.Bind(wx.EVT_CLOSE, self._on_close)
|
|
|
|
self._set_sizers()
|
|
|
|
# Set threads wx.CallAfter handlers using subscribe
|
|
self._set_publisher(self._update_handler, 'update')
|
|
self._set_publisher(self._status_list_handler, 'dlworker')
|
|
self._set_publisher(self._download_manager_handler, 'dlmanager')
|
|
|
|
def _set_publisher(self, handler, topic):
|
|
Publisher.subscribe(handler, topic)
|
|
|
|
def _create_statictext(self, label):
|
|
statictext = wx.StaticText(self._panel, label=label)
|
|
return statictext
|
|
|
|
def _create_textctrl(self, style=None, event_handler=None):
|
|
if style is None:
|
|
textctrl = wx.TextCtrl(self._panel, size=self.TEXTCTRL_SIZE)
|
|
else:
|
|
textctrl = wx.TextCtrl(self._panel, size=self.TEXTCTRL_SIZE, style=style)
|
|
|
|
if event_handler is not None:
|
|
textctrl.Bind(wx.EVT_TEXT, event_handler)
|
|
|
|
return textctrl
|
|
|
|
def _create_button(self, label, event_handler=None):
|
|
btn = wx.Button(self._panel, label=label, size=self.BUTTONS_SIZE)
|
|
|
|
if event_handler is not None:
|
|
btn.Bind(wx.EVT_BUTTON, event_handler)
|
|
|
|
return btn
|
|
|
|
def _create_popup(self, text, title, style):
|
|
''' Create popup message. '''
|
|
wx.MessageBox(text, title, style)
|
|
|
|
def _set_sizers(self):
|
|
hor_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
vertical_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
vertical_sizer.AddSpacer(self.SIZE_10)
|
|
|
|
vertical_sizer.Add(self._url_text)
|
|
vertical_sizer.Add(self._url_list, 1, wx.EXPAND)
|
|
|
|
vertical_sizer.AddSpacer(self.SIZE_10)
|
|
|
|
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
buttons_sizer.Add(self._download_btn)
|
|
buttons_sizer.Add(self._update_btn, flag=wx.LEFT | wx.RIGHT, border=self.BUTTONS_SPACE)
|
|
buttons_sizer.Add(self._options_btn)
|
|
vertical_sizer.Add(buttons_sizer, flag=wx.ALIGN_CENTER_HORIZONTAL)
|
|
|
|
vertical_sizer.AddSpacer(self.SIZE_10)
|
|
|
|
vertical_sizer.Add(self._status_list, 2, wx.EXPAND)
|
|
|
|
vertical_sizer.AddSpacer(self.SIZE_5)
|
|
|
|
vertical_sizer.Add(self._status_bar)
|
|
|
|
vertical_sizer.AddSpacer(self.SIZE_5)
|
|
|
|
hor_sizer.Add(vertical_sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, border=self.SIZE_20)
|
|
|
|
self._panel.SetSizer(hor_sizer)
|
|
|
|
def _update_youtubedl(self):
|
|
''' Update youtube-dl executable. '''
|
|
self._update_btn.Disable()
|
|
self._download_btn.Disable()
|
|
self.update_thread = UpdateThread(self.opt_manager.options['youtubedl_path'])
|
|
|
|
def _status_bar_write(self, msg):
|
|
''' Write msg to self._status_bar. '''
|
|
self._status_bar.SetLabel(msg)
|
|
|
|
def _reset_buttons(self):
|
|
''' Reset GUI and variables after download process. '''
|
|
self._download_btn.SetLabel(self.DOWNLOAD_LABEL)
|
|
self._download_btn.Enable()
|
|
self._update_btn.Enable()
|
|
|
|
def _print_stats(self):
|
|
''' Print stats to self._status_bar after downloading. '''
|
|
suc_downloads = self.download_manager.successful
|
|
dtime = get_time(self.download_manager.time_it_took)
|
|
|
|
days = int(dtime['days'])
|
|
hours = int(dtime['hours'])
|
|
minutes = int(dtime['minutes'])
|
|
seconds = int(dtime['seconds'])
|
|
|
|
msg = self.SUCC_REPORT_MSG.format(suc_downloads, days, hours, minutes, seconds)
|
|
|
|
self._status_bar_write(msg)
|
|
|
|
def _after_download(self):
|
|
''' Run tasks after download process has finished. '''
|
|
if self.opt_manager.options['shutdown']:
|
|
self.opt_manager.save_to_file()
|
|
shutdown_sys(self.opt_manager.options['sudo_password'])
|
|
else:
|
|
self._create_popup(self.DL_COMPLETED_MSG, self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION)
|
|
if self.opt_manager.options['open_dl_dir']:
|
|
open_dir(self.opt_manager.options['save_path'])
|
|
|
|
def _status_list_handler(self, msg):
|
|
data = msg.data
|
|
|
|
self._status_list.write(data)
|
|
|
|
# Report urls been downloaded
|
|
msg = self.URL_REPORT_MSG.format(self.download_manager.active())
|
|
self._status_bar_write(msg)
|
|
|
|
def _download_manager_handler(self, msg):
|
|
''' Handle messages from DownloadManager. '''
|
|
data = msg.data
|
|
|
|
if data == 'finished':
|
|
self._print_stats()
|
|
self._reset_buttons()
|
|
self.download_manager = None
|
|
self._after_download()
|
|
elif data == 'closed':
|
|
self._status_bar_write(self.CLOSED_MSG)
|
|
self._reset_buttons()
|
|
self.download_manager = None
|
|
else:
|
|
self._status_bar_write(self.CLOSING_MSG)
|
|
|
|
def _update_handler(self, msg):
|
|
''' Handle messages from UpdateThread. '''
|
|
data = msg.data
|
|
|
|
if data == 'finish':
|
|
self._reset_buttons()
|
|
self.update_thread = None
|
|
else:
|
|
self._status_bar_write(data)
|
|
|
|
def _get_urls(self):
|
|
return self._url_list.GetValue().split('\n')
|
|
|
|
def _start_download(self):
|
|
''' Handle pre-download tasks & start download process. '''
|
|
self._status_list.clear()
|
|
self._status_list.load_urls(self._get_urls())
|
|
|
|
if self._status_list.is_empty():
|
|
self._create_popup(self.PROVIDE_URL_MSG,
|
|
self.ERROR_LABEL,
|
|
wx.OK | wx.ICON_EXCLAMATION)
|
|
else:
|
|
self.download_manager = DownloadManager(self._status_list.get_items(),
|
|
self.opt_manager,
|
|
self.log_manager)
|
|
|
|
self._status_bar_write(self.DOWNLOAD_STARTED)
|
|
self._download_btn.SetLabel(self.STOP_LABEL)
|
|
self._update_btn.Disable()
|
|
|
|
def _on_urllist_edit(self, event):
|
|
''' Dynamically add url for download.'''
|
|
if self.download_manager is not None:
|
|
self._status_list.load_urls(self._get_urls(), self.download_manager.add_url)
|
|
|
|
def _on_download(self, event):
|
|
''' Event handler method for self._download_btn. '''
|
|
if self.download_manager is None:
|
|
self._start_download()
|
|
else:
|
|
self.download_manager.stop_downloads()
|
|
|
|
def _on_update(self, event):
|
|
''' Event handler method for self._update_btn. '''
|
|
self._update_youtubedl()
|
|
|
|
def _on_options(self, event):
|
|
''' Event handler method for self._options_btn. '''
|
|
self._options_frame.load_all_options()
|
|
self._options_frame.Show()
|
|
|
|
def _on_close(self, event):
|
|
''' Event handler method (wx.EVT_CLOSE). '''
|
|
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()
|
|
|
|
self.opt_manager.save_to_file()
|
|
self.Destroy()
|
|
|
|
|
|
class ListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin):
|
|
|
|
'''
|
|
Custom ListCtrl class.
|
|
|
|
Accessible Methods
|
|
write()
|
|
Params: Python dictionary that contains data to write
|
|
|
|
Return: None
|
|
|
|
has_url()
|
|
Params: Url to search
|
|
|
|
Return: True if url in ListCtrl, else False
|
|
|
|
add_url()
|
|
Params: Url to add
|
|
|
|
Return: None
|
|
|
|
clear()
|
|
Params: None
|
|
|
|
Return: None
|
|
|
|
is_empty()
|
|
Params: None
|
|
|
|
Return: True if ListCtrl is empty, else False
|
|
|
|
get_items()
|
|
Params: None
|
|
|
|
Return: Python list that contains all ListCtrl items
|
|
|
|
get_last_item()
|
|
Params: None
|
|
|
|
Return: Last item inserted in ListCtrl
|
|
'''
|
|
|
|
def __init__(self, columns, *args, **kwargs):
|
|
wx.ListCtrl.__init__(self, *args, **kwargs)
|
|
ListCtrlAutoWidthMixin.__init__(self)
|
|
self.columns = columns
|
|
self._list_index = 0
|
|
self._url_list = set()
|
|
self._set_columns()
|
|
|
|
def write(self, data):
|
|
''' Write data on ListCtrl row-column. '''
|
|
for key in data:
|
|
for column in self.columns:
|
|
if key == column[0]:
|
|
self._write_data(data[key], data['index'], column[1])
|
|
break
|
|
|
|
def load_urls(self, url_list, func=None):
|
|
for url in url_list:
|
|
url = url.replace(' ', '')
|
|
|
|
if url and not self.has_url(url):
|
|
self.add_url(url)
|
|
|
|
if func is not None:
|
|
# Custom hack to add url into download_manager
|
|
# i am gonna change this
|
|
item = self._get_item(self._list_index - 1)
|
|
func(item)
|
|
|
|
def has_url(self, url):
|
|
''' Return True if url in ListCtrl, else return False. '''
|
|
return url in self._url_list
|
|
|
|
def add_url(self, url):
|
|
''' Add url on ListCtrl. '''
|
|
self.InsertStringItem(self._list_index, url)
|
|
self._url_list.add(url)
|
|
self._list_index += 1
|
|
|
|
def clear(self):
|
|
''' Clear ListCtrl & reset self._list_index. '''
|
|
self.DeleteAllItems()
|
|
self._list_index = 0
|
|
self._url_list = set()
|
|
|
|
def is_empty(self):
|
|
''' Return True if list is empty. '''
|
|
return self._list_index == 0
|
|
|
|
def get_items(self):
|
|
''' Return list of items in ListCtrl. '''
|
|
items = []
|
|
|
|
for row in xrange(self._list_index):
|
|
item = self._get_item(row)
|
|
items.append(item)
|
|
|
|
return items
|
|
|
|
def _write_data(self, data, row, column):
|
|
''' Write data on row, column. '''
|
|
if isinstance(data, basestring):
|
|
self.SetStringItem(row, column, data)
|
|
|
|
def _get_item(self, index):
|
|
''' Return single item base on index. '''
|
|
item = self.GetItem(itemId=index, col=0)
|
|
data = dict(url=item.GetText(), index=index)
|
|
return data
|
|
|
|
def _set_columns(self):
|
|
for column in self.columns:
|
|
self.InsertColumn(column[1], column[2], width=column[3])
|
|
if column[4]:
|
|
self.setResizeColumn(column[1])
|
|
|