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.

1027 lines
34 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
  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 os
  6. import gettext
  7. import wx
  8. from wx.lib.pubsub import setuparg1
  9. from wx.lib.pubsub import pub as Publisher
  10. from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
  11. from .parsers import OptionsParser
  12. from .optionsframe import (
  13. OptionsFrame,
  14. LogGUI
  15. )
  16. from .updatemanager import (
  17. UPDATE_PUB_TOPIC,
  18. UpdateThread
  19. )
  20. from .downloadmanager import (
  21. MANAGER_PUB_TOPIC,
  22. WORKER_PUB_TOPIC,
  23. DownloadManager
  24. )
  25. from .utils import (
  26. get_pixmaps_dir,
  27. get_config_path,
  28. get_icon_file,
  29. shutdown_sys,
  30. read_formats,
  31. json_store,
  32. json_load,
  33. to_string,
  34. get_time,
  35. open_dir
  36. )
  37. from .info import (
  38. __descriptionfull__,
  39. __licensefull__,
  40. __projecturl__,
  41. __appname__,
  42. __author__
  43. )
  44. from .version import __version__
  45. class MainFrame(wx.Frame):
  46. """Main window class.
  47. This class is responsible for creating the main app window
  48. and binding the events.
  49. Attributes:
  50. wxEVT_TEXT_PASTE (int): Event type code for the wx.EVT_TEXT_PASTE
  51. BUTTONS_SIZE (tuple): Buttons size (width, height).
  52. BUTTONS_SPACE (tuple): Space between buttons (width, height).
  53. SIZE_20 (int): Constant size number.
  54. SIZE_10 (int): Constant size number.
  55. SIZE_5 (int): Constant size number.
  56. Labels area (strings): Strings for the widgets labels.
  57. STATUSLIST_COLUMNS (dict): Python dictionary which holds informations
  58. about the wxListCtrl columns. For more informations read the
  59. comments above the STATUSLIST_COLUMNS declaration.
  60. Args:
  61. opt_manager (optionsmanager.OptionsManager): Object responsible for
  62. handling the settings.
  63. log_manager (logmanager.LogManager): Object responsible for handling
  64. the log stuff.
  65. parent (wx.Window): Frame parent.
  66. """
  67. wxEVT_TEXT_PASTE = 'wxClipboardTextEvent'
  68. FRAMES_MIN_SIZE = (440, 360)
  69. BUTTONS_SIZE = (-1, 30) # TODO remove not used anymore
  70. BUTTONS_SPACE = (80, -1) # TODO remove not used anymore
  71. SIZE_20 = 20 # TODO write directly
  72. SIZE_10 = 10
  73. SIZE_5 = 5
  74. # Labels area
  75. URLS_LABEL = _("Enter URLs below")
  76. DOWNLOAD_LABEL = _("Download") # TODO remove not used anymore
  77. UPDATE_LABEL = _("Update")
  78. OPTIONS_LABEL = _("Options")
  79. ERROR_LABEL = _("Error")
  80. STOP_LABEL = _("Stop")
  81. INFO_LABEL = _("Info")
  82. WELCOME_MSG = _("Welcome")
  83. ADD_LABEL = _("Add")
  84. DOWNLOAD_LIST_LABEL = _("Download list")
  85. DELETE_LABEL = _("Delete")
  86. PLAY_LABEL = _("Play")
  87. UP_LABEL = _("Up")
  88. DOWN_LABEL = _("Down")
  89. RELOAD_LABEL = _("Reload")
  90. PAUSE_LABEL = _("Pause")
  91. START_LABEL = _("Start")
  92. ABOUT_LABEL = _("About")
  93. VIEWLOG_LABEL = _("View Log")
  94. SUCC_REPORT_MSG = _("Successfully downloaded {0} url(s) in {1} "
  95. "day(s) {2} hour(s) {3} minute(s) {4} second(s)")
  96. DL_COMPLETED_MSG = _("Downloads completed")
  97. URL_REPORT_MSG = _("Downloading {0} url(s)")
  98. CLOSING_MSG = _("Stopping downloads")
  99. CLOSED_MSG = _("Downloads stopped")
  100. PROVIDE_URL_MSG = _("You need to provide at least one url")
  101. DOWNLOAD_STARTED = _("Downloads started")
  102. CHOOSE_DIRECTORY = _("Choose Directory")
  103. DOWNLOAD_ACTIVE = _("Download in progress. Please wait for all the downloads to complete")
  104. UPDATE_ACTIVE = _("Update already in progress.")
  105. UPDATING_MSG = _("Downloading latest youtube-dl. Please wait...")
  106. UPDATE_ERR_MSG = _("Youtube-dl download failed [{0}]")
  107. UPDATE_SUCC_MSG = _("Youtube-dl downloaded correctly")
  108. OPEN_DIR_ERR = _("Unable to open directory: '{dir}'. "
  109. "The specified path does not exist")
  110. SHUTDOWN_ERR = _("Error while shutting down. "
  111. "Make sure you typed the correct password")
  112. SHUTDOWN_MSG = _("Shutting down system")
  113. VIDEO_LABEL = _("Title")
  114. EXTENSION_LABEL = _("Extension")
  115. SIZE_LABEL = _("Size")
  116. PERCENT_LABEL = _("Percent")
  117. ETA_LABEL = _("ETA")
  118. SPEED_LABEL = _("Speed")
  119. STATUS_LABEL = _("Status")
  120. #################################
  121. # STATUSLIST_COLUMNS
  122. #
  123. # Dictionary which contains the columns for the wxListCtrl widget.
  124. # Each key represents a column and holds informations about itself.
  125. # Structure informations:
  126. # column_key: (column_number, column_label, minimum_width, is_resizable)
  127. #
  128. STATUSLIST_COLUMNS = {
  129. 'filename': (0, VIDEO_LABEL, 150, True),
  130. 'extension': (1, EXTENSION_LABEL, 60, False),
  131. 'filesize': (2, SIZE_LABEL, 80, False),
  132. 'percent': (3, PERCENT_LABEL, 65, False),
  133. 'eta': (4, ETA_LABEL, 45, False),
  134. 'speed': (5, SPEED_LABEL, 90, False),
  135. 'status': (6, STATUS_LABEL, 160, False)
  136. }
  137. def __init__(self, opt_manager, log_manager, parent=None):
  138. wx.Frame.__init__(self, parent, title=__appname__, size=opt_manager.options['main_win_size'])
  139. self.opt_manager = opt_manager
  140. self.log_manager = log_manager
  141. self.download_manager = None
  142. self.update_thread = None
  143. self.app_icon = None
  144. # TODO move it elsewhere?
  145. self._download_items = {}
  146. # Set up youtube-dl options parser
  147. self._options_parser = OptionsParser()
  148. # Get the pixmaps directory
  149. self._pixmaps_path = get_pixmaps_dir()
  150. # Get stored save paths file
  151. self._stored_paths = os.path.join(get_config_path(), "spaths")
  152. # Get video formats
  153. self._video_formats = read_formats()
  154. # Set the app icon
  155. app_icon_path = get_icon_file()
  156. if app_icon_path is not None:
  157. self.app_icon = wx.Icon(app_icon_path, wx.BITMAP_TYPE_PNG)
  158. self.SetIcon(self.app_icon)
  159. # Set the data for all the wx.Button items
  160. # name, label, icon, size, event_handler
  161. buttons_data = (
  162. ("delete", self.DELETE_LABEL, "delete_32px.png", (55, 55), self._on_delete),
  163. ("play", self.PLAY_LABEL, "camera_32px.png", (55, 55), self._on_play),
  164. ("up", self.UP_LABEL, "arrow_up_32px.png", (55, 55), self._on_arrow_up),
  165. ("down", self.DOWN_LABEL, "arrow_down_32px.png", (55, 55), self._on_arrow_down),
  166. ("reload", self.RELOAD_LABEL, "reload_32px.png", (55, 55), self._on_reload),
  167. ("pause", self.PAUSE_LABEL, "pause_32px.png", (55, 55), self._on_pause),
  168. ("start", self.START_LABEL, "cloud_download_32px.png", (55, 55), self._on_start),
  169. ("savepath", "...", None, (40, 27), self._on_savepath),
  170. ("add", self.ADD_LABEL, None, (-1, -1), self._on_add)
  171. )
  172. # Set the data for the settings menu item
  173. # label, event_handler
  174. menu_data = (
  175. (self.OPTIONS_LABEL, self._on_options),
  176. (self.UPDATE_LABEL, self._on_update),
  177. (self.VIEWLOG_LABEL, self._on_viewlog),
  178. (self.ABOUT_LABEL, self._on_about)
  179. )
  180. # Create options frame
  181. self._options_frame = OptionsFrame(self)
  182. # Create frame components
  183. self._panel = wx.Panel(self)
  184. self._url_text = self._create_statictext(self.URLS_LABEL)
  185. self._settings_button = self._create_bitmap_button("settings_20px.png", (30, 30), self._on_settings)
  186. self._url_list = self._create_textctrl(wx.TE_MULTILINE | wx.TE_DONTWRAP, self._on_urllist_edit)
  187. self._folder_icon = self._create_static_bitmap("folder_32px.png")
  188. self._path_combobox = ExtComboBox(self._panel, 5, style=wx.CB_READONLY)
  189. self._videoformat_combobox = ExtComboBox(self._panel, choices=self._video_formats.values(), style=wx.CB_READONLY)
  190. self._download_text = self._create_statictext(self.DOWNLOAD_LIST_LABEL)
  191. self._status_list = ListCtrl(self.STATUSLIST_COLUMNS,
  192. parent=self._panel,
  193. style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
  194. # Dictionary to store all the buttons
  195. self._buttons = {}
  196. for item in buttons_data:
  197. name, label, icon, size, evt_handler = item
  198. button = wx.Button(self._panel, label=label, size=size)
  199. if icon is not None:
  200. button.SetBitmap(wx.Bitmap(os.path.join(self._pixmaps_path, icon)), wx.TOP)
  201. if evt_handler is not None:
  202. button.Bind(wx.EVT_BUTTON, evt_handler)
  203. self._buttons[name] = button
  204. self._status_bar = self.CreateStatusBar()
  205. # Create extra components
  206. self._settings_menu = wx.Menu()
  207. for item in menu_data:
  208. label, evt_handler = item
  209. menu_item = self._settings_menu.Append(-1, label)
  210. self.Bind(wx.EVT_MENU, evt_handler, menu_item)
  211. # Overwrite the hover event to avoid changing the statusbar
  212. self._settings_menu.Bind(wx.EVT_MENU_HIGHLIGHT, lambda event: None)
  213. # Bind extra events
  214. self.Bind(wx.EVT_TEXT, self._update_videoformat, self._videoformat_combobox)
  215. self.Bind(wx.EVT_TEXT, self._update_savepath, self._path_combobox)
  216. self.Bind(wx.EVT_CLOSE, self._on_close)
  217. # Set threads wxCallAfter handlers
  218. self._set_publisher(self._update_handler, UPDATE_PUB_TOPIC)
  219. self._set_publisher(self._download_worker_handler, WORKER_PUB_TOPIC)
  220. self._set_publisher(self._download_manager_handler, MANAGER_PUB_TOPIC)
  221. # Set up extra stuff
  222. self.Center()
  223. self.SetMinSize(self.FRAMES_MIN_SIZE)
  224. self._videoformat_combobox.SetMaxSize((210 ,-1))
  225. self._set_buttons_width()
  226. self._status_bar_write(self.WELCOME_MSG)
  227. self._videoformat_combobox.SetValue(self._video_formats[self.opt_manager.options["video_format"]])
  228. self._path_combobox.LoadMultiple(json_load(self._stored_paths))
  229. self._path_combobox.SetValue(self.opt_manager.options["save_path"])
  230. self._set_layout()
  231. def _update_videoformat(self, event):
  232. self.opt_manager.options["video_format"] = self._video_formats[self._videoformat_combobox.GetValue()]
  233. def _update_savepath(self, event):
  234. self.opt_manager.options["save_path"] = self._path_combobox.GetValue()
  235. def _on_delete(self, event):
  236. raise Exception("Implement me!")
  237. def _on_play(self, event):
  238. raise Exception("Implement me!")
  239. def _on_arrow_up(self, event):
  240. raise Exception("Implement me!")
  241. def _on_arrow_down(self, event):
  242. raise Exception("Implement me!")
  243. def _on_reload(self, event):
  244. raise Exception("Implement me!")
  245. def _on_pause(self, event):
  246. raise Exception("Implement me!")
  247. def _on_start(self, event):
  248. raise Exception("Implement me!")
  249. def _on_savepath(self, event):
  250. dlg = wx.DirDialog(self, self.CHOOSE_DIRECTORY, self._path_combobox.GetStringSelection(), wx.DD_CHANGE_DIR)
  251. if dlg.ShowModal() == wx.ID_OK:
  252. path = dlg.GetPath()
  253. self._path_combobox.Append(path)
  254. self._path_combobox.SetValue(path)
  255. self._update_savepath(None)
  256. dlg.Destroy()
  257. def _on_add(self, event):
  258. options = self._options_parser.parse(self.opt_manager.options)
  259. urls = self._get_urls()
  260. self._url_list.SetValue("")
  261. # Try to get the extension
  262. extension = self._videoformat_combobox.GetValue().split()[0]
  263. if extension == "default":
  264. extension = "-"
  265. else:
  266. extension = "." + extension
  267. for url in urls:
  268. # TODO validate url? youtube-dl can support keywords
  269. download_item = DownloadItem(url, options, extension=extension)
  270. if download_item.object_id not in self._download_items:
  271. download_item.progress_stats["status"] = "Queued"
  272. self._download_items[download_item.object_id] = download_item
  273. self._status_list.bind_item(download_item)
  274. if not urls:
  275. self._create_popup(self.PROVIDE_URL_MSG,
  276. self.ERROR_LABEL,
  277. wx.OK | wx.ICON_EXCLAMATION)
  278. # TODO Call download manager here
  279. def _on_settings(self, event):
  280. event_object_pos = event.EventObject.GetPosition()
  281. # Update the object +30 on Y axis to make the menu look like it belongs to the button
  282. event_object_pos = (event_object_pos[0], event_object_pos[1] + 30)
  283. self.PopupMenu(self._settings_menu, event_object_pos)
  284. def _on_viewlog(self, event):
  285. log_window = LogGUI(self)
  286. log_window.load(self.log_manager.log_file)
  287. log_window.Show()
  288. def _on_about(self, event):
  289. info = wx.AboutDialogInfo()
  290. if self.app_icon is not None:
  291. info.SetIcon(self.app_icon)
  292. info.SetName(__appname__)
  293. info.SetVersion(__version__)
  294. info.SetDescription(__descriptionfull__)
  295. info.SetWebSite(__projecturl__)
  296. info.SetLicense(__licensefull__)
  297. info.AddDeveloper(__author__)
  298. wx.AboutBox(info)
  299. def _set_publisher(self, handler, topic):
  300. """Sets a handler for the given topic.
  301. Args:
  302. handler (function): Can be any function with one parameter
  303. the message that the caller sends.
  304. topic (string): Can be any string that identifies the caller.
  305. You can bind multiple handlers on the same topic or
  306. multiple topics on the same handler.
  307. """
  308. Publisher.subscribe(handler, topic)
  309. def _set_buttons_width(self):
  310. """Re-adjust buttons size on runtime so that all buttons
  311. look the same. """
  312. widths = [
  313. #self._download_btn.GetSize()[0],
  314. #self._update_btn.GetSize()[0],
  315. #self._options_btn.GetSize()[0],
  316. ]
  317. max_width = -1
  318. for item in widths:
  319. if item > max_width:
  320. max_width = item
  321. #self._download_btn.SetMinSize((max_width, self.BUTTONS_SIZE[1]))
  322. #self._update_btn.SetMinSize((max_width, self.BUTTONS_SIZE[1]))
  323. #self._options_btn.SetMinSize((max_width, self.BUTTONS_SIZE[1]))
  324. self._panel.Layout()
  325. def _create_statictext(self, label):
  326. return wx.StaticText(self._panel, label=label)
  327. def _create_bitmap_button(self, icon, size=(-1, -1), handler=None):
  328. bitmap = wx.Bitmap(os.path.join(self._pixmaps_path, icon))
  329. button = wx.BitmapButton(self._panel, bitmap=bitmap, size=size, style=wx.NO_BORDER)
  330. if handler is not None:
  331. button.Bind(wx.EVT_BUTTON, handler)
  332. return button
  333. def _create_static_bitmap(self, icon):
  334. bitmap = wx.Bitmap(os.path.join(self._pixmaps_path, icon))
  335. return wx.StaticBitmap(self._panel, bitmap=bitmap)
  336. def _create_textctrl(self, style=None, event_handler=None):
  337. if style is None:
  338. textctrl = wx.TextCtrl(self._panel)
  339. else:
  340. textctrl = wx.TextCtrl(self._panel, style=style)
  341. if event_handler is not None:
  342. textctrl.Bind(wx.EVT_TEXT_PASTE, event_handler)
  343. textctrl.Bind(wx.EVT_MIDDLE_DOWN, event_handler)
  344. if os.name == 'nt':
  345. # Enable CTRL+A on Windows
  346. def win_ctrla_eventhandler(event):
  347. if event.GetKeyCode() == wx.WXK_CONTROL_A:
  348. event.GetEventObject().SelectAll()
  349. event.Skip()
  350. textctrl.Bind(wx.EVT_CHAR, win_ctrla_eventhandler)
  351. return textctrl
  352. def _create_button(self, label, event_handler=None):
  353. # TODO remove not used anymore
  354. btn = wx.Button(self._panel, label=label, size=self.BUTTONS_SIZE)
  355. if event_handler is not None:
  356. btn.Bind(wx.EVT_BUTTON, event_handler)
  357. return btn
  358. def _create_popup(self, text, title, style):
  359. wx.MessageBox(text, title, style)
  360. def _set_layout(self):
  361. """Sets the layout of the main window. """
  362. main_sizer = wx.BoxSizer(wx.VERTICAL)
  363. panel_sizer = wx.BoxSizer(wx.VERTICAL)
  364. top_sizer = wx.BoxSizer(wx.HORIZONTAL)
  365. top_sizer.Add(self._url_text, 0, wx.ALIGN_BOTTOM | wx.BOTTOM, 5)
  366. top_sizer.AddSpacer((100, 20), 1)
  367. top_sizer.Add(self._settings_button, flag=wx.ALIGN_RIGHT)
  368. panel_sizer.Add(top_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
  369. panel_sizer.Add(self._url_list, 1, wx.EXPAND)
  370. mid_sizer = wx.BoxSizer(wx.HORIZONTAL)
  371. mid_sizer.Add(self._folder_icon)
  372. mid_sizer.Add(self._path_combobox, 2, wx.ALIGN_CENTER_VERTICAL)
  373. mid_sizer.Add(self._buttons["savepath"], flag=wx.ALIGN_CENTER_VERTICAL)
  374. mid_sizer.AddSpacer((10, 20), 1)
  375. mid_sizer.Add(self._videoformat_combobox, 1, wx.ALIGN_CENTER_VERTICAL)
  376. mid_sizer.Add(self._buttons["add"], flag=wx.ALIGN_CENTER_VERTICAL)
  377. panel_sizer.Add(mid_sizer, 0, wx.EXPAND | wx.ALL, 10)
  378. panel_sizer.Add(self._download_text, 0, wx.BOTTOM | wx.LEFT, 5)
  379. panel_sizer.Add(self._status_list, 2, wx.EXPAND)
  380. bottom_sizer = wx.BoxSizer(wx.HORIZONTAL)
  381. bottom_sizer.Add(self._buttons["delete"])
  382. bottom_sizer.Add(self._buttons["play"])
  383. bottom_sizer.Add(self._buttons["up"])
  384. bottom_sizer.Add(self._buttons["down"])
  385. bottom_sizer.Add(self._buttons["reload"])
  386. bottom_sizer.Add(self._buttons["pause"])
  387. bottom_sizer.AddSpacer((100, 20), 1)
  388. bottom_sizer.Add(self._buttons["start"])
  389. panel_sizer.Add(bottom_sizer, 0, wx.EXPAND | wx.TOP, 10)
  390. self._panel.SetSizer(panel_sizer)
  391. main_sizer.Add(self._panel, 1, wx.ALL | wx.EXPAND, 10)
  392. self.SetSizer(main_sizer)
  393. def _update_youtubedl(self):
  394. """Update youtube-dl binary to the latest version. """
  395. #self._update_btn.Disable()
  396. #self._download_btn.Disable()
  397. if self.download_manager is not None and self.download_manager.is_alive():
  398. self._create_popup(self.DOWNLOAD_ACTIVE,
  399. self.ERROR_LABEL,
  400. wx.OK | wx.ICON_EXCLAMATION)
  401. elif self.update_thread is not None and self.update_thread.is_alive():
  402. self._create_popup(self.UPDATE_ACTIVE,
  403. self.INFO_LABEL,
  404. wx.OK | wx.ICON_INFORMATION)
  405. else:
  406. self.update_thread = UpdateThread(self.opt_manager.options['youtubedl_path'])
  407. def _status_bar_write(self, msg):
  408. """Display msg in the status bar. """
  409. self._status_bar.SetStatusText(msg)
  410. def _reset_widgets(self):
  411. """Resets GUI widgets after update or download process. """
  412. pass
  413. #self._download_btn.SetLabel(self.DOWNLOAD_LABEL)
  414. #self._download_btn.Enable()
  415. #self._update_btn.Enable()
  416. def _print_stats(self):
  417. """Display download stats in the status bar. """
  418. suc_downloads = self.download_manager.successful
  419. dtime = get_time(self.download_manager.time_it_took)
  420. msg = self.SUCC_REPORT_MSG.format(suc_downloads,
  421. dtime['days'],
  422. dtime['hours'],
  423. dtime['minutes'],
  424. dtime['seconds'])
  425. self._status_bar_write(msg)
  426. def _after_download(self):
  427. """Run tasks after download process has been completed.
  428. Note:
  429. Here you can add any tasks you want to run after the
  430. download process has been completed.
  431. """
  432. if self.opt_manager.options['shutdown']:
  433. self.opt_manager.save_to_file()
  434. success = shutdown_sys(self.opt_manager.options['sudo_password'])
  435. if success:
  436. self._status_bar_write(self.SHUTDOWN_MSG)
  437. else:
  438. self._status_bar_write(self.SHUTDOWN_ERR)
  439. else:
  440. self._create_popup(self.DL_COMPLETED_MSG, self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION)
  441. if self.opt_manager.options['open_dl_dir']:
  442. success = open_dir(self.opt_manager.options['save_path'])
  443. if not success:
  444. self._status_bar_write(self.OPEN_DIR_ERR.format(dir=self.opt_manager.options['save_path']))
  445. def _download_worker_handler(self, msg):
  446. """downloadmanager.Worker thread handler.
  447. Handles messages from the Worker thread.
  448. Args:
  449. See downloadmanager.Worker _talk_to_gui() method.
  450. """
  451. signal, data = msg.data
  452. if signal == 'send':
  453. self._status_list.write(data)
  454. if signal == 'receive':
  455. self.download_manager.send_to_worker(self._status_list.get(data))
  456. def _download_manager_handler(self, msg):
  457. """downloadmanager.DownloadManager thread handler.
  458. Handles messages from the DownloadManager thread.
  459. Args:
  460. See downloadmanager.DownloadManager _talk_to_gui() method.
  461. """
  462. data = msg.data
  463. if data == 'finished':
  464. self._print_stats()
  465. self._reset_widgets()
  466. self.download_manager = None
  467. self._after_download()
  468. elif data == 'closed':
  469. self._status_bar_write(self.CLOSED_MSG)
  470. self._reset_widgets()
  471. self.download_manager = None
  472. elif data == 'closing':
  473. self._status_bar_write(self.CLOSING_MSG)
  474. elif data == 'report_active':
  475. # Report number of urls been downloaded
  476. msg = self.URL_REPORT_MSG.format(self.download_manager.active())
  477. self._status_bar_write(msg)
  478. def _update_handler(self, msg):
  479. """updatemanager.UpdateThread thread handler.
  480. Handles messages from the UpdateThread thread.
  481. Args:
  482. See updatemanager.UpdateThread _talk_to_gui() method.
  483. """
  484. data = msg.data
  485. if data[0] == 'download':
  486. self._status_bar_write(self.UPDATING_MSG)
  487. elif data[0] == 'error':
  488. self._status_bar_write(self.UPDATE_ERR_MSG.format(data[1]))
  489. elif data[0] == 'correct':
  490. self._status_bar_write(self.UPDATE_SUCC_MSG)
  491. else:
  492. self._reset_widgets()
  493. self.update_thread = None
  494. def _get_urls(self):
  495. """Returns urls list. """
  496. return [line for line in self._url_list.GetValue().split('\n') if line]
  497. def _start_download(self):
  498. """Handles pre-download tasks & starts the download process. """
  499. self._status_list.clear()
  500. self._status_list.load_urls(self._get_urls())
  501. if self._status_list.is_empty():
  502. self._create_popup(self.PROVIDE_URL_MSG,
  503. self.ERROR_LABEL,
  504. wx.OK | wx.ICON_EXCLAMATION)
  505. else:
  506. self.download_manager = DownloadManager(self._status_list.get_items(),
  507. self.opt_manager,
  508. self.log_manager)
  509. self._status_bar_write(self.DOWNLOAD_STARTED)
  510. #self._download_btn.SetLabel(self.STOP_LABEL)
  511. #self._update_btn.Disable()
  512. def _paste_from_clipboard(self):
  513. """Paste the content of the clipboard to the self._url_list widget.
  514. It also adds a new line at the end of the data if not exist.
  515. """
  516. if not wx.TheClipboard.IsOpened():
  517. if wx.TheClipboard.Open():
  518. if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)):
  519. data = wx.TextDataObject()
  520. wx.TheClipboard.GetData(data)
  521. data = data.GetText()
  522. if data[-1] != '\n':
  523. data += '\n'
  524. self._url_list.WriteText(data)
  525. wx.TheClipboard.Close()
  526. def _on_urllist_edit(self, event):
  527. """Event handler of the self._url_list widget.
  528. This method is triggered when the users pastes text into
  529. the URLs list either by using CTRL+V or by using the middle
  530. click of the mouse.
  531. """
  532. #TODO Remove not used anymore
  533. if event.ClassName == self.wxEVT_TEXT_PASTE:
  534. self._paste_from_clipboard()
  535. else:
  536. wx.TheClipboard.UsePrimarySelection(True)
  537. self._paste_from_clipboard()
  538. wx.TheClipboard.UsePrimarySelection(False)
  539. # Dynamically add urls after download process has started
  540. if self.download_manager is not None:
  541. self._status_list.load_urls(self._get_urls(), self.download_manager.add_url)
  542. def _on_download(self, event):
  543. """Event handler of the self._download_btn widget.
  544. This method is used when the download-stop button is pressed to
  545. start or stop the download process.
  546. """
  547. if self.download_manager is None:
  548. self._start_download()
  549. else:
  550. self.download_manager.stop_downloads()
  551. def _on_update(self, event):
  552. """Event handler of the self._update_btn widget.
  553. This method is used when the update button is pressed to start
  554. the update process.
  555. Note:
  556. Currently there is not way to stop the update process.
  557. """
  558. self._update_youtubedl()
  559. def _on_options(self, event):
  560. """Event handler of the self._options_btn widget.
  561. This method is used when the options button is pressed to show
  562. the options window.
  563. """
  564. print "Have to adjust the options window first!!"
  565. #self._options_frame.load_all_options()
  566. #self._options_frame.Show()
  567. def _on_close(self, event):
  568. """Event handler for the wx.EVT_CLOSE event.
  569. This method is used when the user tries to close the program
  570. to save the options and make sure that the download & update
  571. processes are not running.
  572. """
  573. if self.download_manager is not None:
  574. self.download_manager.stop_downloads()
  575. self.download_manager.join()
  576. if self.update_thread is not None:
  577. self.update_thread.join()
  578. # Store main-options frame size
  579. self.opt_manager.options['main_win_size'] = self.GetSize()
  580. self.opt_manager.options['opts_win_size'] = self._options_frame.GetSize()
  581. #TODO re-enable after options frame update
  582. #self._options_frame.save_all_options()
  583. self.opt_manager.save_to_file()
  584. json_store(self._stored_paths, self._path_combobox.GetStrings())
  585. self.Destroy()
  586. class ListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin):
  587. """Custom ListCtrl widget.
  588. Args:
  589. columns (dict): See MainFrame class STATUSLIST_COLUMNS attribute.
  590. """
  591. def __init__(self, columns, *args, **kwargs):
  592. wx.ListCtrl.__init__(self, *args, **kwargs)
  593. ListCtrlAutoWidthMixin.__init__(self)
  594. self.columns = columns
  595. self._list_index = 0
  596. self._url_list = set()
  597. self._set_columns()
  598. def get(self, data):
  599. """Return data from ListCtrl.
  600. Args:
  601. data (dict): Dictionary which contains three keys. The 'index'
  602. that identifies the current row, the 'source' which identifies
  603. a column in the wxListCtrl and the 'dest' which tells
  604. wxListCtrl under which key to store the retrieved value. For
  605. more informations see the _talk_to_gui() method under
  606. downloadmanager.py Worker class.
  607. Returns:
  608. A dictionary which holds the 'index' (row) and the data from the
  609. given row-column combination.
  610. Example:
  611. args: data = {'index': 0, 'source': 'filename', 'dest': 'new_filename'}
  612. The wxListCtrl will store the value from the 'filename' column
  613. into a new dictionary with a key value 'new_filename'.
  614. return: {'index': 0, 'new_filename': 'The filename retrieved'}
  615. """
  616. value = None
  617. # If the source column exists
  618. if data['source'] in self.columns:
  619. value = self.GetItemText(data['index'], self.columns[data['source']][0])
  620. return {'index': data['index'], data['dest']: value}
  621. def write(self, data):
  622. """Write data on ListCtrl row-column.
  623. Args:
  624. data (dict): Dictionary that contains the data to be
  625. written on the ListCtrl. In order for this method to
  626. write the given data there must be an 'index' key that
  627. identifies the current row. For a valid data dictionary see
  628. Worker class __init__() method under downloadmanager.py module.
  629. """
  630. for key in data:
  631. if key in self.columns:
  632. self._write_data(data['index'], self.columns[key][0], data[key])
  633. def load_urls(self, url_list, func=None):
  634. """Load URLs from the url_list on the ListCtrl widget.
  635. Args:
  636. url_list (list): List of strings that contains the URLs to add.
  637. func (function): Callback function. It's used to add the URLs
  638. on the download_manager.
  639. """
  640. for url in url_list:
  641. url = url.replace(' ', '')
  642. if url and not self.has_url(url):
  643. self.add_url(url)
  644. if func is not None:
  645. # Custom hack to add url into download_manager
  646. item = self._get_item(self._list_index - 1)
  647. func(item)
  648. def has_url(self, url):
  649. """Returns True if the url is aleady in the ListCtrl else False.
  650. Args:
  651. url (string): URL string.
  652. """
  653. return url in self._url_list
  654. def bind_item(self, download_item):
  655. #TODO remove line below
  656. print self._list_index, download_item.object_id
  657. self.InsertStringItem(self._list_index, download_item.url)
  658. self.SetItemData(self._list_index, download_item.object_id)
  659. self._update_from_item(self._list_index, download_item)
  660. self._list_index += 1
  661. def _update_from_item(self, row, download_item):
  662. for key in download_item.progress_stats:
  663. column = self.columns[key][0]
  664. #TODO remove line below
  665. print row, column, download_item.progress_stats[key]
  666. self.SetStringItem(row, column, download_item.progress_stats[key])
  667. def add_url(self, url):
  668. """Adds the given url in the ListCtrl.
  669. Args:
  670. url (string): URL string.
  671. """
  672. self.InsertStringItem(self._list_index, url)
  673. self._url_list.add(url)
  674. self._list_index += 1
  675. def clear(self):
  676. """Clear the ListCtrl widget & reset self._list_index and
  677. self._url_list. """
  678. self.DeleteAllItems()
  679. self._list_index = 0
  680. self._url_list = set()
  681. def is_empty(self):
  682. """Returns True if the list is empty else False. """
  683. return self._list_index == 0
  684. def get_items(self):
  685. """Returns a list of items inside the ListCtrl.
  686. Returns:
  687. List of dictionaries that contains the 'url' and the
  688. 'index'(row) for each item of the ListCtrl.
  689. """
  690. items = []
  691. for row in xrange(self._list_index):
  692. item = self._get_item(row)
  693. items.append(item)
  694. return items
  695. def _write_data(self, row, column, data):
  696. """Write data on row-column. """
  697. if isinstance(data, basestring):
  698. self.SetStringItem(row, column, data)
  699. def _get_item(self, index):
  700. """Returns the corresponding ListCtrl item for the given index.
  701. Args:
  702. index (int): Index that identifies the row of the item.
  703. Index must be smaller than the self._list_index.
  704. Returns:
  705. Dictionary that contains the URL string of the row and the
  706. row number(index).
  707. """
  708. item = self.GetItem(itemId=index, col=0)
  709. data = dict(url=item.GetText(), index=index)
  710. return data
  711. def _set_columns(self):
  712. """Initializes ListCtrl columns.
  713. See MainFrame STATUSLIST_COLUMNS attribute for more info. """
  714. for column_item in sorted(self.columns.values()):
  715. self.InsertColumn(column_item[0], column_item[1], width=wx.LIST_AUTOSIZE_USEHEADER)
  716. # If the column width obtained from wxLIST_AUTOSIZE_USEHEADER
  717. # is smaller than the minimum allowed column width
  718. # then set the column width to the minumum allowed size
  719. if self.GetColumnWidth(column_item[0]) < column_item[2]:
  720. self.SetColumnWidth(column_item[0], column_item[2])
  721. # Set auto-resize if enabled
  722. if column_item[3]:
  723. self.setResizeColumn(column_item[0])
  724. # TODO Extra widgets below should move to other module with widgets
  725. class ExtComboBox(wx.ComboBox):
  726. def __init__(self, parent, max_items=-1, *args, **kwargs):
  727. super(ExtComboBox, self).__init__(parent, *args, **kwargs)
  728. assert max_items > 0 or max_items == -1
  729. self.max_items = max_items
  730. def Append(self, new_value):
  731. if self.FindString(new_value) == wx.NOT_FOUND:
  732. super(ExtComboBox, self).Append(new_value)
  733. if self.max_items != -1 and self.GetCount() > self.max_items:
  734. self.SetItems(self.GetStrings()[1:])
  735. def SetValue(self, new_value):
  736. if self.FindString(new_value) == wx.NOT_FOUND:
  737. self.Append(new_value)
  738. self.SetSelection(self.FindString(new_value))
  739. def LoadMultiple(self, items_list):
  740. for item in items_list:
  741. self.Append(item)
  742. class DownloadItem(object):
  743. def __init__(self, url, options, filename="", extension="", path="", filesize=0.0, playlist_index=-1):
  744. self.url = url
  745. self.path = path
  746. self.options = options
  747. self.filename = filename
  748. self.filesize = filesize
  749. self.extension = extension
  750. self.playlist_index = playlist_index
  751. self.progress_stats = {
  752. "extension": extension,
  753. "filesize": "-",
  754. "percent": "0%",
  755. "status": "-",
  756. "speed": "-",
  757. "eta": "-"
  758. }
  759. self.object_id = hash(url + to_string(options))
  760. def get_absolute_path(self):
  761. return os.path.join(self.path, self.filename, self.extension)
  762. class DownloadPlaylist(object):
  763. # TODO feauture use
  764. def __init__(self, size, items):
  765. self.size = size
  766. self.items = items