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.

582 lines
19 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. """Youtubedlg module responsible for the main app window. """
  4. from __future__ import unicode_literals
  5. import gettext
  6. import wx
  7. from wx.lib.pubsub import setuparg1
  8. from wx.lib.pubsub import pub as Publisher
  9. from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
  10. from .optionsframe import OptionsFrame
  11. from .updatemanager import (
  12. UPDATE_PUB_TOPIC,
  13. UpdateThread
  14. )
  15. from .downloadmanager import (
  16. MANAGER_PUB_TOPIC,
  17. WORKER_PUB_TOPIC,
  18. DownloadManager
  19. )
  20. from .utils import (
  21. get_icon_file,
  22. shutdown_sys,
  23. get_time,
  24. open_dir
  25. )
  26. from .info import (
  27. __appname__
  28. )
  29. class MainFrame(wx.Frame):
  30. """Main window class.
  31. This class is responsible for creating the main app window
  32. and binding the events.
  33. Attributes:
  34. BUTTONS_SIZE (tuple): Buttons size (width, height).
  35. BUTTONS_SPACE (tuple): Space between buttons (width, height).
  36. SIZE_20 (int): Constant size number.
  37. SIZE_10 (int): Constant size number.
  38. SIZE_5 (int): Constant size number.
  39. Labels area (strings): Strings for the widgets labels.
  40. STATUSLIST_COLUMNS (tuple): Tuple of tuples that contains informations
  41. about the ListCtrl columns. First item is the column name. Second
  42. item is the column position. Third item is the column label.
  43. Fourth item is the column default width. Last item is a boolean
  44. flag if True the current column is resizable.
  45. Args:
  46. opt_manager (optionsmanager.OptionsManager): Object responsible for
  47. handling the settings.
  48. log_manager (logmanager.LogManager): Object responsible for handling
  49. the log stuff.
  50. parent (wx.Window): Frame parent.
  51. """
  52. BUTTONS_SIZE = (90, 30)
  53. BUTTONS_SPACE = (80, -1)
  54. SIZE_20 = 20
  55. SIZE_10 = 10
  56. SIZE_5 = 5
  57. # Labels area
  58. URLS_LABEL = _("URLs")
  59. DOWNLOAD_LABEL = _("Download")
  60. UPDATE_LABEL = _("Update")
  61. OPTIONS_LABEL = _("Options")
  62. ERROR_LABEL = _("Error")
  63. STOP_LABEL = _("Stop")
  64. INFO_LABEL = _("Info")
  65. WELCOME_MSG = _("Welcome")
  66. SUCC_REPORT_MSG = _("Successfully downloaded {0} url(s) in {1} "
  67. "day(s) {2} hour(s) {3} minute(s) {4} second(s)")
  68. DL_COMPLETED_MSG = _("Download completed")
  69. URL_REPORT_MSG = _("Downloading {0} url(s)")
  70. CLOSING_MSG = _("Stopping downloads")
  71. CLOSED_MSG = _("Downloads stopped")
  72. PROVIDE_URL_MSG = _("You need to provide at least one url")
  73. DOWNLOAD_STARTED = _("Download started")
  74. UPDATING_MSG = _("Downloading latest youtube-dl. Please wait...")
  75. UPDATE_ERR_MSG = _("Youtube-dl download failed [{0}]")
  76. UPDATE_SUCC_MSG = _("Youtube-dl downloaded correctly")
  77. OPEN_DIR_ERR = _("Unable to open directory: '{dir}'. "
  78. "The specified path does not exist")
  79. SHUTDOWN_ERR = _("Error while shutting down. "
  80. "Make sure you typed the correct password")
  81. SHUTDOWN_MSG = _("Shutting down system")
  82. VIDEO_LABEL = _("Title")
  83. SIZE_LABEL = _("Size")
  84. PERCENT_LABEL = _("Percent")
  85. ETA_LABEL = _("ETA")
  86. SPEED_LABEL = _("Speed")
  87. STATUS_LABEL = _("Status")
  88. #################################
  89. STATUSLIST_COLUMNS = (
  90. ('filename', 0, VIDEO_LABEL, 150, True),
  91. ('filesize', 1, SIZE_LABEL, 80, False),
  92. ('percent', 2, PERCENT_LABEL, 65, False),
  93. ('eta', 3, ETA_LABEL, 45, False),
  94. ('speed', 4, SPEED_LABEL, 90, False),
  95. ('status', 5, STATUS_LABEL, 160, False)
  96. )
  97. def __init__(self, opt_manager, log_manager, parent=None):
  98. wx.Frame.__init__(self, parent, title=__appname__, size=opt_manager.options['main_win_size'])
  99. self.opt_manager = opt_manager
  100. self.log_manager = log_manager
  101. self.download_manager = None
  102. self.update_thread = None
  103. self.app_icon = get_icon_file()
  104. # Create the app icon
  105. if self.app_icon is not None:
  106. self.app_icon = wx.Icon(self.app_icon, wx.BITMAP_TYPE_PNG)
  107. self.SetIcon(self.app_icon)
  108. # Create options frame
  109. self._options_frame = OptionsFrame(self)
  110. # Create components
  111. self._panel = wx.Panel(self)
  112. self._url_text = self._create_statictext(self.URLS_LABEL)
  113. self._url_list = self._create_textctrl(wx.TE_MULTILINE | wx.TE_DONTWRAP, self._on_urllist_edit)
  114. self._download_btn = self._create_button(self.DOWNLOAD_LABEL, self._on_download)
  115. self._update_btn = self._create_button(self.UPDATE_LABEL, self._on_update)
  116. self._options_btn = self._create_button(self.OPTIONS_LABEL, self._on_options)
  117. self._status_list = ListCtrl(self.STATUSLIST_COLUMNS,
  118. parent=self._panel,
  119. style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
  120. self._status_bar = self._create_statictext(self.WELCOME_MSG)
  121. # Bind extra events
  122. self.Bind(wx.EVT_CLOSE, self._on_close)
  123. self._set_sizers()
  124. # Set threads wxCallAfter handlers using subscribe
  125. self._set_publisher(self._update_handler, UPDATE_PUB_TOPIC)
  126. self._set_publisher(self._status_list_handler, WORKER_PUB_TOPIC)
  127. self._set_publisher(self._download_manager_handler, MANAGER_PUB_TOPIC)
  128. def _set_publisher(self, handler, topic):
  129. """Sets a handler for the given topic.
  130. Args:
  131. handler (function): Can be any function with one parameter
  132. the message that the caller sends.
  133. topic (string): Can be any string that identifies the caller.
  134. You can bind multiple handlers on the same topic or
  135. multiple topics on the same handler.
  136. """
  137. Publisher.subscribe(handler, topic)
  138. def _create_statictext(self, label):
  139. statictext = wx.StaticText(self._panel, label=label)
  140. return statictext
  141. def _create_textctrl(self, style=None, event_handler=None):
  142. if style is None:
  143. textctrl = wx.TextCtrl(self._panel)
  144. else:
  145. textctrl = wx.TextCtrl(self._panel, style=style)
  146. if event_handler is not None:
  147. textctrl.Bind(wx.EVT_TEXT_PASTE, event_handler)
  148. return textctrl
  149. def _create_button(self, label, event_handler=None):
  150. btn = wx.Button(self._panel, label=label, size=self.BUTTONS_SIZE)
  151. if event_handler is not None:
  152. btn.Bind(wx.EVT_BUTTON, event_handler)
  153. return btn
  154. def _create_popup(self, text, title, style):
  155. wx.MessageBox(text, title, style)
  156. def _set_sizers(self):
  157. """Sets the sizers of the main window. """
  158. hor_sizer = wx.BoxSizer(wx.HORIZONTAL)
  159. vertical_sizer = wx.BoxSizer(wx.VERTICAL)
  160. vertical_sizer.AddSpacer(self.SIZE_10)
  161. vertical_sizer.Add(self._url_text)
  162. vertical_sizer.Add(self._url_list, 1, wx.EXPAND)
  163. vertical_sizer.AddSpacer(self.SIZE_10)
  164. buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
  165. buttons_sizer.Add(self._download_btn)
  166. buttons_sizer.Add(self.BUTTONS_SPACE, 1)
  167. buttons_sizer.Add(self._update_btn)
  168. buttons_sizer.Add(self.BUTTONS_SPACE, 1)
  169. buttons_sizer.Add(self._options_btn)
  170. vertical_sizer.Add(buttons_sizer, flag=wx.ALIGN_CENTER_HORIZONTAL)
  171. vertical_sizer.AddSpacer(self.SIZE_10)
  172. vertical_sizer.Add(self._status_list, 2, wx.EXPAND)
  173. vertical_sizer.AddSpacer(self.SIZE_5)
  174. vertical_sizer.Add(self._status_bar)
  175. vertical_sizer.AddSpacer(self.SIZE_5)
  176. hor_sizer.Add(vertical_sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, border=self.SIZE_20)
  177. self._panel.SetSizer(hor_sizer)
  178. def _update_youtubedl(self):
  179. """Update youtube-dl binary to the latest version. """
  180. self._update_btn.Disable()
  181. self._download_btn.Disable()
  182. self.update_thread = UpdateThread(self.opt_manager.options['youtubedl_path'])
  183. def _status_bar_write(self, msg):
  184. """Display msg in the status bar. """
  185. self._status_bar.SetLabel(msg)
  186. def _reset_widgets(self):
  187. """Resets GUI widgets after update or download process. """
  188. self._download_btn.SetLabel(self.DOWNLOAD_LABEL)
  189. self._download_btn.Enable()
  190. self._update_btn.Enable()
  191. def _print_stats(self):
  192. """Display download stats in the status bar. """
  193. suc_downloads = self.download_manager.successful
  194. dtime = get_time(self.download_manager.time_it_took)
  195. msg = self.SUCC_REPORT_MSG.format(suc_downloads,
  196. dtime['days'],
  197. dtime['hours'],
  198. dtime['minutes'],
  199. dtime['seconds'])
  200. self._status_bar_write(msg)
  201. def _after_download(self):
  202. """Run tasks after download process has been completed.
  203. Note:
  204. Here you can add any tasks you want to run after the
  205. download process has been completed.
  206. """
  207. if self.opt_manager.options['shutdown']:
  208. self.opt_manager.save_to_file()
  209. success = shutdown_sys(self.opt_manager.options['sudo_password'])
  210. if success:
  211. self._status_bar_write(self.SHUTDOWN_MSG)
  212. else:
  213. self._status_bar_write(self.SHUTDOWN_ERR)
  214. else:
  215. self._create_popup(self.DL_COMPLETED_MSG, self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION)
  216. if self.opt_manager.options['open_dl_dir']:
  217. success = open_dir(self.opt_manager.options['save_path'])
  218. if not success:
  219. self._status_bar_write(self.OPEN_DIR_ERR.format(dir=self.opt_manager.options['save_path']))
  220. def _status_list_handler(self, msg):
  221. """downloadmanager.Worker thread handler.
  222. Handles messages from the Worker thread.
  223. Args:
  224. See downloadmanager.Worker _talk_to_gui() method.
  225. """
  226. data = msg.data
  227. self._status_list.write(data)
  228. # Report number of urls been downloaded
  229. msg = self.URL_REPORT_MSG.format(self.download_manager.active())
  230. self._status_bar_write(msg)
  231. def _download_manager_handler(self, msg):
  232. """downloadmanager.DownloadManager thread handler.
  233. Handles messages from the DownloadManager thread.
  234. Args:
  235. See downloadmanager.DownloadManager _talk_to_gui() method.
  236. """
  237. data = msg.data
  238. if data == 'finished':
  239. self._print_stats()
  240. self._reset_widgets()
  241. self.download_manager = None
  242. self._after_download()
  243. elif data == 'closed':
  244. self._status_bar_write(self.CLOSED_MSG)
  245. self._reset_widgets()
  246. self.download_manager = None
  247. else:
  248. self._status_bar_write(self.CLOSING_MSG)
  249. def _update_handler(self, msg):
  250. """updatemanager.UpdateThread thread handler.
  251. Handles messages from the UpdateThread thread.
  252. Args:
  253. See updatemanager.UpdateThread _talk_to_gui() method.
  254. """
  255. data = msg.data
  256. if data[0] == 'download':
  257. self._status_bar_write(self.UPDATING_MSG)
  258. elif data[0] == 'error':
  259. self._status_bar_write(self.UPDATE_ERR_MSG.format(data[1]))
  260. elif data[0] == 'correct':
  261. self._status_bar_write(self.UPDATE_SUCC_MSG)
  262. else:
  263. self._reset_widgets()
  264. self.update_thread = None
  265. def _get_urls(self):
  266. """Returns urls list. """
  267. return self._url_list.GetValue().split('\n')
  268. def _start_download(self):
  269. """Handles pre-download tasks & starts the download process. """
  270. self._status_list.clear()
  271. self._status_list.load_urls(self._get_urls())
  272. if self._status_list.is_empty():
  273. self._create_popup(self.PROVIDE_URL_MSG,
  274. self.ERROR_LABEL,
  275. wx.OK | wx.ICON_EXCLAMATION)
  276. else:
  277. self.download_manager = DownloadManager(self._status_list.get_items(),
  278. self.opt_manager,
  279. self.log_manager)
  280. self._status_bar_write(self.DOWNLOAD_STARTED)
  281. self._download_btn.SetLabel(self.STOP_LABEL)
  282. self._update_btn.Disable()
  283. def _on_urllist_edit(self, event):
  284. """Event handler of the self._url_list widget.
  285. This method is triggered by the wx.EVT_TEXT_PASTE.
  286. """
  287. if not wx.TheClipboard.IsOpened():
  288. # Auto append newline on paste
  289. if wx.TheClipboard.Open():
  290. if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)):
  291. data = wx.TextDataObject()
  292. wx.TheClipboard.GetData(data)
  293. self._url_list.WriteText(data.GetText() + '\n')
  294. wx.TheClipboard.Close()
  295. # Dynamically add urls after download process has started
  296. if self.download_manager is not None:
  297. self._status_list.load_urls(self._get_urls(), self.download_manager.add_url)
  298. def _on_download(self, event):
  299. """Event handler of the self._download_btn widget.
  300. This method is used when the download-stop button is pressed to
  301. start or stop the download process.
  302. """
  303. if self.download_manager is None:
  304. self._start_download()
  305. else:
  306. self.download_manager.stop_downloads()
  307. def _on_update(self, event):
  308. """Event handler of the self._update_btn widget.
  309. This method is used when the update button is pressed to start
  310. the update process.
  311. Note:
  312. Currently there is not way to stop the update process.
  313. """
  314. self._update_youtubedl()
  315. def _on_options(self, event):
  316. """Event handler of the self._options_btn widget.
  317. This method is used when the options button is pressed to show
  318. the optios window.
  319. """
  320. self._options_frame.load_all_options()
  321. self._options_frame.Show()
  322. def _on_close(self, event):
  323. """Event handler for the wx.EVT_CLOSE event.
  324. This method is used when the user tries to close the program
  325. to save the options and make sure that the download & update
  326. processes are not running.
  327. """
  328. if self.download_manager is not None:
  329. self.download_manager.stop_downloads()
  330. self.download_manager.join()
  331. if self.update_thread is not None:
  332. self.update_thread.join()
  333. # Store main-options frame size
  334. self.opt_manager.options['main_win_size'] = self.GetSize()
  335. self.opt_manager.options['opts_win_size'] = self._options_frame.GetSize()
  336. self._options_frame.save_all_options()
  337. self.opt_manager.save_to_file()
  338. self.Destroy()
  339. class ListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin):
  340. """Custom ListCtrl widget.
  341. Args:
  342. columns (tuple): See MainFrame class STATUSLIST_COLUMNS attribute.
  343. """
  344. def __init__(self, columns, *args, **kwargs):
  345. wx.ListCtrl.__init__(self, *args, **kwargs)
  346. ListCtrlAutoWidthMixin.__init__(self)
  347. self.columns = columns
  348. self._list_index = 0
  349. self._url_list = set()
  350. self._set_columns()
  351. def write(self, data):
  352. """Write data on ListCtrl row-column.
  353. Args:
  354. data (dictionary): Dictionary that contains the data to be
  355. written on the ListCtrl. In order for this method to
  356. write the given data there must be an 'index' key that
  357. identifies the current row and a corresponding key for
  358. each item of the self.columns.
  359. Note:
  360. Income data must contain all the columns keys else a KeyError will
  361. be raised. Also there must be an 'index' key that identifies the
  362. row to write the data. For a valid data dictionary see
  363. downloaders.YoutubeDLDownloader self._data.
  364. """
  365. for column in self.columns:
  366. column_key = column[0]
  367. self._write_data(data[column_key], data['index'], column[1])
  368. def load_urls(self, url_list, func=None):
  369. """Load URLs from the url_list on the ListCtrl widget.
  370. Args:
  371. url_list (list): List of strings that contains the URLs to add.
  372. func (function): Callback function. It's used to add the URLs
  373. on the download_manager.
  374. """
  375. for url in url_list:
  376. url = url.replace(' ', '')
  377. if url and not self.has_url(url):
  378. self.add_url(url)
  379. if func is not None:
  380. # Custom hack to add url into download_manager
  381. item = self._get_item(self._list_index - 1)
  382. func(item)
  383. def has_url(self, url):
  384. """Returns True if the url is aleady in the ListCtrl else False.
  385. Args:
  386. url (string): URL string.
  387. """
  388. return url in self._url_list
  389. def add_url(self, url):
  390. """Adds the given url in the ListCtrl.
  391. Args:
  392. url (string): URL string.
  393. """
  394. self.InsertStringItem(self._list_index, url)
  395. self._url_list.add(url)
  396. self._list_index += 1
  397. def clear(self):
  398. """Clear the ListCtrl widget & reset self._list_index and
  399. self._url_list. """
  400. self.DeleteAllItems()
  401. self._list_index = 0
  402. self._url_list = set()
  403. def is_empty(self):
  404. """Returns True if the list is empty else False. """
  405. return self._list_index == 0
  406. def get_items(self):
  407. """Returns a list of items inside the ListCtrl.
  408. Returns:
  409. List of dictionaries that contains the 'url' and the
  410. 'index'(row) for each item of the ListCtrl.
  411. """
  412. items = []
  413. for row in xrange(self._list_index):
  414. item = self._get_item(row)
  415. items.append(item)
  416. return items
  417. def _write_data(self, data, row, column):
  418. """Write data on row-column. """
  419. if isinstance(data, basestring):
  420. self.SetStringItem(row, column, data)
  421. def _get_item(self, index):
  422. """Returns the corresponding ListCtrl item for the given index.
  423. Args:
  424. index (int): Index that identifies the row of the item.
  425. Index must be smaller than the self._list_index.
  426. Returns:
  427. Dictionary that contains the URL string of the row and the
  428. row number(index).
  429. """
  430. item = self.GetItem(itemId=index, col=0)
  431. data = dict(url=item.GetText(), index=index)
  432. return data
  433. def _set_columns(self):
  434. """Initializes ListCtrl columns.
  435. See MainFrame STATUSLIST_COLUMNS attribute for more info. """
  436. for column in self.columns:
  437. self.InsertColumn(column[1], column[2], width=column[3])
  438. if column[4]:
  439. self.setResizeColumn(column[1])