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.

440 lines
14 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
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. """Youtubedlg module for managing the download process.
  4. This module is responsible for managing the download process
  5. and update the GUI interface.
  6. Attributes:
  7. MANAGER_PUB_TOPIC (string): wxPublisher subscription topic of the
  8. DownloadManager thread.
  9. WORKER_PUB_TOPIC (string): wxPublisher subscription topic of the
  10. Worker thread.
  11. Note:
  12. It's not the actual module that downloads the urls
  13. thats the job of the 'downloaders' module.
  14. """
  15. from __future__ import unicode_literals
  16. import time
  17. import os.path
  18. from threading import (
  19. Thread,
  20. Lock
  21. )
  22. from wx import CallAfter
  23. from wx.lib.pubsub import setuparg1
  24. from wx.lib.pubsub import pub as Publisher
  25. from .parsers import OptionsParser
  26. from .updatemanager import UpdateThread
  27. from .downloaders import YoutubeDLDownloader
  28. from .utils import YOUTUBEDL_BIN
  29. MANAGER_PUB_TOPIC = 'dlmanager'
  30. WORKER_PUB_TOPIC = 'dlworker'
  31. class DownloadManager(Thread):
  32. """Manages the download process.
  33. Attributes:
  34. WAIT_TIME (float): Time in seconds to sleep.
  35. Args:
  36. urls_list (list): Python list that contains multiple dictionaries
  37. with the url to download and the corresponding row(index) in
  38. which the worker should send the download process information.
  39. opt_manager (optionsmanager.OptionsManager): Object responsible for
  40. managing the youtubedlg options.
  41. log_manager (logmanager.LogManager): Object responsible for writing
  42. errors to the log.
  43. """
  44. WAIT_TIME = 0.1
  45. def __init__(self, urls_list, opt_manager, log_manager=None):
  46. super(DownloadManager, self).__init__()
  47. self.opt_manager = opt_manager
  48. self.log_manager = log_manager
  49. self.urls_list = urls_list
  50. self._time_it_took = 0
  51. self._successful = 0
  52. self._running = True
  53. # Init the custom workers thread pool
  54. log_lock = None if log_manager is None else Lock()
  55. wparams = (opt_manager, self._youtubedl_path(), log_manager, log_lock)
  56. self._workers = [Worker(*wparams) for i in xrange(opt_manager.options['workers_number'])]
  57. self.start()
  58. @property
  59. def successful(self):
  60. """Returns number of successful downloads. """
  61. return self._successful
  62. @property
  63. def time_it_took(self):
  64. """Returns time(seconds) it took for the download process
  65. to complete. """
  66. return self._time_it_took
  67. def run(self):
  68. self._check_youtubedl()
  69. self._time_it_took = time.time()
  70. while self._running:
  71. for worker in self._workers:
  72. if worker.available() and self.urls_list:
  73. worker.download(self.urls_list.pop(0))
  74. time.sleep(self.WAIT_TIME)
  75. if not self.urls_list and self._jobs_done():
  76. break
  77. self._talk_to_gui('report_active')
  78. # Close all the workers
  79. for worker in self._workers:
  80. worker.close()
  81. # Join and collect
  82. for worker in self._workers:
  83. worker.join()
  84. self._successful += worker.successful
  85. self._time_it_took = time.time() - self._time_it_took
  86. if not self._running:
  87. self._talk_to_gui('closed')
  88. else:
  89. self._talk_to_gui('finished')
  90. def active(self):
  91. """Returns number of active items.
  92. Note:
  93. active_items = (workers that work) + (items waiting in the url_list).
  94. """
  95. counter = 0
  96. for worker in self._workers:
  97. if not worker.available():
  98. counter += 1
  99. counter += len(self.urls_list)
  100. return counter
  101. def stop_downloads(self):
  102. """Stop the download process. Also send 'closing'
  103. signal back to the GUI.
  104. Note:
  105. It does NOT kill the workers thats the job of the
  106. clean up task in the run() method.
  107. """
  108. self._talk_to_gui('closing')
  109. self._running = False
  110. def add_url(self, url):
  111. """Add given url to the urls_list.
  112. Args:
  113. url (dict): Python dictionary that contains two keys.
  114. The url and the index of the corresponding row in which
  115. the worker should send back the information about the
  116. download process.
  117. """
  118. self.urls_list.append(url)
  119. def send_to_worker(self, data):
  120. """Send data to the Workers.
  121. Args:
  122. data (dict): Python dictionary that holds the 'index'
  123. which is used to identify the Worker thread and the data which
  124. can be any of the Worker's class valid data. For a list of valid
  125. data keys see __init__() under the Worker class.
  126. """
  127. if 'index' in data:
  128. for worker in self._workers:
  129. if worker.has_index(data['index']):
  130. worker.update_data(data)
  131. def _talk_to_gui(self, data):
  132. """Send data back to the GUI using wxCallAfter and wxPublisher.
  133. Args:
  134. data (string): Unique signal string that informs the GUI for the
  135. download process.
  136. Note:
  137. DownloadManager supports 4 signals.
  138. 1) closing: The download process is closing.
  139. 2) closed: The download process has closed.
  140. 3) finished: The download process was completed normally.
  141. 4) report_active: Signal the gui to read the number of active
  142. downloads using the active() method.
  143. """
  144. CallAfter(Publisher.sendMessage, MANAGER_PUB_TOPIC, data)
  145. def _check_youtubedl(self):
  146. """Check if youtube-dl binary exists. If not try to download it. """
  147. if not os.path.exists(self._youtubedl_path()):
  148. UpdateThread(self.opt_manager.options['youtubedl_path'], True).join()
  149. def _jobs_done(self):
  150. """Returns True if the workers have finished their jobs else False. """
  151. for worker in self._workers:
  152. if not worker.available():
  153. return False
  154. return True
  155. def _youtubedl_path(self):
  156. """Returns the path to youtube-dl binary. """
  157. path = self.opt_manager.options['youtubedl_path']
  158. path = os.path.join(path, YOUTUBEDL_BIN)
  159. return path
  160. class Worker(Thread):
  161. """Simple worker which downloads the given url using a downloader
  162. from the downloaders.py module.
  163. Attributes:
  164. WAIT_TIME (float): Time in seconds to sleep.
  165. Args:
  166. opt_manager (optionsmanager.OptionsManager): Check DownloadManager
  167. description.
  168. youtubedl (string): Absolute path to youtube-dl binary.
  169. log_manager (logmanager.LogManager): Check DownloadManager
  170. description.
  171. log_lock (threading.Lock): Synchronization lock for the log_manager.
  172. If the log_manager is set (not None) then the caller has to make
  173. sure that the log_lock is also set.
  174. Note:
  175. For available data keys see self._data under the __init__() method.
  176. """
  177. WAIT_TIME = 0.1
  178. def __init__(self, opt_manager, youtubedl, log_manager=None, log_lock=None):
  179. super(Worker, self).__init__()
  180. self.opt_manager = opt_manager
  181. self.log_manager = log_manager
  182. self.log_lock = log_lock
  183. self._downloader = YoutubeDLDownloader(youtubedl, self._data_hook, self._log_data)
  184. self._options_parser = OptionsParser()
  185. self._successful = 0
  186. self._running = True
  187. self._wait_for_reply = False
  188. self._data = {
  189. 'playlist_index': None,
  190. 'playlist_size': None,
  191. 'new_filename': None,
  192. 'extension': None,
  193. 'filesize': None,
  194. 'filename': None,
  195. 'percent': None,
  196. 'status': None,
  197. 'index': None,
  198. 'speed': None,
  199. 'path': None,
  200. 'eta': None,
  201. 'url': None
  202. }
  203. self.start()
  204. def run(self):
  205. while self._running:
  206. if self._data['url'] is not None:
  207. options = self._options_parser.parse(self.opt_manager.options)
  208. ret_code = self._downloader.download(self._data['url'], options)
  209. if (ret_code == YoutubeDLDownloader.OK or
  210. ret_code == YoutubeDLDownloader.ALREADY):
  211. self._successful += 1
  212. # Ask GUI for name updates
  213. self._talk_to_gui('receive', {'source': 'filename', 'dest': 'new_filename'})
  214. # Wait until you get a reply
  215. while self._wait_for_reply:
  216. time.sleep(self.WAIT_TIME)
  217. self._reset()
  218. time.sleep(self.WAIT_TIME)
  219. # Call the destructor function of YoutubeDLDownloader object
  220. self._downloader.close()
  221. def download(self, item):
  222. """Download given item.
  223. Args:
  224. item (dict): Python dictionary that contains two keys.
  225. The url and the index of the corresponding row in which
  226. the worker should send back the information about the
  227. download process.
  228. """
  229. self._data['url'] = item['url']
  230. self._data['index'] = item['index']
  231. def stop_download(self):
  232. """Stop the download process of the worker. """
  233. self._downloader.stop()
  234. def close(self):
  235. """Kill the worker after stopping the download process. """
  236. self._running = False
  237. self._downloader.stop()
  238. def available(self):
  239. """Return True if the worker has no job else False. """
  240. return self._data['url'] is None
  241. def has_index(self, index):
  242. """Return True if index is equal to self._data['index'] else False. """
  243. return self._data['index'] == index
  244. def update_data(self, data):
  245. """Update self._data from the given data. """
  246. if self._wait_for_reply:
  247. # Update data only if a receive request has been issued
  248. for key in data:
  249. self._data[key] = data[key]
  250. self._wait_for_reply = False
  251. @property
  252. def successful(self):
  253. """Return the number of successful downloads for current worker. """
  254. return self._successful
  255. def _reset(self):
  256. """Reset self._data back to the original state. """
  257. for key in self._data:
  258. self._data[key] = None
  259. def _log_data(self, data):
  260. """Callback method for self._downloader.
  261. This method is used to write the given data in a synchronized way
  262. to the log file using the self.log_manager and the self.log_lock.
  263. Args:
  264. data (string): String to write to the log file.
  265. """
  266. if self.log_manager is not None:
  267. self.log_lock.acquire()
  268. self.log_manager.log(data)
  269. self.log_lock.release()
  270. def _data_hook(self, data):
  271. """Callback method for self._downloader.
  272. This method updates self._data and sends the updates back to the
  273. GUI using the self._talk_to_gui() method.
  274. Args:
  275. data (dict): Python dictionary which contains information
  276. about the download process. For more info see the
  277. extract_data() function under the downloaders.py module.
  278. """
  279. # Temp dictionary which holds the updates
  280. temp_dict = {}
  281. # Update each key
  282. for key in data:
  283. if self._data[key] != data[key]:
  284. self._data[key] = data[key]
  285. temp_dict[key] = data[key]
  286. # Build the playlist status if there is an update
  287. if self._data['playlist_index'] is not None:
  288. if 'status' in temp_dict or 'playlist_index' in temp_dict:
  289. temp_dict['status'] = '{status} {index}/{size}'.format(
  290. status=self._data['status'],
  291. index=self._data['playlist_index'],
  292. size=self._data['playlist_size']
  293. )
  294. if len(temp_dict):
  295. self._talk_to_gui('send', temp_dict)
  296. def _talk_to_gui(self, signal, data):
  297. """Communicate with the GUI using wxCallAfter and wxPublisher.
  298. Send/Ask data to/from the GUI. Note that if the signal is 'receive'
  299. then the Worker will wait until it receives a reply from the GUI.
  300. Args:
  301. signal (string): Unique string that informs the GUI about the
  302. communication procedure.
  303. data (dict): Python dictionary which holds the data to be sent
  304. back to the GUI. If the signal is 'send' then the dictionary
  305. contains the updates for the GUI (e.g. percentage, eta). If
  306. the signal is 'receive' then the dictionary contains exactly
  307. three keys. The 'index' (row) from which we want to retrieve
  308. the data, the 'source' which identifies a column in the
  309. wxListCtrl widget and the 'dest' which tells the wxListCtrl
  310. under which key to store the retrieved data.
  311. Note:
  312. Worker class supports 2 signals.
  313. 1) send: The Worker sends data back to the GUI
  314. (e.g. Send status updates).
  315. 2) receive: The Worker asks data from the GUI
  316. (e.g. Receive the name of a file).
  317. Structure:
  318. ('send', {'index': <item_row>, data_to_send*})
  319. ('receive', {'index': <item_row>, 'source': 'source_key', 'dest': 'destination_key'})
  320. """
  321. data['index'] = self._data['index']
  322. if signal == 'receive':
  323. self._wait_for_reply = True
  324. CallAfter(Publisher.sendMessage, WORKER_PUB_TOPIC, (signal, data))