Credit @m5moufl for behindkink (#3740)
[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 )
82
83 __license__ = 'Public Domain'
84
85 import codecs
86 import io
87 import os
88 import random
89 import sys
90
91
92 from .options import (
93     parseOpts,
94 )
95 from .utils import (
96     compat_getpass,
97     compat_print,
98     DateRange,
99     DEFAULT_OUTTMPL,
100     decodeOption,
101     DownloadError,
102     MaxDownloadsReached,
103     preferredencoding,
104     read_batch_urls,
105     SameFileError,
106     setproctitle,
107     std_headers,
108     write_string,
109 )
110 from .update import update_self
111 from .downloader import (
112     FileDownloader,
113 )
114 from .extractor import gen_extractors
115 from .YoutubeDL import YoutubeDL
116 from .postprocessor import (
117     AtomicParsleyPP,
118     FFmpegAudioFixPP,
119     FFmpegMetadataPP,
120     FFmpegVideoConvertor,
121     FFmpegExtractAudioPP,
122     FFmpegEmbedSubtitlePP,
123     XAttrMetadataPP,
124     ExecAfterDownloadPP,
125 )
126
127
128 def _real_main(argv=None):
129     # Compatibility fixes for Windows
130     if sys.platform == 'win32':
131         # https://github.com/rg3/youtube-dl/issues/820
132         codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
133
134     setproctitle(u'youtube-dl')
135
136     parser, opts, args = parseOpts(argv)
137
138     # Set user agent
139     if opts.user_agent is not None:
140         std_headers['User-Agent'] = opts.user_agent
141
142     # Set referer
143     if opts.referer is not None:
144         std_headers['Referer'] = opts.referer
145
146     # Custom HTTP headers
147     if opts.headers is not None:
148         for h in opts.headers:
149             if h.find(':', 1) < 0:
150                 parser.error(u'wrong header formatting, it should be key:value, not "%s"'%h)
151             key, value = h.split(':', 2)
152             if opts.verbose:
153                 write_string(u'[debug] Adding header from command line option %s:%s\n'%(key, value))
154             std_headers[key] = value
155
156     # Dump user agent
157     if opts.dump_user_agent:
158         compat_print(std_headers['User-Agent'])
159         sys.exit(0)
160
161     # Batch file verification
162     batch_urls = []
163     if opts.batchfile is not None:
164         try:
165             if opts.batchfile == '-':
166                 batchfd = sys.stdin
167             else:
168                 batchfd = io.open(opts.batchfile, 'r', encoding='utf-8', errors='ignore')
169             batch_urls = read_batch_urls(batchfd)
170             if opts.verbose:
171                 write_string(u'[debug] Batch file urls: ' + repr(batch_urls) + u'\n')
172         except IOError:
173             sys.exit(u'ERROR: batch file could not be read')
174     all_urls = batch_urls + args
175     all_urls = [url.strip() for url in all_urls]
176     _enc = preferredencoding()
177     all_urls = [url.decode(_enc, 'ignore') if isinstance(url, bytes) else url for url in all_urls]
178
179     extractors = gen_extractors()
180
181     if opts.list_extractors:
182         for ie in sorted(extractors, key=lambda ie: ie.IE_NAME.lower()):
183             compat_print(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else ''))
184             matchedUrls = [url for url in all_urls if ie.suitable(url)]
185             for mu in matchedUrls:
186                 compat_print(u'  ' + mu)
187         sys.exit(0)
188     if opts.list_extractor_descriptions:
189         for ie in sorted(extractors, key=lambda ie: ie.IE_NAME.lower()):
190             if not ie._WORKING:
191                 continue
192             desc = getattr(ie, 'IE_DESC', ie.IE_NAME)
193             if desc is False:
194                 continue
195             if hasattr(ie, 'SEARCH_KEY'):
196                 _SEARCHES = (u'cute kittens', u'slithering pythons', u'falling cat', u'angry poodle', u'purple fish', u'running tortoise', u'sleeping bunny')
197                 _COUNTS = (u'', u'5', u'10', u'all')
198                 desc += u' (Example: "%s%s:%s" )' % (ie.SEARCH_KEY, random.choice(_COUNTS), random.choice(_SEARCHES))
199             compat_print(desc)
200         sys.exit(0)
201
202
203     # Conflicting, missing and erroneous options
204     if opts.usenetrc and (opts.username is not None or opts.password is not None):
205         parser.error(u'using .netrc conflicts with giving username/password')
206     if opts.password is not None and opts.username is None:
207         parser.error(u'account username missing\n')
208     if opts.outtmpl is not None and (opts.usetitle or opts.autonumber or opts.useid):
209         parser.error(u'using output template conflicts with using title, video ID or auto number')
210     if opts.usetitle and opts.useid:
211         parser.error(u'using title conflicts with using video ID')
212     if opts.username is not None and opts.password is None:
213         opts.password = compat_getpass(u'Type account password and press [Return]: ')
214     if opts.ratelimit is not None:
215         numeric_limit = FileDownloader.parse_bytes(opts.ratelimit)
216         if numeric_limit is None:
217             parser.error(u'invalid rate limit specified')
218         opts.ratelimit = numeric_limit
219     if opts.min_filesize is not None:
220         numeric_limit = FileDownloader.parse_bytes(opts.min_filesize)
221         if numeric_limit is None:
222             parser.error(u'invalid min_filesize specified')
223         opts.min_filesize = numeric_limit
224     if opts.max_filesize is not None:
225         numeric_limit = FileDownloader.parse_bytes(opts.max_filesize)
226         if numeric_limit is None:
227             parser.error(u'invalid max_filesize specified')
228         opts.max_filesize = numeric_limit
229     if opts.retries is not None:
230         try:
231             opts.retries = int(opts.retries)
232         except (TypeError, ValueError):
233             parser.error(u'invalid retry count specified')
234     if opts.buffersize is not None:
235         numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize)
236         if numeric_buffersize is None:
237             parser.error(u'invalid buffer size specified')
238         opts.buffersize = numeric_buffersize
239     if opts.playliststart <= 0:
240         raise ValueError(u'Playlist start must be positive')
241     if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
242         raise ValueError(u'Playlist end must be greater than playlist start')
243     if opts.extractaudio:
244         if opts.audioformat not in ['best', 'aac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']:
245             parser.error(u'invalid audio format specified')
246     if opts.audioquality:
247         opts.audioquality = opts.audioquality.strip('k').strip('K')
248         if not opts.audioquality.isdigit():
249             parser.error(u'invalid audio quality specified')
250     if opts.recodevideo is not None:
251         if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg', 'mkv']:
252             parser.error(u'invalid video recode format specified')
253     if opts.date is not None:
254         date = DateRange.day(opts.date)
255     else:
256         date = DateRange(opts.dateafter, opts.datebefore)
257     if opts.default_search not in ('auto', 'auto_warning', 'error', 'fixup_error', None) and ':' not in opts.default_search:
258         parser.error(u'--default-search invalid; did you forget a colon (:) at the end?')
259
260     # Do not download videos when there are audio-only formats
261     if opts.extractaudio and not opts.keepvideo and opts.format is None:
262         opts.format = 'bestaudio/best'
263
264     # --all-sub automatically sets --write-sub if --write-auto-sub is not given
265     # this was the old behaviour if only --all-sub was given.
266     if opts.allsubtitles and (opts.writeautomaticsub == False):
267         opts.writesubtitles = True
268
269     if sys.version_info < (3,):
270         # In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
271         if opts.outtmpl is not None:
272             opts.outtmpl = opts.outtmpl.decode(preferredencoding())
273     outtmpl =((opts.outtmpl is not None and opts.outtmpl)
274             or (opts.format == '-1' and opts.usetitle and u'%(title)s-%(id)s-%(format)s.%(ext)s')
275             or (opts.format == '-1' and u'%(id)s-%(format)s.%(ext)s')
276             or (opts.usetitle and opts.autonumber and u'%(autonumber)s-%(title)s-%(id)s.%(ext)s')
277             or (opts.usetitle and u'%(title)s-%(id)s.%(ext)s')
278             or (opts.useid and u'%(id)s.%(ext)s')
279             or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s')
280             or DEFAULT_OUTTMPL)
281     if not os.path.splitext(outtmpl)[1] and opts.extractaudio:
282         parser.error(u'Cannot download a video and extract audio into the same'
283                      u' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
284                      u' template'.format(outtmpl))
285
286     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
287     download_archive_fn = os.path.expanduser(opts.download_archive) if opts.download_archive is not None else opts.download_archive
288
289     ydl_opts = {
290         'usenetrc': opts.usenetrc,
291         'username': opts.username,
292         'password': opts.password,
293         'twofactor': opts.twofactor,
294         'videopassword': opts.videopassword,
295         'quiet': (opts.quiet or any_printing),
296         'no_warnings': opts.no_warnings,
297         'forceurl': opts.geturl,
298         'forcetitle': opts.gettitle,
299         'forceid': opts.getid,
300         'forcethumbnail': opts.getthumbnail,
301         'forcedescription': opts.getdescription,
302         'forceduration': opts.getduration,
303         'forcefilename': opts.getfilename,
304         'forceformat': opts.getformat,
305         'forcejson': opts.dumpjson,
306         'simulate': opts.simulate,
307         'skip_download': (opts.skip_download or opts.simulate or any_printing),
308         'format': opts.format,
309         'format_limit': opts.format_limit,
310         'listformats': opts.listformats,
311         'outtmpl': outtmpl,
312         'autonumber_size': opts.autonumber_size,
313         'restrictfilenames': opts.restrictfilenames,
314         'ignoreerrors': opts.ignoreerrors,
315         'ratelimit': opts.ratelimit,
316         'nooverwrites': opts.nooverwrites,
317         'retries': opts.retries,
318         'buffersize': opts.buffersize,
319         'noresizebuffer': opts.noresizebuffer,
320         'continuedl': opts.continue_dl,
321         'noprogress': opts.noprogress,
322         'progress_with_newline': opts.progress_with_newline,
323         'playliststart': opts.playliststart,
324         'playlistend': opts.playlistend,
325         'noplaylist': opts.noplaylist,
326         'logtostderr': opts.outtmpl == '-',
327         'consoletitle': opts.consoletitle,
328         'nopart': opts.nopart,
329         'updatetime': opts.updatetime,
330         'writedescription': opts.writedescription,
331         'writeannotations': opts.writeannotations,
332         'writeinfojson': opts.writeinfojson,
333         'writethumbnail': opts.writethumbnail,
334         'writesubtitles': opts.writesubtitles,
335         'writeautomaticsub': opts.writeautomaticsub,
336         'allsubtitles': opts.allsubtitles,
337         'listsubtitles': opts.listsubtitles,
338         'subtitlesformat': opts.subtitlesformat,
339         'subtitleslangs': opts.subtitleslangs,
340         'matchtitle': decodeOption(opts.matchtitle),
341         'rejecttitle': decodeOption(opts.rejecttitle),
342         'max_downloads': opts.max_downloads,
343         'prefer_free_formats': opts.prefer_free_formats,
344         'verbose': opts.verbose,
345         'dump_intermediate_pages': opts.dump_intermediate_pages,
346         'write_pages': opts.write_pages,
347         'test': opts.test,
348         'keepvideo': opts.keepvideo,
349         'min_filesize': opts.min_filesize,
350         'max_filesize': opts.max_filesize,
351         'min_views': opts.min_views,
352         'max_views': opts.max_views,
353         'daterange': date,
354         'cachedir': opts.cachedir,
355         'youtube_print_sig_code': opts.youtube_print_sig_code,
356         'age_limit': opts.age_limit,
357         'download_archive': download_archive_fn,
358         'cookiefile': opts.cookiefile,
359         'nocheckcertificate': opts.no_check_certificate,
360         'prefer_insecure': opts.prefer_insecure,
361         'proxy': opts.proxy,
362         'socket_timeout': opts.socket_timeout,
363         'bidi_workaround': opts.bidi_workaround,
364         'debug_printtraffic': opts.debug_printtraffic,
365         'prefer_ffmpeg': opts.prefer_ffmpeg,
366         'include_ads': opts.include_ads,
367         'default_search': opts.default_search,
368         'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
369         'encoding': opts.encoding,
370         'exec_cmd': opts.exec_cmd,
371     }
372
373     with YoutubeDL(ydl_opts) as ydl:
374         ydl.print_debug_header()
375         ydl.add_default_info_extractors()
376
377         # PostProcessors
378         # Add the metadata pp first, the other pps will copy it
379         if opts.addmetadata:
380             ydl.add_post_processor(FFmpegMetadataPP())
381         if opts.extractaudio:
382             ydl.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, nopostoverwrites=opts.nopostoverwrites))
383         if opts.recodevideo:
384             ydl.add_post_processor(FFmpegVideoConvertor(preferedformat=opts.recodevideo))
385         if opts.embedsubtitles:
386             ydl.add_post_processor(FFmpegEmbedSubtitlePP(subtitlesformat=opts.subtitlesformat))
387         if opts.xattrs:
388             ydl.add_post_processor(XAttrMetadataPP())
389         if opts.embedthumbnail:
390             if not opts.addmetadata:
391                 ydl.add_post_processor(FFmpegAudioFixPP())
392             ydl.add_post_processor(AtomicParsleyPP())
393
394
395         # Please keep ExecAfterDownload towards the bottom as it allows the user to modify the final file in any way.
396         # So if the user is able to remove the file before your postprocessor runs it might cause a few problems.
397         if opts.exec_cmd:
398             ydl.add_post_processor(ExecAfterDownloadPP(
399                 verboseOutput=opts.verbose, exec_cmd=opts.exec_cmd))
400
401         # Update version
402         if opts.update_self:
403             update_self(ydl.to_screen, opts.verbose)
404
405         # Remove cache dir
406         if opts.rm_cachedir:
407             ydl.cache.remove()
408
409         # Maybe do nothing
410         if (len(all_urls) < 1) and (opts.load_info_filename is None):
411             if not (opts.update_self or opts.rm_cachedir):
412                 parser.error(u'you must provide at least one URL')
413             else:
414                 sys.exit()
415
416         try:
417             if opts.load_info_filename is not None:
418                 retcode = ydl.download_with_info_file(opts.load_info_filename)
419             else:
420                 retcode = ydl.download(all_urls)
421         except MaxDownloadsReached:
422             ydl.to_screen(u'--max-download limit reached, aborting.')
423             retcode = 101
424
425     sys.exit(retcode)
426
427
428 def main(argv=None):
429     try:
430         _real_main(argv)
431     except DownloadError:
432         sys.exit(1)
433     except SameFileError:
434         sys.exit(u'ERROR: fixed output name but more than one file to download')
435     except KeyboardInterrupt:
436         sys.exit(u'\nERROR: Interrupted by user')