Credit @thornomad for glide (#3944)
[youtube-dl] / youtube_dl / __init__.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 __authors__  = (
5     'Ricardo Garcia Gonzalez',
6     'Danny Colligan',
7     'Benjamin Johnson',
8     'Vasyl\' Vavrychuk',
9     'Witold Baryluk',
10     'Paweł Paprota',
11     'Gergely Imreh',
12     'Rogério Brito',
13     'Philipp Hagemeister',
14     'Sören Schulze',
15     'Kevin Ngo',
16     'Ori Avtalion',
17     'shizeeg',
18     'Filippo Valsorda',
19     'Christian Albrecht',
20     'Dave Vasilevsky',
21     'Jaime Marquínez Ferrándiz',
22     'Jeff Crouse',
23     'Osama Khalid',
24     'Michael Walter',
25     'M. Yasoob Ullah Khalid',
26     'Julien Fraichard',
27     'Johny Mo Swag',
28     'Axel Noack',
29     'Albert Kim',
30     'Pierre Rudloff',
31     'Huarong Huo',
32     'Ismael Mejía',
33     'Steffan \'Ruirize\' James',
34     'Andras Elso',
35     'Jelle van der Waa',
36     'Marcin Cieślak',
37     'Anton Larionov',
38     'Takuya Tsuchida',
39     'Sergey M.',
40     'Michael Orlitzky',
41     'Chris Gahan',
42     'Saimadhav Heblikar',
43     'Mike Col',
44     'Oleg Prutz',
45     'pulpe',
46     'Andreas Schmitz',
47     'Michael Kaiser',
48     'Niklas Laxström',
49     'David Triendl',
50     'Anthony Weems',
51     'David Wagner',
52     'Juan C. Olivares',
53     'Mattias Harrysson',
54     'phaer',
55     'Sainyam Kapoor',
56     'Nicolas Évrard',
57     'Jason Normore',
58     'Hoje Lee',
59     'Adam Thalhammer',
60     'Georg Jähnig',
61     'Ralf Haring',
62     'Koki Takahashi',
63     'Ariset Llerena',
64     'Adam Malcontenti-Wilson',
65     'Tobias Bell',
66     'Naglis Jonaitis',
67     'Charles Chen',
68     'Hassaan Ali',
69     'Dobrosław Żybort',
70     'David Fabijan',
71     'Sebastian Haas',
72     'Alexander Kirk',
73     'Erik Johnson',
74     'Keith Beckman',
75     'Ole Ernst',
76     'Aaron McDaniel (mcd1992)',
77     'Magnus Kolstad',
78     'Hari Padmanaban',
79     'Carlos Ramos',
80     '5moufl',
81     'lenaten',
82     'Dennis Scheiba',
83     'Damon Timm',
84 )
85
86 __license__ = 'Public Domain'
87
88 import codecs
89 import io
90 import os
91 import random
92 import sys
93
94
95 from .options import (
96     parseOpts,
97 )
98 from .utils import (
99     compat_getpass,
100     compat_print,
101     DateRange,
102     DEFAULT_OUTTMPL,
103     decodeOption,
104     DownloadError,
105     MaxDownloadsReached,
106     preferredencoding,
107     read_batch_urls,
108     SameFileError,
109     setproctitle,
110     std_headers,
111     write_string,
112 )
113 from .update import update_self
114 from .downloader import (
115     FileDownloader,
116 )
117 from .extractor import gen_extractors
118 from .YoutubeDL import YoutubeDL
119 from .postprocessor import (
120     AtomicParsleyPP,
121     FFmpegAudioFixPP,
122     FFmpegMetadataPP,
123     FFmpegVideoConvertor,
124     FFmpegExtractAudioPP,
125     FFmpegEmbedSubtitlePP,
126     XAttrMetadataPP,
127     ExecAfterDownloadPP,
128 )
129
130
131 def _real_main(argv=None):
132     # Compatibility fixes for Windows
133     if sys.platform == 'win32':
134         # https://github.com/rg3/youtube-dl/issues/820
135         codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
136
137     setproctitle(u'youtube-dl')
138
139     parser, opts, args = parseOpts(argv)
140
141     # Set user agent
142     if opts.user_agent is not None:
143         std_headers['User-Agent'] = opts.user_agent
144
145     # Set referer
146     if opts.referer is not None:
147         std_headers['Referer'] = opts.referer
148
149     # Custom HTTP headers
150     if opts.headers is not None:
151         for h in opts.headers:
152             if h.find(':', 1) < 0:
153                 parser.error(u'wrong header formatting, it should be key:value, not "%s"'%h)
154             key, value = h.split(':', 2)
155             if opts.verbose:
156                 write_string(u'[debug] Adding header from command line option %s:%s\n'%(key, value))
157             std_headers[key] = value
158
159     # Dump user agent
160     if opts.dump_user_agent:
161         compat_print(std_headers['User-Agent'])
162         sys.exit(0)
163
164     # Batch file verification
165     batch_urls = []
166     if opts.batchfile is not None:
167         try:
168             if opts.batchfile == '-':
169                 batchfd = sys.stdin
170             else:
171                 batchfd = io.open(opts.batchfile, 'r', encoding='utf-8', errors='ignore')
172             batch_urls = read_batch_urls(batchfd)
173             if opts.verbose:
174                 write_string(u'[debug] Batch file urls: ' + repr(batch_urls) + u'\n')
175         except IOError:
176             sys.exit(u'ERROR: batch file could not be read')
177     all_urls = batch_urls + args
178     all_urls = [url.strip() for url in all_urls]
179     _enc = preferredencoding()
180     all_urls = [url.decode(_enc, 'ignore') if isinstance(url, bytes) else url for url in all_urls]
181
182     extractors = gen_extractors()
183
184     if opts.list_extractors:
185         for ie in sorted(extractors, key=lambda ie: ie.IE_NAME.lower()):
186             compat_print(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else ''))
187             matchedUrls = [url for url in all_urls if ie.suitable(url)]
188             for mu in matchedUrls:
189                 compat_print(u'  ' + mu)
190         sys.exit(0)
191     if opts.list_extractor_descriptions:
192         for ie in sorted(extractors, key=lambda ie: ie.IE_NAME.lower()):
193             if not ie._WORKING:
194                 continue
195             desc = getattr(ie, 'IE_DESC', ie.IE_NAME)
196             if desc is False:
197                 continue
198             if hasattr(ie, 'SEARCH_KEY'):
199                 _SEARCHES = (u'cute kittens', u'slithering pythons', u'falling cat', u'angry poodle', u'purple fish', u'running tortoise', u'sleeping bunny')
200                 _COUNTS = (u'', u'5', u'10', u'all')
201                 desc += u' (Example: "%s%s:%s" )' % (ie.SEARCH_KEY, random.choice(_COUNTS), random.choice(_SEARCHES))
202             compat_print(desc)
203         sys.exit(0)
204
205
206     # Conflicting, missing and erroneous options
207     if opts.usenetrc and (opts.username is not None or opts.password is not None):
208         parser.error(u'using .netrc conflicts with giving username/password')
209     if opts.password is not None and opts.username is None:
210         parser.error(u'account username missing\n')
211     if opts.outtmpl is not None and (opts.usetitle or opts.autonumber or opts.useid):
212         parser.error(u'using output template conflicts with using title, video ID or auto number')
213     if opts.usetitle and opts.useid:
214         parser.error(u'using title conflicts with using video ID')
215     if opts.username is not None and opts.password is None:
216         opts.password = compat_getpass(u'Type account password and press [Return]: ')
217     if opts.ratelimit is not None:
218         numeric_limit = FileDownloader.parse_bytes(opts.ratelimit)
219         if numeric_limit is None:
220             parser.error(u'invalid rate limit specified')
221         opts.ratelimit = numeric_limit
222     if opts.min_filesize is not None:
223         numeric_limit = FileDownloader.parse_bytes(opts.min_filesize)
224         if numeric_limit is None:
225             parser.error(u'invalid min_filesize specified')
226         opts.min_filesize = numeric_limit
227     if opts.max_filesize is not None:
228         numeric_limit = FileDownloader.parse_bytes(opts.max_filesize)
229         if numeric_limit is None:
230             parser.error(u'invalid max_filesize specified')
231         opts.max_filesize = numeric_limit
232     if opts.retries is not None:
233         try:
234             opts.retries = int(opts.retries)
235         except (TypeError, ValueError):
236             parser.error(u'invalid retry count specified')
237     if opts.buffersize is not None:
238         numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize)
239         if numeric_buffersize is None:
240             parser.error(u'invalid buffer size specified')
241         opts.buffersize = numeric_buffersize
242     if opts.playliststart <= 0:
243         raise ValueError(u'Playlist start must be positive')
244     if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
245         raise ValueError(u'Playlist end must be greater than playlist start')
246     if opts.extractaudio:
247         if opts.audioformat not in ['best', 'aac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']:
248             parser.error(u'invalid audio format specified')
249     if opts.audioquality:
250         opts.audioquality = opts.audioquality.strip('k').strip('K')
251         if not opts.audioquality.isdigit():
252             parser.error(u'invalid audio quality specified')
253     if opts.recodevideo is not None:
254         if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg', 'mkv']:
255             parser.error(u'invalid video recode format specified')
256     if opts.date is not None:
257         date = DateRange.day(opts.date)
258     else:
259         date = DateRange(opts.dateafter, opts.datebefore)
260
261     # Do not download videos when there are audio-only formats
262     if opts.extractaudio and not opts.keepvideo and opts.format is None:
263         opts.format = 'bestaudio/best'
264
265     # --all-sub automatically sets --write-sub if --write-auto-sub is not given
266     # this was the old behaviour if only --all-sub was given.
267     if opts.allsubtitles and (opts.writeautomaticsub == False):
268         opts.writesubtitles = True
269
270     if sys.version_info < (3,):
271         # In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
272         if opts.outtmpl is not None:
273             opts.outtmpl = opts.outtmpl.decode(preferredencoding())
274     outtmpl =((opts.outtmpl is not None and opts.outtmpl)
275             or (opts.format == '-1' and opts.usetitle and u'%(title)s-%(id)s-%(format)s.%(ext)s')
276             or (opts.format == '-1' and u'%(id)s-%(format)s.%(ext)s')
277             or (opts.usetitle and opts.autonumber and u'%(autonumber)s-%(title)s-%(id)s.%(ext)s')
278             or (opts.usetitle and u'%(title)s-%(id)s.%(ext)s')
279             or (opts.useid and u'%(id)s.%(ext)s')
280             or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s')
281             or DEFAULT_OUTTMPL)
282     if not os.path.splitext(outtmpl)[1] and opts.extractaudio:
283         parser.error(u'Cannot download a video and extract audio into the same'
284                      u' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
285                      u' template'.format(outtmpl))
286
287     any_printing = opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration or opts.dumpjson
288     download_archive_fn = os.path.expanduser(opts.download_archive) if opts.download_archive is not None else opts.download_archive
289
290     ydl_opts = {
291         'usenetrc': opts.usenetrc,
292         'username': opts.username,
293         'password': opts.password,
294         'twofactor': opts.twofactor,
295         'videopassword': opts.videopassword,
296         'quiet': (opts.quiet or any_printing),
297         'no_warnings': opts.no_warnings,
298         'forceurl': opts.geturl,
299         'forcetitle': opts.gettitle,
300         'forceid': opts.getid,
301         'forcethumbnail': opts.getthumbnail,
302         'forcedescription': opts.getdescription,
303         'forceduration': opts.getduration,
304         'forcefilename': opts.getfilename,
305         'forceformat': opts.getformat,
306         'forcejson': opts.dumpjson,
307         'simulate': opts.simulate,
308         'skip_download': (opts.skip_download or opts.simulate or any_printing),
309         'format': opts.format,
310         'format_limit': opts.format_limit,
311         'listformats': opts.listformats,
312         'outtmpl': outtmpl,
313         'autonumber_size': opts.autonumber_size,
314         'restrictfilenames': opts.restrictfilenames,
315         'ignoreerrors': opts.ignoreerrors,
316         'ratelimit': opts.ratelimit,
317         'nooverwrites': opts.nooverwrites,
318         'retries': opts.retries,
319         'buffersize': opts.buffersize,
320         'noresizebuffer': opts.noresizebuffer,
321         'continuedl': opts.continue_dl,
322         'noprogress': opts.noprogress,
323         'progress_with_newline': opts.progress_with_newline,
324         'playliststart': opts.playliststart,
325         'playlistend': opts.playlistend,
326         'noplaylist': opts.noplaylist,
327         'logtostderr': opts.outtmpl == '-',
328         'consoletitle': opts.consoletitle,
329         'nopart': opts.nopart,
330         'updatetime': opts.updatetime,
331         'writedescription': opts.writedescription,
332         'writeannotations': opts.writeannotations,
333         'writeinfojson': opts.writeinfojson,
334         'writethumbnail': opts.writethumbnail,
335         'writesubtitles': opts.writesubtitles,
336         'writeautomaticsub': opts.writeautomaticsub,
337         'allsubtitles': opts.allsubtitles,
338         'listsubtitles': opts.listsubtitles,
339         'subtitlesformat': opts.subtitlesformat,
340         'subtitleslangs': opts.subtitleslangs,
341         'matchtitle': decodeOption(opts.matchtitle),
342         'rejecttitle': decodeOption(opts.rejecttitle),
343         'max_downloads': opts.max_downloads,
344         'prefer_free_formats': opts.prefer_free_formats,
345         'verbose': opts.verbose,
346         'dump_intermediate_pages': opts.dump_intermediate_pages,
347         'write_pages': opts.write_pages,
348         'test': opts.test,
349         'keepvideo': opts.keepvideo,
350         'min_filesize': opts.min_filesize,
351         'max_filesize': opts.max_filesize,
352         'min_views': opts.min_views,
353         'max_views': opts.max_views,
354         'daterange': date,
355         'cachedir': opts.cachedir,
356         'youtube_print_sig_code': opts.youtube_print_sig_code,
357         'age_limit': opts.age_limit,
358         'download_archive': download_archive_fn,
359         'cookiefile': opts.cookiefile,
360         'nocheckcertificate': opts.no_check_certificate,
361         'prefer_insecure': opts.prefer_insecure,
362         'proxy': opts.proxy,
363         'socket_timeout': opts.socket_timeout,
364         'bidi_workaround': opts.bidi_workaround,
365         'debug_printtraffic': opts.debug_printtraffic,
366         'prefer_ffmpeg': opts.prefer_ffmpeg,
367         'include_ads': opts.include_ads,
368         'default_search': opts.default_search,
369         'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
370         'encoding': opts.encoding,
371         'exec_cmd': opts.exec_cmd,
372         'extract_flat': opts.extract_flat,
373     }
374
375     with YoutubeDL(ydl_opts) as ydl:
376         ydl.print_debug_header()
377         ydl.add_default_info_extractors()
378
379         # PostProcessors
380         # Add the metadata pp first, the other pps will copy it
381         if opts.addmetadata:
382             ydl.add_post_processor(FFmpegMetadataPP())
383         if opts.extractaudio:
384             ydl.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, nopostoverwrites=opts.nopostoverwrites))
385         if opts.recodevideo:
386             ydl.add_post_processor(FFmpegVideoConvertor(preferedformat=opts.recodevideo))
387         if opts.embedsubtitles:
388             ydl.add_post_processor(FFmpegEmbedSubtitlePP(subtitlesformat=opts.subtitlesformat))
389         if opts.xattrs:
390             ydl.add_post_processor(XAttrMetadataPP())
391         if opts.embedthumbnail:
392             if not opts.addmetadata:
393                 ydl.add_post_processor(FFmpegAudioFixPP())
394             ydl.add_post_processor(AtomicParsleyPP())
395
396
397         # Please keep ExecAfterDownload towards the bottom as it allows the user to modify the final file in any way.
398         # So if the user is able to remove the file before your postprocessor runs it might cause a few problems.
399         if opts.exec_cmd:
400             ydl.add_post_processor(ExecAfterDownloadPP(
401                 verboseOutput=opts.verbose, exec_cmd=opts.exec_cmd))
402
403         # Update version
404         if opts.update_self:
405             update_self(ydl.to_screen, opts.verbose)
406
407         # Remove cache dir
408         if opts.rm_cachedir:
409             ydl.cache.remove()
410
411         # Maybe do nothing
412         if (len(all_urls) < 1) and (opts.load_info_filename is None):
413             if not (opts.update_self or opts.rm_cachedir):
414                 parser.error(u'you must provide at least one URL')
415             else:
416                 sys.exit()
417
418         try:
419             if opts.load_info_filename is not None:
420                 retcode = ydl.download_with_info_file(opts.load_info_filename)
421             else:
422                 retcode = ydl.download(all_urls)
423         except MaxDownloadsReached:
424             ydl.to_screen(u'--max-download limit reached, aborting.')
425             retcode = 101
426
427     sys.exit(retcode)
428
429
430 def main(argv=None):
431     try:
432         _real_main(argv)
433     except DownloadError:
434         sys.exit(1)
435     except SameFileError:
436         sys.exit(u'ERROR: fixed output name but more than one file to download')
437     except KeyboardInterrupt:
438         sys.exit(u'\nERROR: Interrupted by user')