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.

348 lines
11 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
  1. #!/usr/bin/env python2
  2. """Python module to download videos.
  3. This module contains the actual downloaders responsible
  4. for downloading the video files. It's more like a driver module
  5. that connects the youtubedlg with different 3rd party (or not) downloaders.
  6. Note:
  7. downloaders.py is part of the youtubedlg package but it can be used
  8. as a stand alone driver module for downloading videos.
  9. """
  10. import os
  11. import sys
  12. import locale
  13. import subprocess
  14. class YoutubeDLDownloader(object):
  15. """Python class for downloading videos using youtube-dl & subprocess.
  16. Attributes:
  17. OK, ERROR, STOPPED, ALREADY, FILESIZE_ABORT (int): 'Random' integers
  18. that describe the return code from the download() method.
  19. Args:
  20. youtubedl_path (string): Absolute path to youtube-dl binary.
  21. data_hook (function): Optional callback function to retrieve download
  22. process data.
  23. log_manager (logmanager.LogManager): Object responsible for writing
  24. errors to the log.
  25. Note:
  26. For available data keys check self._data under __init__()
  27. Example:
  28. How to use YoutubeDLDownloader from a python script.
  29. from downloaders import YoutubeDLDownloader
  30. def data_hook(data):
  31. print data
  32. downloader = YoutubeDLDownloader('/usr/bin/youtube-dl', data_hook)
  33. downloader.download(<URL STRING>, ['-f', 'flv'])
  34. """
  35. OK = 0
  36. ERROR = 1
  37. STOPPED = 2
  38. ALREADY = 3
  39. FILESIZE_ABORT = 4
  40. def __init__(self, youtubedl_path, data_hook=None, log_manager=None):
  41. self.youtubedl_path = youtubedl_path
  42. self.log_manager = log_manager
  43. self.data_hook = data_hook
  44. self._return_code = 0
  45. self._proc = None
  46. self._data = {
  47. 'playlist_index': None,
  48. 'playlist_size': None,
  49. 'filesize': None,
  50. 'filename': None,
  51. 'percent': None,
  52. 'status': None,
  53. 'speed': None,
  54. 'eta': None
  55. }
  56. def download(self, url, options):
  57. """Download url using given options.
  58. Args:
  59. url (string): URL string to download.
  60. options (list): Python list that contains youtube-dl options.
  61. Returns:
  62. An integer that shows the status of the download process.
  63. Right now we support 5 different return codes.
  64. OK (0): The download process completed successfully.
  65. ERROR (1): An error occured during the download process.
  66. STOPPED (2): The download process was stopped from the user.
  67. ALREADY (3): The given url is already downloaded.
  68. FILESIZE_ABORT (4): The corresponding url video file was larger or
  69. smaller from the given options filesize limit.
  70. """
  71. self._reset()
  72. cmd = self._get_cmd(url, options)
  73. self._create_process(cmd)
  74. while self._proc_is_alive():
  75. stdout, stderr = self._read()
  76. if stderr:
  77. self._return_code = self.ERROR
  78. self._log(stderr)
  79. if stdout:
  80. self._sync_data(extract_data(stdout))
  81. self._hook_data()
  82. self._last_data_hook()
  83. return self._return_code
  84. def stop(self):
  85. """Stop the download process and set return code to STOPPED. """
  86. if self._proc_is_alive():
  87. self._proc.kill()
  88. self._return_code = self.STOPPED
  89. def _last_data_hook(self):
  90. """Set the last data information based on the return code. """
  91. if self._return_code == self.OK:
  92. self._data['status'] = 'Finished'
  93. elif self._return_code == self.ERROR:
  94. self._data['status'] = 'Error'
  95. self._data['speed'] = ''
  96. self._data['eta'] = ''
  97. elif self._return_code == self.STOPPED:
  98. self._data['status'] = 'Stopped'
  99. self._data['speed'] = ''
  100. self._data['eta'] = ''
  101. elif self._return_code == self.ALREADY:
  102. self._data['status'] = 'Already Downloaded'
  103. else:
  104. self._data['status'] = 'Filesize Abort'
  105. self._hook_data()
  106. def _reset(self):
  107. """Reset the data. """
  108. self._return_code = 0
  109. self._data = {
  110. 'playlist_index': None,
  111. 'playlist_size': None,
  112. 'filesize': None,
  113. 'filename': None,
  114. 'percent': None,
  115. 'status': None,
  116. 'speed': None,
  117. 'eta': None
  118. }
  119. def _sync_data(self, data):
  120. """ Synchronise self._data with data. It also filters some keys.
  121. Args:
  122. data (dictionary): Python dictionary that contains different
  123. keys. The keys are not standar the dictionary can also be
  124. empty when there are no data to extract. See extract_data().
  125. """
  126. for key in data:
  127. if key == 'filename':
  128. # Keep only the filename on data['filename']
  129. data['filename'] = os.path.basename(data['filename'])
  130. if key == 'status':
  131. if data['status'] == 'Already Downloaded':
  132. # Set self._return_code to already downloaded
  133. # and trash that key (GUI won't read it if it's None)
  134. self._return_code = self.ALREADY
  135. data['status'] = None
  136. if data['status'] == 'Filesize Abort':
  137. # Set self._return_code to filesize abort
  138. # and trash that key (GUI won't read it if it's None)
  139. self._return_code = self.FILESIZE_ABORT
  140. data['status'] = None
  141. self._data[key] = data[key]
  142. def _log(self, data):
  143. """Log data using log_manager.
  144. Args:
  145. data (string): String to write in the log file.
  146. """
  147. if self.log_manager is not None:
  148. self.log_manager.log(data)
  149. def _hook_data(self):
  150. """Pass self._data back to data_hook. """
  151. if self.data_hook is not None:
  152. self.data_hook(self._data)
  153. def _proc_is_alive(self):
  154. """Return True if self._proc is alive. Else False. """
  155. if self._proc is None:
  156. return False
  157. return self._proc.poll() is None
  158. def _read(self):
  159. """Read subprocess stdout, stderr.
  160. Returns:
  161. Python tuple that contains the STDOUT string and
  162. the STDERR string.
  163. """
  164. stdout = stderr = ''
  165. stdout = self._read_stream(self._proc.stdout)
  166. if not stdout:
  167. stderr = self._read_stream(self._proc.stderr)
  168. return stdout, stderr
  169. def _read_stream(self, stream):
  170. """Read subprocess stream.
  171. Args:
  172. stream (subprocess.PIPE): Subprocess pipe. Can be either STDOUT
  173. or STDERR.
  174. Returns:
  175. String that contains the stream (STDOUT or STDERR) string.
  176. """
  177. if self._proc is None:
  178. return ''
  179. return stream.readline().rstrip()
  180. def _get_cmd(self, url, options):
  181. """Build the subprocess command.
  182. Args:
  183. url (string): URL string to download.
  184. options (list): Python list that contains youtube-dl options.
  185. Returns:
  186. Python list that contains the command to execute.
  187. """
  188. if os.name == 'nt':
  189. cmd = [self.youtubedl_path] + options + [url]
  190. else:
  191. cmd = ['python', self.youtubedl_path] + options + [url]
  192. return cmd
  193. def _create_process(self, cmd):
  194. """Create new subprocess.
  195. Args:
  196. cmd (list): Python list that contains the command to execute.
  197. """
  198. encoding = info = None
  199. # Hide subprocess window on Windows
  200. if os.name == 'nt':
  201. info = subprocess.STARTUPINFO()
  202. info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  203. # Encode command for subprocess
  204. # Refer to http://stackoverflow.com/a/9951851/35070
  205. if sys.version_info < (3, 0) and sys.platform == 'win32':
  206. try:
  207. encoding = locale.getpreferredencoding()
  208. u'TEST'.encode(encoding)
  209. except:
  210. encoding = 'UTF-8'
  211. if encoding is not None:
  212. cmd = [item.encode(encoding, 'ignore') for item in cmd]
  213. self._proc = subprocess.Popen(cmd,
  214. stdout=subprocess.PIPE,
  215. stderr=subprocess.PIPE,
  216. startupinfo=info)
  217. def extract_data(stdout):
  218. """Extract data from youtube-dl stdout.
  219. Args:
  220. stdout (string): String that contains the youtube-dl stdout.
  221. Returns:
  222. Python dictionary. For available keys check self._data under
  223. YoutubeDLDownloader.__init__().
  224. """
  225. data_dictionary = dict()
  226. if not stdout:
  227. return data_dictionary
  228. stdout = [string for string in stdout.split(' ') if string != '']
  229. stdout[0] = stdout[0].lstrip('\r')
  230. if stdout[0] == '[download]':
  231. data_dictionary['status'] = 'Downloading'
  232. # Get filename
  233. if stdout[1] == 'Destination:':
  234. data_dictionary['filename'] = ' '.join(stdout[1:])
  235. # Get progress info
  236. if '%' in stdout[1]:
  237. if stdout[1] == '100%':
  238. data_dictionary['speed'] = ''
  239. data_dictionary['eta'] = ''
  240. else:
  241. data_dictionary['percent'] = stdout[1]
  242. data_dictionary['filesize'] = stdout[3]
  243. data_dictionary['speed'] = stdout[5]
  244. data_dictionary['eta'] = stdout[7]
  245. # Get playlist info
  246. if stdout[1] == 'Downloading' and stdout[2] == 'video':
  247. data_dictionary['playlist_index'] = stdout[3]
  248. data_dictionary['playlist_size'] = stdout[5]
  249. # Get file already downloaded status
  250. if stdout[-1] == 'downloaded':
  251. data_dictionary['status'] = 'Already Downloaded'
  252. # Get filesize abort status
  253. if stdout[-1] == 'Aborting.':
  254. data_dictionary['status'] = 'Filesize Abort'
  255. elif stdout[0] == '[ffmpeg]':
  256. data_dictionary['status'] = 'Post Processing'
  257. else:
  258. data_dictionary['status'] = 'Pre Processing'
  259. return data_dictionary