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.

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