summaryrefslogtreecommitdiff
path: root/youtube_dl/downloader/external.py
blob: 5d3e5d8d3d748d98ea187e8eca4444c5504e07fb (plain)
    1 from __future__ import unicode_literals
    2 
    3 import os.path
    4 import subprocess
    5 import sys
    6 import re
    7 
    8 from .common import FileDownloader
    9 from ..compat import compat_setenv
   10 from ..postprocessor.ffmpeg import FFmpegPostProcessor, EXT_TO_OUT_FORMATS
   11 from ..utils import (
   12     cli_option,
   13     cli_valueless_option,
   14     cli_bool_option,
   15     cli_configuration_args,
   16     encodeFilename,
   17     encodeArgument,
   18     handle_youtubedl_headers,
   19     check_executable,
   20 )
   21 
   22 
   23 class ExternalFD(FileDownloader):
   24     def real_download(self, filename, info_dict):
   25         self.report_destination(filename)
   26         tmpfilename = self.temp_name(filename)
   27 
   28         retval = self._call_downloader(tmpfilename, info_dict)
   29         if retval == 0:
   30             fsize = os.path.getsize(encodeFilename(tmpfilename))
   31             self.to_screen('\r[%s] Downloaded %s bytes' % (self.get_basename(), fsize))
   32             self.try_rename(tmpfilename, filename)
   33             self._hook_progress({
   34                 'downloaded_bytes': fsize,
   35                 'total_bytes': fsize,
   36                 'filename': filename,
   37                 'status': 'finished',
   38             })
   39             return True
   40         else:
   41             self.to_stderr('\n')
   42             self.report_error('%s exited with code %d' % (
   43                 self.get_basename(), retval))
   44             return False
   45 
   46     @classmethod
   47     def get_basename(cls):
   48         return cls.__name__[:-2].lower()
   49 
   50     @property
   51     def exe(self):
   52         return self.params.get('external_downloader')
   53 
   54     @classmethod
   55     def available(cls):
   56         return check_executable(cls.get_basename(), [cls.AVAILABLE_OPT])
   57 
   58     @classmethod
   59     def supports(cls, info_dict):
   60         return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps')
   61 
   62     @classmethod
   63     def can_download(cls, info_dict):
   64         return cls.available() and cls.supports(info_dict)
   65 
   66     def _option(self, command_option, param):
   67         return cli_option(self.params, command_option, param)
   68 
   69     def _bool_option(self, command_option, param, true_value='true', false_value='false', separator=None):
   70         return cli_bool_option(self.params, command_option, param, true_value, false_value, separator)
   71 
   72     def _valueless_option(self, command_option, param, expected_value=True):
   73         return cli_valueless_option(self.params, command_option, param, expected_value)
   74 
   75     def _configuration_args(self, default=[]):
   76         return cli_configuration_args(self.params, 'external_downloader_args', default)
   77 
   78     def _call_downloader(self, tmpfilename, info_dict):
   79         """ Either overwrite this or implement _make_cmd """
   80         cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
   81 
   82         self._debug_cmd(cmd)
   83 
   84         p = subprocess.Popen(
   85             cmd, stderr=subprocess.PIPE)
   86         _, stderr = p.communicate()
   87         if p.returncode != 0:
   88             self.to_stderr(stderr.decode('utf-8', 'replace'))
   89         return p.returncode
   90 
   91 
   92 class CurlFD(ExternalFD):
   93     AVAILABLE_OPT = '-V'
   94 
   95     def _make_cmd(self, tmpfilename, info_dict):
   96         cmd = [self.exe, '--location', '-o', tmpfilename]
   97         for key, val in info_dict['http_headers'].items():
   98             cmd += ['--header', '%s: %s' % (key, val)]
   99         cmd += self._bool_option('--continue-at', 'continuedl', '-', '0')
  100         cmd += self._valueless_option('--silent', 'noprogress')
  101         cmd += self._valueless_option('--verbose', 'verbose')
  102         cmd += self._option('--limit-rate', 'ratelimit')
  103         cmd += self._option('--retry', 'retries')
  104         cmd += self._option('--max-filesize', 'max_filesize')
  105         cmd += self._option('--interface', 'source_address')
  106         cmd += self._option('--proxy', 'proxy')
  107         cmd += self._valueless_option('--insecure', 'nocheckcertificate')
  108         cmd += self._configuration_args()
  109         cmd += ['--', info_dict['url']]
  110         return cmd
  111 
  112     def _call_downloader(self, tmpfilename, info_dict):
  113         cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
  114 
  115         self._debug_cmd(cmd)
  116 
  117         # curl writes the progress to stderr so don't capture it.
  118         p = subprocess.Popen(cmd)
  119         p.communicate()
  120         return p.returncode
  121 
  122 
  123 class AxelFD(ExternalFD):
  124     AVAILABLE_OPT = '-V'
  125 
  126     def _make_cmd(self, tmpfilename, info_dict):
  127         cmd = [self.exe, '-o', tmpfilename]
  128         for key, val in info_dict['http_headers'].items():
  129             cmd += ['-H', '%s: %s' % (key, val)]
  130         cmd += self._configuration_args()
  131         cmd += ['--', info_dict['url']]
  132         return cmd
  133 
  134 
  135 class WgetFD(ExternalFD):
  136     AVAILABLE_OPT = '--version'
  137 
  138     def _make_cmd(self, tmpfilename, info_dict):
  139         cmd = [self.exe, '-O', tmpfilename, '-nv', '--no-cookies']
  140         for key, val in info_dict['http_headers'].items():
  141             cmd += ['--header', '%s: %s' % (key, val)]
  142         cmd += self._option('--bind-address', 'source_address')
  143         cmd += self._option('--proxy', 'proxy')
  144         cmd += self._valueless_option('--no-check-certificate', 'nocheckcertificate')
  145         cmd += self._configuration_args()
  146         cmd += ['--', info_dict['url']]
  147         return cmd
  148 
  149 
  150 class Aria2cFD(ExternalFD):
  151     AVAILABLE_OPT = '-v'
  152 
  153     def _make_cmd(self, tmpfilename, info_dict):
  154         cmd = [self.exe, '-c']
  155         cmd += self._configuration_args([
  156             '--min-split-size', '1M', '--max-connection-per-server', '4'])
  157         dn = os.path.dirname(tmpfilename)
  158         if dn:
  159             cmd += ['--dir', dn]
  160         cmd += ['--out', os.path.basename(tmpfilename)]
  161         for key, val in info_dict['http_headers'].items():
  162             cmd += ['--header', '%s: %s' % (key, val)]
  163         cmd += self._option('--interface', 'source_address')
  164         cmd += self._option('--all-proxy', 'proxy')
  165         cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
  166         cmd += ['--', info_dict['url']]
  167         return cmd
  168 
  169 
  170 class HttpieFD(ExternalFD):
  171     @classmethod
  172     def available(cls):
  173         return check_executable('http', ['--version'])
  174 
  175     def _make_cmd(self, tmpfilename, info_dict):
  176         cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
  177         for key, val in info_dict['http_headers'].items():
  178             cmd += ['%s:%s' % (key, val)]
  179         return cmd
  180 
  181 
  182 class FFmpegFD(ExternalFD):
  183     @classmethod
  184     def supports(cls, info_dict):
  185         return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
  186 
  187     @classmethod
  188     def available(cls):
  189         return FFmpegPostProcessor().available
  190 
  191     def _call_downloader(self, tmpfilename, info_dict):
  192         url = info_dict['url']
  193         ffpp = FFmpegPostProcessor(downloader=self)
  194         if not ffpp.available:
  195             self.report_error('m3u8 download detected but ffmpeg or avconv could not be found. Please install one.')
  196             return False
  197         ffpp.check_version()
  198 
  199         args = [ffpp.executable, '-y']
  200 
  201         args += self._configuration_args()
  202 
  203         # start_time = info_dict.get('start_time') or 0
  204         # if start_time:
  205         #     args += ['-ss', compat_str(start_time)]
  206         # end_time = info_dict.get('end_time')
  207         # if end_time:
  208         #     args += ['-t', compat_str(end_time - start_time)]
  209 
  210         if info_dict['http_headers'] and re.match(r'^https?://', url):
  211             # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
  212             # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
  213             headers = handle_youtubedl_headers(info_dict['http_headers'])
  214             args += [
  215                 '-headers',
  216                 ''.join('%s: %s\r\n' % (key, val) for key, val in headers.items())]
  217 
  218         env = None
  219         proxy = self.params.get('proxy')
  220         if proxy:
  221             if not re.match(r'^[\da-zA-Z]+://', proxy):
  222                 proxy = 'http://%s' % proxy
  223 
  224             if proxy.startswith('socks'):
  225                 self.report_warning(
  226                     '%s does not support SOCKS proxies. Downloading is likely to fail. '
  227                     'Consider adding --hls-prefer-native to your command.' % self.get_basename())
  228 
  229             # Since December 2015 ffmpeg supports -http_proxy option (see
  230             # http://git.videolan.org/?p=ffmpeg.git;a=commit;h=b4eb1f29ebddd60c41a2eb39f5af701e38e0d3fd)
  231             # We could switch to the following code if we are able to detect version properly
  232             # args += ['-http_proxy', proxy]
  233             env = os.environ.copy()
  234             compat_setenv('HTTP_PROXY', proxy, env=env)
  235             compat_setenv('http_proxy', proxy, env=env)
  236 
  237         protocol = info_dict.get('protocol')
  238 
  239         if protocol == 'rtmp':
  240             player_url = info_dict.get('player_url')
  241             page_url = info_dict.get('page_url')
  242             app = info_dict.get('app')
  243             play_path = info_dict.get('play_path')
  244             tc_url = info_dict.get('tc_url')
  245             flash_version = info_dict.get('flash_version')
  246             live = info_dict.get('rtmp_live', False)
  247             if player_url is not None:
  248                 args += ['-rtmp_swfverify', player_url]
  249             if page_url is not None:
  250                 args += ['-rtmp_pageurl', page_url]
  251             if app is not None:
  252                 args += ['-rtmp_app', app]
  253             if play_path is not None:
  254                 args += ['-rtmp_playpath', play_path]
  255             if tc_url is not None:
  256                 args += ['-rtmp_tcurl', tc_url]
  257             if flash_version is not None:
  258                 args += ['-rtmp_flashver', flash_version]
  259             if live:
  260                 args += ['-rtmp_live', 'live']
  261 
  262         args += ['-i', url, '-c', 'copy']
  263         if protocol in ('m3u8', 'm3u8_native'):
  264             if self.params.get('hls_use_mpegts', False) or tmpfilename == '-':
  265                 args += ['-f', 'mpegts']
  266             else:
  267                 args += ['-f', 'mp4', '-bsf:a', 'aac_adtstoasc']
  268         elif protocol == 'rtmp':
  269             args += ['-f', 'flv']
  270         else:
  271             args += ['-f', EXT_TO_OUT_FORMATS.get(info_dict['ext'], info_dict['ext'])]
  272 
  273         args = [encodeArgument(opt) for opt in args]
  274         args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
  275 
  276         self._debug_cmd(args)
  277 
  278         proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
  279         try:
  280             retval = proc.wait()
  281         except KeyboardInterrupt:
  282             # subprocces.run would send the SIGKILL signal to ffmpeg and the
  283             # mp4 file couldn't be played, but if we ask ffmpeg to quit it
  284             # produces a file that is playable (this is mostly useful for live
  285             # streams). Note that Windows is not affected and produces playable
  286             # files (see https://github.com/rg3/youtube-dl/issues/8300).
  287             if sys.platform != 'win32':
  288                 proc.communicate(b'q')
  289             raise
  290         return retval
  291 
  292 
  293 class AVconvFD(FFmpegFD):
  294     pass
  295 
  296 
  297 _BY_NAME = dict(
  298     (klass.get_basename(), klass)
  299     for name, klass in globals().items()
  300     if name.endswith('FD') and name != 'ExternalFD'
  301 )
  302 
  303 
  304 def list_external_downloaders():
  305     return sorted(_BY_NAME.keys())
  306 
  307 
  308 def get_external_downloader(external_downloader):
  309     """ Given the name of the executable, see whether we support the given
  310         downloader . """
  311     # Drop .exe extension on Windows
  312     bn = os.path.splitext(os.path.basename(external_downloader))[0]
  313     return _BY_NAME[bn]

Generated by cgit