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.

242 lines
6.7 KiB

  1. #! /usr/bin/env python
  2. import subprocess
  3. from .Utils import (
  4. get_encoding,
  5. get_filename,
  6. encode_list,
  7. os_type
  8. )
  9. class DownloadObject(object):
  10. '''
  11. Download videos using youtube-dl & subprocess.
  12. Params
  13. youtubedl_path: Absolute path of youtube-dl.
  14. data_hook: Can be any function with one parameter, the data.
  15. logger: Can be any logger which implements log().
  16. Accessible Methods
  17. download()
  18. Params: URL to download
  19. Options list e.g. ['--help']
  20. Return: See downlaoad() return codes
  21. Return-Codes:
  22. OK : 'Url downloaded successfully'
  23. ERROR : 'Error occured while downloading'
  24. ALREADY: 'Url is already downloaded'
  25. stop()
  26. Params: None
  27. Acessible Variables
  28. files_list: Python list that contains all the files DownloadObject
  29. instance has downloaded.
  30. Data_hook Keys
  31. See self._init_data().
  32. '''
  33. # download() return codes
  34. OK = 0
  35. ERROR = 1
  36. ALREADY = -1
  37. STDERR_IGNORE = '' # Default filter for our self._log() method
  38. def __init__(self, youtubedl_path, data_hook=None, logger=None):
  39. self.youtubedl_path = youtubedl_path
  40. self.data_hook = data_hook
  41. self.logger = logger
  42. self.files_list = []
  43. self._return_code = 0
  44. self._proc = None
  45. self._init_data()
  46. def _init_data(self):
  47. ''' Keep the __init__() clean. '''
  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. self._return_code = self.OK
  60. cmd = self._get_cmd(url, options)
  61. cmd = self._encode_cmd(cmd)
  62. info = self._get_process_info()
  63. self._proc = self._create_process(cmd, info)
  64. while self._proc_is_alive():
  65. stdout, stderr = self._read()
  66. data = extract_data(stdout)
  67. synced = self._sync_data(data)
  68. if stderr != '':
  69. self._return_code = self.ERROR
  70. if self.logger is not None:
  71. self._log(stderr)
  72. if self.data_hook is not None and synced:
  73. self._hook_data()
  74. return self._return_code
  75. def stop(self):
  76. if self._proc is not None:
  77. self._proc.kill()
  78. def _sync_data(self, data):
  79. ''' Sync data between extract_data() dictionary and self._data.
  80. Return True if synced else return False.
  81. '''
  82. synced = False
  83. for key in data:
  84. if key == 'filename':
  85. # Save full file path on files_list
  86. self._add_on_files_list(data['filename'])
  87. # Keep only the filename not the path on data['filename']
  88. data['filename'] = get_filename(data['filename'])
  89. if key == 'status':
  90. # Set self._return_code to already downloaded
  91. if data[key] == 'already_downloaded':
  92. self._return_code = self.ALREADY
  93. self._data[key] = data[key]
  94. synced = True
  95. return synced
  96. def _add_on_files_list(self, filename):
  97. self.files_list.append(filename)
  98. def _log(self, data):
  99. if data != self.STDERR_IGNORE:
  100. self.logger.log(data)
  101. def _hook_data(self):
  102. ''' Pass self._data back to data_hook. '''
  103. self.data_hook(self._data)
  104. def _proc_is_alive(self):
  105. ''' Return True if self._proc is alive. '''
  106. if self._proc is None:
  107. return False
  108. return self._proc.poll() is None
  109. def _read(self):
  110. ''' Read subprocess stdout, stderr. '''
  111. stdout = self._read_stdout()
  112. if stdout == '':
  113. stderr = self._read_stderr()
  114. else:
  115. stderr = ''
  116. return stdout, stderr
  117. def _read_stdout(self):
  118. if self._proc is None:
  119. return ''
  120. stdout = self._proc.stdout.readline()
  121. return stdout.rstrip()
  122. def _read_stderr(self):
  123. if self._proc is None:
  124. return ''
  125. stderr = self._proc.stderr.readline()
  126. return stderr.rstrip()
  127. def _create_process(self, cmd, info):
  128. return subprocess.Popen(cmd,
  129. stdout=subprocess.PIPE,
  130. stderr=subprocess.PIPE,
  131. startupinfo=info)
  132. def _get_cmd(self, url, options):
  133. ''' Return command for subprocess. '''
  134. if os_type == 'nt':
  135. cmd = [self.youtubedl_path] + options + [url]
  136. else:
  137. cmd = ['python', self.youtubedl_path] + options + [url]
  138. return cmd
  139. def _encode_cmd(self, cmd):
  140. ''' Encode command for subprocess.
  141. Refer to http://stackoverflow.com/a/9951851/35070
  142. '''
  143. encoding = get_encoding()
  144. if encoding is not None:
  145. cmd = encode_list(cmd, encoding)
  146. return cmd
  147. def _get_process_info(self):
  148. ''' Hide subprocess window on Windows. '''
  149. if os_type == 'nt':
  150. info = subprocess.STARTUPINFO()
  151. info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  152. return info
  153. else:
  154. return None
  155. def extract_data(stdout):
  156. ''' Extract data from youtube-dl stdout. '''
  157. data_dictionary = {}
  158. stdout = [s for s in stdout.split(' ') if s != '']
  159. if len(stdout) == 0:
  160. return data_dictionary
  161. header = stdout.pop(0).replace('[', '').replace(']', '')
  162. if header == 'download':
  163. data_dictionary['status'] = 'download'
  164. # Get filename
  165. if stdout[0] == 'Destination:':
  166. data_dictionary['filename'] = ' '.join(stdout[1:])
  167. # Get progress info
  168. elif '%' in stdout[0]:
  169. if stdout[0] == '100%':
  170. data_dictionary['speed'] = ''
  171. else:
  172. data_dictionary['percent'] = stdout[0]
  173. data_dictionary['filesize'] = stdout[2]
  174. data_dictionary['speed'] = stdout[4]
  175. data_dictionary['eta'] = stdout[6]
  176. # Get playlist info
  177. elif stdout[0] == 'Downloading' and stdout[1] == 'video':
  178. data_dictionary['playlist_index'] = stdout[2]
  179. data_dictionary['playlist_size'] = stdout[4]
  180. # Get file already downloaded status
  181. elif stdout[-1] == 'downloaded':
  182. data_dictionary['status'] = 'already_downloaded'
  183. if header == 'ffmpeg':
  184. data_dictionary['status'] = 'post_process'
  185. if header == 'youtube':
  186. data_dictionary['status'] = 'pre_process'
  187. return data_dictionary