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.

259 lines
7.1 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
  1. #!/usr/bin/env python2
  2. ''' Python module to download videos using youtube-dl & subprocess. '''
  3. import os
  4. import sys
  5. import locale
  6. import subprocess
  7. class DownloadObject(object):
  8. '''
  9. Download videos using youtube-dl & subprocess.
  10. Params
  11. youtubedl_path: Absolute path of youtube-dl.
  12. data_hook: Can be any function with one parameter, the data.
  13. logger: Can be any logger which implements log().
  14. Accessible Methods
  15. download()
  16. Params: URL to download
  17. Options list e.g. ['--help']
  18. Return: DownlaodObject.OK
  19. DownloadObject.ERROR
  20. DownloadObject.STOPPED
  21. DownloadObject.ALREADY
  22. stop()
  23. Params: None
  24. Return: None
  25. Properties
  26. files_list: Python list that contains all the files DownloadObject
  27. instance has downloaded.
  28. Data_hook Keys
  29. 'playlist_index',
  30. 'playlist_size',
  31. 'filesize',
  32. 'filename',
  33. 'percent',
  34. 'status',
  35. 'speed',
  36. 'eta'
  37. '''
  38. # download() return codes
  39. OK = 0
  40. ERROR = 1
  41. STOPPED = 2
  42. ALREADY = 3
  43. def __init__(self, youtubedl_path, data_hook=None, logger=None):
  44. self.youtubedl_path = youtubedl_path
  45. self.data_hook = data_hook
  46. self.logger = logger
  47. self._return_code = 0
  48. self._files_list = []
  49. self._proc = None
  50. self._data = {
  51. 'playlist_index': None,
  52. 'playlist_size': None,
  53. 'filesize': None,
  54. 'filename': None,
  55. 'percent': None,
  56. 'status': None,
  57. 'speed': None,
  58. 'eta': None
  59. }
  60. @property
  61. def files_list(self):
  62. ''' Return list that contains all files
  63. DownloadObject has downloaded.
  64. '''
  65. return self._files_list
  66. def download(self, url, options):
  67. ''' Download given url using youtube-dl &
  68. return self._return_code.
  69. '''
  70. self._return_code = self.OK
  71. cmd = self._get_cmd(url, options)
  72. self._create_process(cmd)
  73. while self._proc_is_alive():
  74. stdout, stderr = self._read()
  75. data = extract_data(stdout)
  76. if self._update_data(data):
  77. self._hook_data()
  78. if stderr != '':
  79. self._return_code = self.ERROR
  80. self._log(stderr)
  81. return self._return_code
  82. def stop(self):
  83. ''' Stop downloading. '''
  84. if self._proc_is_alive():
  85. self._proc.kill()
  86. self._return_code = self.STOPPED
  87. def _update_data(self, data):
  88. ''' Update self._data from data.
  89. Return True if updated else return False.
  90. '''
  91. updated = False
  92. for key in data:
  93. if key == 'filename':
  94. # Save full file path on self._files_list
  95. self._add_on_files_list(data['filename'])
  96. # Keep only the filename on data['filename']
  97. data['filename'] = os.path.basename(data['filename'])
  98. if key == 'status':
  99. # Set self._return_code to already downloaded
  100. if data[key] == 'Already Downloaded':
  101. self._return_code = self.ALREADY
  102. # Trash that key
  103. data[key] = None
  104. self._data[key] = data[key]
  105. if not updated:
  106. updated = True
  107. return updated
  108. def _add_on_files_list(self, filename):
  109. ''' Add filename on self._files_list. '''
  110. self._files_list.append(filename)
  111. def _log(self, data):
  112. ''' Log data using self.logger. '''
  113. if self.logger is not None:
  114. self.logger.log(data)
  115. def _hook_data(self):
  116. ''' Pass self._data back to data_hook. '''
  117. if self.data_hook is not None:
  118. self.data_hook(self._data)
  119. def _proc_is_alive(self):
  120. ''' Return True if self._proc is alive. '''
  121. if self._proc is None:
  122. return False
  123. return self._proc.poll() is None
  124. def _read(self):
  125. ''' Read subprocess stdout, stderr. '''
  126. stdout = stderr = ''
  127. stdout = self._read_stream(self._proc.stdout)
  128. if stdout == '':
  129. stderr = self._read_stream(self._proc.stderr)
  130. return stdout, stderr
  131. def _read_stream(self, stream):
  132. ''' Read subprocess stream. '''
  133. if self._proc is None:
  134. return ''
  135. data = stream.readline()
  136. return data.rstrip()
  137. def _get_cmd(self, url, options):
  138. ''' Return command for subprocess. '''
  139. if os.name == 'nt':
  140. cmd = [self.youtubedl_path] + options + [url]
  141. else:
  142. cmd = ['python', self.youtubedl_path] + options + [url]
  143. return cmd
  144. def _create_process(self, cmd):
  145. ''' Create new subprocess. '''
  146. encoding = info = None
  147. # Hide subprocess window on Windows
  148. if os.name == 'nt':
  149. info = subprocess.STARTUPINFO()
  150. info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  151. # Encode command for subprocess
  152. # Refer to http://stackoverflow.com/a/9951851/35070
  153. if sys.version_info < (3, 0) and sys.platform == 'win32':
  154. try:
  155. encoding = locale.getpreferredencoding()
  156. u'TEST'.encode(encoding)
  157. except:
  158. encoding = 'UTF-8'
  159. if encoding is not None:
  160. cmd = [item.encode(encoding, 'ignore') for item in cmd]
  161. self._proc = subprocess.Popen(cmd,
  162. stdout=subprocess.PIPE,
  163. stderr=subprocess.PIPE,
  164. startupinfo=info)
  165. def extract_data(stdout):
  166. ''' Extract data from youtube-dl stdout. '''
  167. data_dictionary = {}
  168. stdout = [string for string in stdout.split(' ') if string != '']
  169. if len(stdout) == 0:
  170. return data_dictionary
  171. header = stdout.pop(0)
  172. if header == '[download]':
  173. data_dictionary['status'] = 'Downloading'
  174. # Get filename
  175. if stdout[0] == 'Destination:':
  176. data_dictionary['filename'] = ' '.join(stdout[1:])
  177. # Get progress info
  178. if '%' in stdout[0]:
  179. if stdout[0] == '100%':
  180. data_dictionary['speed'] = ''
  181. data_dictionary['eta'] = ''
  182. else:
  183. data_dictionary['percent'] = stdout[0]
  184. data_dictionary['filesize'] = stdout[2]
  185. data_dictionary['speed'] = stdout[4]
  186. data_dictionary['eta'] = stdout[6]
  187. # Get playlist info
  188. if stdout[0] == 'Downloading' and stdout[1] == 'video':
  189. data_dictionary['playlist_index'] = stdout[2]
  190. data_dictionary['playlist_size'] = stdout[4]
  191. # Get file already downloaded status
  192. if stdout[-1] == 'downloaded':
  193. data_dictionary['status'] = 'Already Downloaded'
  194. elif header == '[ffmpeg]':
  195. data_dictionary['status'] = 'Post Processing'
  196. else:
  197. data_dictionary['status'] = 'Pre Processing'
  198. return data_dictionary