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.

426 lines
13 KiB

10 years ago
  1. #!/usr/bin/env python2
  2. ''' Contains code for main app frame & custom ListCtrl. '''
  3. import os.path
  4. import wx
  5. from wx.lib.pubsub import setuparg1
  6. from wx.lib.pubsub import pub as Publisher
  7. from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
  8. from .LogManager import LogManager
  9. from .OptionsFrame import OptionsFrame
  10. from .UpdateThread import UpdateThread
  11. from .OptionsManager import OptionsManager
  12. from .DownloadThread import DownloadManager, DownloadThread
  13. from .utils import (
  14. get_youtubedl_filename,
  15. get_config_path,
  16. get_icon_path,
  17. shutdown_sys,
  18. get_time,
  19. open_dir
  20. )
  21. from .data import (
  22. __author__,
  23. __appname__
  24. )
  25. CONFIG_PATH = os.path.join(get_config_path(), __appname__.lower())
  26. class MainFrame(wx.Frame):
  27. ''' Youtube-dlG main frame. '''
  28. def __init__(self, parent=None):
  29. wx.Frame.__init__(self, parent, -1, __appname__, size=(650, 440))
  30. self.init_gui()
  31. icon = get_icon_path()
  32. if icon is not None:
  33. self.SetIcon(wx.Icon(icon, wx.BITMAP_TYPE_PNG))
  34. Publisher.subscribe(self.update_handler, "update")
  35. Publisher.subscribe(self.download_handler, "dlmanager")
  36. Publisher.subscribe(self.download_handler, "dlthread")
  37. self.opt_manager = OptionsManager(CONFIG_PATH)
  38. self.download_manager = None
  39. self.update_thread = None
  40. self.log_manager = None
  41. if self.opt_manager.options['enable_log']:
  42. self.log_manager = LogManager(
  43. CONFIG_PATH,
  44. self.opt_manager.options['log_time']
  45. )
  46. def init_gui(self):
  47. ''' Initialize youtube-dlG GUI components & sizers. '''
  48. panel = wx.Panel(self)
  49. # Create components
  50. self.url_list = wx.TextCtrl(panel,
  51. size=(-1, 120),
  52. style=wx.TE_MULTILINE | wx.TE_DONTWRAP)
  53. self.download_button = wx.Button(panel, label='Download', size=(90, 30))
  54. self.update_button = wx.Button(panel, label='Update', size=(90, 30))
  55. self.options_button = wx.Button(panel, label='Options', size=(90, 30))
  56. self.status_list = ListCtrl(panel,
  57. style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
  58. self.status_bar = wx.StaticText(panel, label='Author: ' + __author__)
  59. # Set sizers
  60. main_sizer = wx.BoxSizer(wx.VERTICAL)
  61. main_sizer.AddSpacer(10)
  62. # URLs label
  63. main_sizer.Add(wx.StaticText(panel, label='URLs'), flag=wx.LEFT, border=20)
  64. # URLs list
  65. horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL)
  66. horizontal_sizer.Add(self.url_list, 1)
  67. main_sizer.Add(horizontal_sizer, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=20)
  68. main_sizer.AddSpacer(10)
  69. # Buttons
  70. horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL)
  71. horizontal_sizer.Add(self.download_button)
  72. horizontal_sizer.Add(self.update_button, flag=wx.LEFT | wx.RIGHT, border=80)
  73. horizontal_sizer.Add(self.options_button)
  74. main_sizer.Add(horizontal_sizer, flag=wx.ALIGN_CENTER_HORIZONTAL)
  75. main_sizer.AddSpacer(10)
  76. # Status list
  77. horizontal_sizer = wx.BoxSizer(wx.HORIZONTAL)
  78. horizontal_sizer.Add(self.status_list, 1, flag=wx.EXPAND)
  79. main_sizer.Add(horizontal_sizer, 1, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=20)
  80. main_sizer.AddSpacer(5)
  81. # Status bar
  82. main_sizer.Add(self.status_bar, flag=wx.LEFT, border=20)
  83. main_sizer.AddSpacer(5)
  84. panel.SetSizer(main_sizer)
  85. # Bind events
  86. self.Bind(wx.EVT_BUTTON, self.OnDownload, self.download_button)
  87. self.Bind(wx.EVT_BUTTON, self.OnUpdate, self.update_button)
  88. self.Bind(wx.EVT_BUTTON, self.OnOptions, self.options_button)
  89. self.Bind(wx.EVT_TEXT, self.OnListCtrlEdit, self.url_list)
  90. self.Bind(wx.EVT_CLOSE, self.OnClose)
  91. def youtubedl_exist(self):
  92. ''' Return True if youtube-dl executable exists. '''
  93. path = os.path.join(self.opt_manager.options['youtubedl_path'],
  94. get_youtubedl_filename())
  95. return os.path.exists(path)
  96. def update_youtubedl(self, quiet=False):
  97. ''' Update youtube-dl executable. '''
  98. if not quiet:
  99. self.update_button.Disable()
  100. self.download_button.Disable()
  101. self.update_thread = UpdateThread(
  102. self.opt_manager.options['youtubedl_path'],
  103. quiet
  104. )
  105. def status_bar_write(self, msg):
  106. ''' Write msg to self.status_bar. '''
  107. self.status_bar.SetLabel(msg)
  108. def reset(self):
  109. ''' Reset GUI and variables after download process. '''
  110. self.download_button.SetLabel('Download')
  111. self.update_button.Enable()
  112. self.download_manager.join()
  113. self.download_manager = None
  114. def print_stats(self):
  115. ''' Print stats to self.status_bar after downloading. '''
  116. successful_downloads = self.download_manager.successful_downloads
  117. dtime = get_time(self.download_manager.time)
  118. msg = 'Successfully downloaded %s url(s) in ' % successful_downloads
  119. days = int(dtime['days'])
  120. hours = int(dtime['hours'])
  121. minutes = int(dtime['minutes'])
  122. seconds = int(dtime['seconds'])
  123. if days != 0:
  124. msg += '%s days, ' % days
  125. if hours != 0:
  126. msg += '%s hours, ' % hours
  127. if minutes != 0:
  128. msg += '%s minutes, ' % minutes
  129. msg += '%s seconds ' % seconds
  130. self.status_bar_write(msg)
  131. def fin_tasks(self):
  132. ''' Run tasks after download process has finished. '''
  133. if self.opt_manager.options['shutdown']:
  134. self.opt_manager.save_to_file()
  135. shutdown_sys(self.opt_manager.options['sudo_password'])
  136. else:
  137. self.create_popup('Downloads completed', 'Info', wx.OK | wx.ICON_INFORMATION)
  138. if self.opt_manager.options['open_dl_dir']:
  139. open_dir(self.opt_manager.options['save_path'])
  140. def download_handler(self, msg):
  141. ''' Handle messages from DownloadManager & DownloadThread. '''
  142. topic = msg.topic[0]
  143. data = msg.data
  144. if topic == 'dlthread':
  145. self.status_list.write(data)
  146. msg = 'Downloading %s url(s)' % self.download_manager.not_finished()
  147. self.status_bar_write(msg)
  148. if topic == 'dlmanager':
  149. if data == 'closing':
  150. self.status_bar_write('Stopping downloads')
  151. if data == 'closed':
  152. self.status_bar_write('Downloads stopped')
  153. self.reset()
  154. if data == 'finished':
  155. self.print_stats()
  156. self.reset()
  157. self.fin_tasks()
  158. def update_handler(self, msg):
  159. ''' Handle messages from UpdateThread. '''
  160. if msg.data == 'finish':
  161. self.download_button.Enable()
  162. self.update_button.Enable()
  163. self.update_thread.join()
  164. self.update_thread = None
  165. else:
  166. self.status_bar_write(msg.data)
  167. def load_on_list(self, url):
  168. ''' Load url on ListCtrl. Return True if url
  169. loaded successfully, else return False.
  170. '''
  171. url = url.replace(' ', '')
  172. if url != '' and not self.status_list.has_url(url):
  173. self.status_list.add_url(url)
  174. return True
  175. return False
  176. def start_download(self):
  177. ''' Handle pre-download tasks & start download process. '''
  178. self.status_list.clear()
  179. if not self.youtubedl_exist():
  180. self.update_youtubedl(True)
  181. for url in self.url_list.GetValue().split('\n'):
  182. self.load_on_list(url)
  183. if self.status_list.is_empty():
  184. self.create_popup(
  185. 'You need to provide at least one url',
  186. 'Error',
  187. wx.OK | wx.ICON_EXCLAMATION
  188. )
  189. else:
  190. threads_list = []
  191. for item in self.status_list.get_items():
  192. threads_list.append(self.create_thread(item))
  193. self.download_manager = DownloadManager(threads_list, self.update_thread)
  194. self.status_bar_write('Download started')
  195. self.download_button.SetLabel('Stop')
  196. self.update_button.Disable()
  197. def create_thread(self, item):
  198. ''' Return DownloadThread created from item. '''
  199. return DownloadThread(item['url'],
  200. item['index'],
  201. self.opt_manager,
  202. self.log_manager)
  203. def create_popup(self, text, title, style):
  204. ''' Create popup message. '''
  205. wx.MessageBox(text, title, style)
  206. def OnListCtrlEdit(self, event):
  207. ''' Dynamically add url for download.'''
  208. if self.download_manager is not None:
  209. for url in self.url_list.GetValue().split('\n'):
  210. # If url successfully loaded on list
  211. if self.load_on_list(url):
  212. thread = self.create_thread(self.status_list.get_last_item())
  213. self.download_manager.add_thread(thread)
  214. def OnDownload(self, event):
  215. ''' Event handler method for self.download_button. '''
  216. if self.download_manager is None:
  217. self.start_download()
  218. else:
  219. self.download_manager.close()
  220. def OnUpdate(self, event):
  221. ''' Event handler method for self.update_button. '''
  222. self.update_youtubedl()
  223. def OnOptions(self, event):
  224. ''' Event handler method for self.options_button. '''
  225. options_frame = OptionsFrame(
  226. self.opt_manager,
  227. parent=self,
  228. logger=self.log_manager
  229. )
  230. options_frame.Show()
  231. def OnClose(self, event):
  232. ''' Event handler method (wx.EVT_CLOSE). '''
  233. if self.download_manager is not None:
  234. self.download_manager.close()
  235. self.download_manager.join()
  236. if self.update_thread is not None:
  237. self.update_thread.join()
  238. self.opt_manager.save_to_file()
  239. self.Destroy()
  240. class ListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin):
  241. '''
  242. Custom ListCtrl class.
  243. Accessible Methods
  244. write()
  245. Params: Python dictionary that contains data to write
  246. Return: None
  247. has_url()
  248. Params: Url to search
  249. Return: True if url in ListCtrl, else False
  250. add_url()
  251. Params: Url to add
  252. Return: None
  253. clear()
  254. Params: None
  255. Return: None
  256. is_empty()
  257. Params: None
  258. Return: True if ListCtrl is empty, else False
  259. get_items()
  260. Params: None
  261. Return: Python list that contains all ListCtrl items
  262. get_last_item()
  263. Params: None
  264. Return: Last item inserted in ListCtrl
  265. '''
  266. # Hold column for each data
  267. DATA_COLUMNS = {
  268. 'filename': 0,
  269. 'filesize': 1,
  270. 'percent': 2,
  271. 'status': 5,
  272. 'speed': 4,
  273. 'eta': 3
  274. }
  275. def __init__(self, parent=None, style=0):
  276. wx.ListCtrl.__init__(self, parent, -1, wx.DefaultPosition, wx.DefaultSize, style)
  277. ListCtrlAutoWidthMixin.__init__(self)
  278. self.InsertColumn(0, 'Video', width=150)
  279. self.InsertColumn(1, 'Size', width=80)
  280. self.InsertColumn(2, 'Percent', width=65)
  281. self.InsertColumn(3, 'ETA', width=45)
  282. self.InsertColumn(4, 'Speed', width=90)
  283. self.InsertColumn(5, 'Status', width=160)
  284. self.setResizeColumn(0)
  285. self._list_index = 0
  286. self._url_list = []
  287. def write(self, data):
  288. ''' Write data on ListCtrl row-column. '''
  289. for key in data:
  290. if key in self.DATA_COLUMNS:
  291. self._write_data(data[key], data['index'], self.DATA_COLUMNS[key])
  292. def has_url(self, url):
  293. ''' Return True if url in ListCtrl, else return False. '''
  294. return url in self._url_list
  295. def add_url(self, url):
  296. ''' Add url on ListCtrl. '''
  297. self.InsertStringItem(self._list_index, url)
  298. self._url_list.append(url)
  299. self._list_index += 1
  300. def clear(self):
  301. ''' Clear ListCtrl & reset self._list_index. '''
  302. self.DeleteAllItems()
  303. self._list_index = 0
  304. self._url_list = []
  305. def is_empty(self):
  306. ''' Return True if list is empty. '''
  307. return self._list_index == 0
  308. def get_items(self):
  309. ''' Return list of items in ListCtrl. '''
  310. items = []
  311. for row in range(self._list_index):
  312. item = self._get_item(row)
  313. items.append(item)
  314. return items
  315. def get_last_item(self):
  316. ''' Return last item of ListCtrl '''
  317. return self._get_item(self._list_index - 1)
  318. def _write_data(self, data, row, column):
  319. ''' Write data on row, column. '''
  320. if isinstance(data, basestring):
  321. self.SetStringItem(row, column, data)
  322. def _get_item(self, index):
  323. ''' Return single item base on index. '''
  324. data = {}
  325. item = self.GetItem(itemId=index, col=0)
  326. data['url'] = item.GetText()
  327. data['index'] = index
  328. return data