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