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.

347 lines
10 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
  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. # Close all the workers
  78. for worker in self._workers:
  79. worker.close()
  80. # Join and collect
  81. for worker in self._workers:
  82. worker.join()
  83. self._successful += worker.successful
  84. self._time_it_took = time.time() - self._time_it_took
  85. if not self._running:
  86. self._talk_to_gui('closed')
  87. else:
  88. self._talk_to_gui('finished')
  89. def active(self):
  90. """Returns number of active items.
  91. Note:
  92. active_items = (workers that work) + (items waiting in the url_list).
  93. """
  94. counter = 0
  95. for worker in self._workers:
  96. if not worker.available():
  97. counter += 1
  98. counter += len(self.urls_list)
  99. return counter
  100. def stop_downloads(self):
  101. """Stop the download process. Also send 'closing'
  102. signal back to the GUI.
  103. Note:
  104. It does NOT kill the workers thats the job of the
  105. clean up task in the run() method.
  106. """
  107. self._talk_to_gui('closing')
  108. self._running = False
  109. def add_url(self, url):
  110. """Add given url to the urls_list.
  111. Args:
  112. url (dictionary): Python dictionary that contains two keys.
  113. The url and the index of the corresponding row in which
  114. the worker should send back the information about the
  115. download process.
  116. """
  117. self.urls_list.append(url)
  118. def _talk_to_gui(self, data):
  119. """Send data back to the GUI using wxCallAfter and wxPublisher.
  120. Args:
  121. data (string): Unique signal string that informs the GUI for the
  122. download process.
  123. Note:
  124. DownloadManager supports 3 signals.
  125. 1) closing: The download process is closing.
  126. 2) closed: The download process has closed.
  127. 3) finished: The download process was completed normally.
  128. """
  129. CallAfter(Publisher.sendMessage, MANAGER_PUB_TOPIC, data)
  130. def _check_youtubedl(self):
  131. """Check if youtube-dl binary exists. If not try to download it. """
  132. if not os.path.exists(self._youtubedl_path()):
  133. UpdateThread(self.opt_manager.options['youtubedl_path'], True).join()
  134. def _jobs_done(self):
  135. """Returns True if the workers have finished their jobs else False. """
  136. for worker in self._workers:
  137. if not worker.available():
  138. return False
  139. return True
  140. def _youtubedl_path(self):
  141. """Returns the path to youtube-dl binary. """
  142. path = self.opt_manager.options['youtubedl_path']
  143. path = os.path.join(path, YOUTUBEDL_BIN)
  144. return path
  145. class Worker(Thread):
  146. """Simple worker which downloads the given url using a downloader
  147. from the 'downloaders' module.
  148. Attributes:
  149. WAIT_TIME (float): Time in seconds to sleep.
  150. Args:
  151. opt_manager (optionsmanager.OptionsManager): Check DownloadManager
  152. description.
  153. youtubedl (string): Absolute path to youtube-dl binary.
  154. log_manager (logmanager.LogManager): Check DownloadManager
  155. description.
  156. log_lock (threading.Lock): Synchronization lock for the log_manager.
  157. If the log_manager is set (not None) then the caller has to make
  158. sure that the log_lock is also set.
  159. """
  160. WAIT_TIME = 0.1
  161. def __init__(self, opt_manager, youtubedl, log_manager=None, log_lock=None):
  162. super(Worker, self).__init__()
  163. self.opt_manager = opt_manager
  164. self.log_manager = log_manager
  165. self.log_lock = log_lock
  166. self._downloader = YoutubeDLDownloader(youtubedl, self._data_hook, self._log_data)
  167. self._options_parser = OptionsParser()
  168. self._running = True
  169. self._url = None
  170. self._index = -1
  171. self._successful = 0
  172. self.start()
  173. def run(self):
  174. while self._running:
  175. if self._url is not None:
  176. options = self._options_parser.parse(self.opt_manager.options)
  177. ret_code = self._downloader.download(self._url, options)
  178. if (ret_code == YoutubeDLDownloader.OK or
  179. ret_code == YoutubeDLDownloader.ALREADY):
  180. self._successful += 1
  181. # Reset url value
  182. self._url = None
  183. time.sleep(self.WAIT_TIME)
  184. # Call the destructor function of YoutubeDLDownloader object
  185. self._downloader.close()
  186. def download(self, item):
  187. """Download given item.
  188. Args:
  189. item (dictionary): Python dictionary that contains two keys.
  190. The url and the index of the corresponding row in which
  191. the worker should send back the information about the
  192. download process.
  193. """
  194. self._url = item['url']
  195. self._index = item['index']
  196. def stop_download(self):
  197. """Stop the download process of the worker. """
  198. self._downloader.stop()
  199. def close(self):
  200. """Kill the worker after stopping the download process. """
  201. self._running = False
  202. self._downloader.stop()
  203. def available(self):
  204. """Return True if the worker has no job else False. """
  205. return self._url is None
  206. @property
  207. def successful(self):
  208. """Return the number of successful downloads for current worker. """
  209. return self._successful
  210. def _log_data(self, data):
  211. """Callback method for self._downloader.
  212. This method is used to write the given data in a synchronized way
  213. to the log file using the self.log_manager and the self.log_lock.
  214. Args:
  215. data (string): String to write to the log file.
  216. """
  217. if self.log_manager is not None:
  218. self.log_lock.acquire()
  219. self.log_manager.log(data)
  220. self.log_lock.release()
  221. def _data_hook(self, data):
  222. """Callback method to be used with the YoutubeDLDownloader object.
  223. This method takes the data from the downloader, merges the
  224. playlist_info with the current status(if any) and sends the
  225. data back to the GUI using the self._talk_to_gui method.
  226. Args:
  227. data (dictionary): Python dictionary which contains information
  228. about the download process. (See YoutubeDLDownloader class).
  229. """
  230. if data['status'] is not None and data['playlist_index'] is not None:
  231. playlist_info = ' '
  232. playlist_info += data['playlist_index']
  233. playlist_info += '/'
  234. playlist_info += data['playlist_size']
  235. data['status'] += playlist_info
  236. self._talk_to_gui(data)
  237. def _talk_to_gui(self, data):
  238. """Send data back to the GUI after inserting the index. """
  239. data['index'] = self._index
  240. CallAfter(Publisher.sendMessage, WORKER_PUB_TOPIC, data)
  241. if __name__ == '__main__':
  242. """Direct call of the module for testing.
  243. Raises:
  244. ValueError: Attempted relative import in non-package
  245. Note:
  246. Before you run the tests change relative imports else an exceptions
  247. will be raised. You need to change relative imports on all the modules
  248. you are gonna use.
  249. """
  250. print "No tests available"