Fix --match-title and --reject-title decoding (Closes #690)
[youtube-dl] / youtube_dl / __init__.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from __future__ import with_statement
5 from __future__ import absolute_import
6
7 __authors__  = (
8     'Ricardo Garcia Gonzalez',
9     'Danny Colligan',
10     'Benjamin Johnson',
11     'Vasyl\' Vavrychuk',
12     'Witold Baryluk',
13     'Paweł Paprota',
14     'Gergely Imreh',
15     'Rogério Brito',
16     'Philipp Hagemeister',
17     'Sören Schulze',
18     'Kevin Ngo',
19     'Ori Avtalion',
20     'shizeeg',
21     'Filippo Valsorda',
22     'Christian Albrecht',
23     'Dave Vasilevsky',
24     'Jaime Marquínez Ferrándiz',
25     'Jeff Crouse',
26     'Osama Khalid',
27     )
28
29 __license__ = 'Public Domain'
30
31 import getpass
32 import optparse
33 import os
34 import re
35 import shlex
36 import socket
37 import subprocess
38 import sys
39 import warnings
40 import platform
41
42 from .utils import *
43 from .update import update_self
44 from .version import __version__
45 from .FileDownloader import *
46 from .InfoExtractors import gen_extractors
47 from .PostProcessor import *
48
49 def parseOpts():
50     def _readOptions(filename_bytes):
51         try:
52             optionf = open(filename_bytes)
53         except IOError:
54             return [] # silently skip if file is not present
55         try:
56             res = []
57             for l in optionf:
58                 res += shlex.split(l, comments=True)
59         finally:
60             optionf.close()
61         return res
62
63     def _format_option_string(option):
64         ''' ('-o', '--option') -> -o, --format METAVAR'''
65
66         opts = []
67
68         if option._short_opts:
69             opts.append(option._short_opts[0])
70         if option._long_opts:
71             opts.append(option._long_opts[0])
72         if len(opts) > 1:
73             opts.insert(1, ', ')
74
75         if option.takes_value(): opts.append(' %s' % option.metavar)
76
77         return "".join(opts)
78
79     def _find_term_columns():
80         columns = os.environ.get('COLUMNS', None)
81         if columns:
82             return int(columns)
83
84         try:
85             sp = subprocess.Popen(['stty', 'size'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
86             out,err = sp.communicate()
87             return int(out.split()[1])
88         except:
89             pass
90         return None
91
92     max_width = 80
93     max_help_position = 80
94
95     # No need to wrap help messages if we're on a wide console
96     columns = _find_term_columns()
97     if columns: max_width = columns
98
99     fmt = optparse.IndentedHelpFormatter(width=max_width, max_help_position=max_help_position)
100     fmt.format_option_strings = _format_option_string
101
102     kw = {
103         'version'   : __version__,
104         'formatter' : fmt,
105         'usage' : '%prog [options] url [url...]',
106         'conflict_handler' : 'resolve',
107     }
108
109     parser = optparse.OptionParser(**kw)
110
111     # option groups
112     general        = optparse.OptionGroup(parser, 'General Options')
113     selection      = optparse.OptionGroup(parser, 'Video Selection')
114     authentication = optparse.OptionGroup(parser, 'Authentication Options')
115     video_format   = optparse.OptionGroup(parser, 'Video Format Options')
116     postproc       = optparse.OptionGroup(parser, 'Post-processing Options')
117     filesystem     = optparse.OptionGroup(parser, 'Filesystem Options')
118     verbosity      = optparse.OptionGroup(parser, 'Verbosity / Simulation Options')
119
120     general.add_option('-h', '--help',
121             action='help', help='print this help text and exit')
122     general.add_option('-v', '--version',
123             action='version', help='print program version and exit')
124     general.add_option('-U', '--update',
125             action='store_true', dest='update_self', help='update this program to latest version')
126     general.add_option('-i', '--ignore-errors',
127             action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
128     general.add_option('-r', '--rate-limit',
129             dest='ratelimit', metavar='LIMIT', help='download rate limit (e.g. 50k or 44.6m)')
130     general.add_option('-R', '--retries',
131             dest='retries', metavar='RETRIES', help='number of retries (default is %default)', default=10)
132     general.add_option('--buffer-size',
133             dest='buffersize', metavar='SIZE', help='size of download buffer (e.g. 1024 or 16k) (default is %default)', default="1024")
134     general.add_option('--no-resize-buffer',
135             action='store_true', dest='noresizebuffer',
136             help='do not automatically adjust the buffer size. By default, the buffer size is automatically resized from an initial value of SIZE.', default=False)
137     general.add_option('--dump-user-agent',
138             action='store_true', dest='dump_user_agent',
139             help='display the current browser identification', default=False)
140     general.add_option('--user-agent',
141             dest='user_agent', help='specify a custom user agent', metavar='UA')
142     general.add_option('--list-extractors',
143             action='store_true', dest='list_extractors',
144             help='List all supported extractors and the URLs they would handle', default=False)
145     general.add_option('--test', action='store_true', dest='test', default=False, help=optparse.SUPPRESS_HELP)
146
147     selection.add_option('--playlist-start',
148             dest='playliststart', metavar='NUMBER', help='playlist video to start at (default is %default)', default=1)
149     selection.add_option('--playlist-end',
150             dest='playlistend', metavar='NUMBER', help='playlist video to end at (default is last)', default=-1)
151     selection.add_option('--match-title', dest='matchtitle', metavar='REGEX',help='download only matching titles (regex or caseless sub-string)')
152     selection.add_option('--reject-title', dest='rejecttitle', metavar='REGEX',help='skip download for matching titles (regex or caseless sub-string)')
153     selection.add_option('--max-downloads', metavar='NUMBER', dest='max_downloads', help='Abort after downloading NUMBER files', default=None)
154     selection.add_option('--min-filesize', metavar='SIZE', dest='min_filesize', help="Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)", default=None)
155     selection.add_option('--max-filesize', metavar='SIZE', dest='max_filesize', help="Do not download any videos larger than SIZE (e.g. 50k or 44.6m)", default=None)
156
157
158     authentication.add_option('-u', '--username',
159             dest='username', metavar='USERNAME', help='account username')
160     authentication.add_option('-p', '--password',
161             dest='password', metavar='PASSWORD', help='account password')
162     authentication.add_option('-n', '--netrc',
163             action='store_true', dest='usenetrc', help='use .netrc authentication data', default=False)
164
165
166     video_format.add_option('-f', '--format',
167             action='store', dest='format', metavar='FORMAT', help='video format code')
168     video_format.add_option('--all-formats',
169             action='store_const', dest='format', help='download all available video formats', const='all')
170     video_format.add_option('--prefer-free-formats',
171             action='store_true', dest='prefer_free_formats', default=False, help='prefer free video formats unless a specific one is requested')
172     video_format.add_option('--max-quality',
173             action='store', dest='format_limit', metavar='FORMAT', help='highest quality format to download')
174     video_format.add_option('-F', '--list-formats',
175             action='store_true', dest='listformats', help='list all available formats (currently youtube only)')
176     video_format.add_option('--write-srt',
177             action='store_true', dest='writesubtitles',
178             help='write video closed captions to a .srt file (currently youtube only)', default=False)
179     video_format.add_option('--srt-lang',
180             action='store', dest='subtitleslang', metavar='LANG',
181             help='language of the closed captions to download (optional) use IETF language tags like \'en\'')
182
183     verbosity.add_option('-q', '--quiet',
184             action='store_true', dest='quiet', help='activates quiet mode', default=False)
185     verbosity.add_option('-s', '--simulate',
186             action='store_true', dest='simulate', help='do not download the video and do not write anything to disk', default=False)
187     verbosity.add_option('--skip-download',
188             action='store_true', dest='skip_download', help='do not download the video', default=False)
189     verbosity.add_option('-g', '--get-url',
190             action='store_true', dest='geturl', help='simulate, quiet but print URL', default=False)
191     verbosity.add_option('-e', '--get-title',
192             action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
193     verbosity.add_option('--get-thumbnail',
194             action='store_true', dest='getthumbnail',
195             help='simulate, quiet but print thumbnail URL', default=False)
196     verbosity.add_option('--get-description',
197             action='store_true', dest='getdescription',
198             help='simulate, quiet but print video description', default=False)
199     verbosity.add_option('--get-filename',
200             action='store_true', dest='getfilename',
201             help='simulate, quiet but print output filename', default=False)
202     verbosity.add_option('--get-format',
203             action='store_true', dest='getformat',
204             help='simulate, quiet but print output format', default=False)
205     verbosity.add_option('--newline',
206             action='store_true', dest='progress_with_newline', help='output progress bar as new lines', default=False)
207     verbosity.add_option('--no-progress',
208             action='store_true', dest='noprogress', help='do not print progress bar', default=False)
209     verbosity.add_option('--console-title',
210             action='store_true', dest='consoletitle',
211             help='display progress in console titlebar', default=False)
212     verbosity.add_option('-v', '--verbose',
213             action='store_true', dest='verbose', help='print various debugging information', default=False)
214
215     filesystem.add_option('-t', '--title',
216             action='store_true', dest='usetitle', help='use title in file name', default=False)
217     filesystem.add_option('--id',
218             action='store_true', dest='useid', help='use video ID in file name', default=False)
219     filesystem.add_option('-l', '--literal',
220             action='store_true', dest='usetitle', help='[deprecated] alias of --title', default=False)
221     filesystem.add_option('-A', '--auto-number',
222             action='store_true', dest='autonumber',
223             help='number downloaded files starting from 00000', default=False)
224     filesystem.add_option('-o', '--output',
225             dest='outtmpl', metavar='TEMPLATE', help='output filename template. Use %(title)s to get the title, %(uploader)s for the uploader name, %(uploader_id)s for the uploader nickname if different, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id and %% for a literal percent. Use - to output to stdout. Can also be used to download to a different directory, for example with -o \'/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s\' .')
226     filesystem.add_option('--restrict-filenames',
227             action='store_true', dest='restrictfilenames',
228             help='Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames', default=False)
229     filesystem.add_option('-a', '--batch-file',
230             dest='batchfile', metavar='FILE', help='file containing URLs to download (\'-\' for stdin)')
231     filesystem.add_option('-w', '--no-overwrites',
232             action='store_true', dest='nooverwrites', help='do not overwrite files', default=False)
233     filesystem.add_option('-c', '--continue',
234             action='store_true', dest='continue_dl', help='resume partially downloaded files', default=True)
235     filesystem.add_option('--no-continue',
236             action='store_false', dest='continue_dl',
237             help='do not resume partially downloaded files (restart from beginning)')
238     filesystem.add_option('--cookies',
239             dest='cookiefile', metavar='FILE', help='file to read cookies from and dump cookie jar in')
240     filesystem.add_option('--no-part',
241             action='store_true', dest='nopart', help='do not use .part files', default=False)
242     filesystem.add_option('--no-mtime',
243             action='store_false', dest='updatetime',
244             help='do not use the Last-modified header to set the file modification time', default=True)
245     filesystem.add_option('--write-description',
246             action='store_true', dest='writedescription',
247             help='write video description to a .description file', default=False)
248     filesystem.add_option('--write-info-json',
249             action='store_true', dest='writeinfojson',
250             help='write video metadata to a .info.json file', default=False)
251
252
253     postproc.add_option('-x', '--extract-audio', action='store_true', dest='extractaudio', default=False,
254             help='convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe)')
255     postproc.add_option('--audio-format', metavar='FORMAT', dest='audioformat', default='best',
256             help='"best", "aac", "vorbis", "mp3", "m4a", "opus", or "wav"; best by default')
257     postproc.add_option('--audio-quality', metavar='QUALITY', dest='audioquality', default='5',
258             help='ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default 5)')
259     postproc.add_option('--recode-video', metavar='FORMAT', dest='recodevideo', default=None,
260             help='Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm)')
261     postproc.add_option('-k', '--keep-video', action='store_true', dest='keepvideo', default=False,
262             help='keeps the video file on disk after the post-processing; the video is erased by default')
263     postproc.add_option('--no-post-overwrites', action='store_true', dest='nopostoverwrites', default=False,
264             help='do not overwrite post-processed files; the post-processed files are overwritten by default')
265
266
267     parser.add_option_group(general)
268     parser.add_option_group(selection)
269     parser.add_option_group(filesystem)
270     parser.add_option_group(verbosity)
271     parser.add_option_group(video_format)
272     parser.add_option_group(authentication)
273     parser.add_option_group(postproc)
274
275     xdg_config_home = os.environ.get('XDG_CONFIG_HOME')
276     if xdg_config_home:
277         userConf = os.path.join(xdg_config_home, 'youtube-dl.conf')
278     else:
279         userConf = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf')
280     argv = _readOptions('/etc/youtube-dl.conf') + _readOptions(userConf) + sys.argv[1:]
281     opts, args = parser.parse_args(argv)
282
283     return parser, opts, args
284
285 def _real_main():
286     parser, opts, args = parseOpts()
287
288     # Open appropriate CookieJar
289     if opts.cookiefile is None:
290         jar = compat_cookiejar.CookieJar()
291     else:
292         try:
293             jar = compat_cookiejar.MozillaCookieJar(opts.cookiefile)
294             if os.access(opts.cookiefile, os.R_OK):
295                 jar.load()
296         except (IOError, OSError) as err:
297             if opts.verbose:
298                 traceback.print_exc()
299             sys.stderr.write(u'ERROR: unable to open cookie file\n')
300             sys.exit(101)
301     # Set user agent
302     if opts.user_agent is not None:
303         std_headers['User-Agent'] = opts.user_agent
304
305     # Dump user agent
306     if opts.dump_user_agent:
307         print(std_headers['User-Agent'])
308         sys.exit(0)
309
310     # Batch file verification
311     batchurls = []
312     if opts.batchfile is not None:
313         try:
314             if opts.batchfile == '-':
315                 batchfd = sys.stdin
316             else:
317                 batchfd = open(opts.batchfile, 'r')
318             batchurls = batchfd.readlines()
319             batchurls = [x.strip() for x in batchurls]
320             batchurls = [x for x in batchurls if len(x) > 0 and not re.search(r'^[#/;]', x)]
321         except IOError:
322             sys.exit(u'ERROR: batch file could not be read')
323     all_urls = batchurls + args
324     all_urls = [url.strip() for url in all_urls]
325
326     # General configuration
327     cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
328     proxy_handler = compat_urllib_request.ProxyHandler()
329     opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
330     compat_urllib_request.install_opener(opener)
331     socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words)
332
333     extractors = gen_extractors()
334
335     if opts.list_extractors:
336         for ie in extractors:
337             print(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else ''))
338             matchedUrls = [url for url in all_urls if ie.suitable(url)]
339             all_urls = [url for url in all_urls if url not in matchedUrls]
340             for mu in matchedUrls:
341                 print(u'  ' + mu)
342         sys.exit(0)
343
344     # Conflicting, missing and erroneous options
345     if opts.usenetrc and (opts.username is not None or opts.password is not None):
346         parser.error(u'using .netrc conflicts with giving username/password')
347     if opts.password is not None and opts.username is None:
348         parser.error(u'account username missing')
349     if opts.outtmpl is not None and (opts.usetitle or opts.autonumber or opts.useid):
350         parser.error(u'using output template conflicts with using title, video ID or auto number')
351     if opts.usetitle and opts.useid:
352         parser.error(u'using title conflicts with using video ID')
353     if opts.username is not None and opts.password is None:
354         opts.password = getpass.getpass(u'Type account password and press return:')
355     if opts.ratelimit is not None:
356         numeric_limit = FileDownloader.parse_bytes(opts.ratelimit)
357         if numeric_limit is None:
358             parser.error(u'invalid rate limit specified')
359         opts.ratelimit = numeric_limit
360     if opts.min_filesize is not None:
361         numeric_limit = FileDownloader.parse_bytes(opts.min_filesize)
362         if numeric_limit is None:
363             parser.error(u'invalid min_filesize specified')
364         opts.min_filesize = numeric_limit
365     if opts.max_filesize is not None:
366         numeric_limit = FileDownloader.parse_bytes(opts.max_filesize)
367         if numeric_limit is None:
368             parser.error(u'invalid max_filesize specified')
369         opts.max_filesize = numeric_limit
370     if opts.retries is not None:
371         try:
372             opts.retries = int(opts.retries)
373         except (TypeError, ValueError) as err:
374             parser.error(u'invalid retry count specified')
375     if opts.buffersize is not None:
376         numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize)
377         if numeric_buffersize is None:
378             parser.error(u'invalid buffer size specified')
379         opts.buffersize = numeric_buffersize
380     try:
381         opts.playliststart = int(opts.playliststart)
382         if opts.playliststart <= 0:
383             raise ValueError(u'Playlist start must be positive')
384     except (TypeError, ValueError) as err:
385         parser.error(u'invalid playlist start number specified')
386     try:
387         opts.playlistend = int(opts.playlistend)
388         if opts.playlistend != -1 and (opts.playlistend <= 0 or opts.playlistend < opts.playliststart):
389             raise ValueError(u'Playlist end must be greater than playlist start')
390     except (TypeError, ValueError) as err:
391         parser.error(u'invalid playlist end number specified')
392     if opts.extractaudio:
393         if opts.audioformat not in ['best', 'aac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']:
394             parser.error(u'invalid audio format specified')
395     if opts.audioquality:
396         opts.audioquality = opts.audioquality.strip('k').strip('K')
397         if not opts.audioquality.isdigit():
398             parser.error(u'invalid audio quality specified')
399     if opts.recodevideo is not None:
400         if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg']:
401             parser.error(u'invalid video recode format specified')
402
403     if sys.version_info < (3,):
404         # In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
405         if opts.outtmpl is not None:
406             opts.outtmpl = opts.outtmpl.decode(preferredencoding())
407     outtmpl =((opts.outtmpl is not None and opts.outtmpl)
408             or (opts.format == '-1' and opts.usetitle and u'%(title)s-%(id)s-%(format)s.%(ext)s')
409             or (opts.format == '-1' and u'%(id)s-%(format)s.%(ext)s')
410             or (opts.usetitle and opts.autonumber and u'%(autonumber)s-%(title)s-%(id)s.%(ext)s')
411             or (opts.usetitle and u'%(title)s-%(id)s.%(ext)s')
412             or (opts.useid and u'%(id)s.%(ext)s')
413             or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s')
414             or u'%(id)s.%(ext)s')
415
416     # File downloader
417     fd = FileDownloader({
418         'usenetrc': opts.usenetrc,
419         'username': opts.username,
420         'password': opts.password,
421         'quiet': (opts.quiet or opts.geturl or opts.gettitle or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
422         'forceurl': opts.geturl,
423         'forcetitle': opts.gettitle,
424         'forcethumbnail': opts.getthumbnail,
425         'forcedescription': opts.getdescription,
426         'forcefilename': opts.getfilename,
427         'forceformat': opts.getformat,
428         'simulate': opts.simulate,
429         'skip_download': (opts.skip_download or opts.simulate or opts.geturl or opts.gettitle or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
430         'format': opts.format,
431         'format_limit': opts.format_limit,
432         'listformats': opts.listformats,
433         'outtmpl': outtmpl,
434         'restrictfilenames': opts.restrictfilenames,
435         'ignoreerrors': opts.ignoreerrors,
436         'ratelimit': opts.ratelimit,
437         'nooverwrites': opts.nooverwrites,
438         'retries': opts.retries,
439         'buffersize': opts.buffersize,
440         'noresizebuffer': opts.noresizebuffer,
441         'continuedl': opts.continue_dl,
442         'noprogress': opts.noprogress,
443         'progress_with_newline': opts.progress_with_newline,
444         'playliststart': opts.playliststart,
445         'playlistend': opts.playlistend,
446         'logtostderr': opts.outtmpl == '-',
447         'consoletitle': opts.consoletitle,
448         'nopart': opts.nopart,
449         'updatetime': opts.updatetime,
450         'writedescription': opts.writedescription,
451         'writeinfojson': opts.writeinfojson,
452         'writesubtitles': opts.writesubtitles,
453         'subtitleslang': opts.subtitleslang,
454         'matchtitle': decodeOption(opts.matchtitle),
455         'rejecttitle': decodeOption(opts.rejecttitle),
456         'max_downloads': opts.max_downloads,
457         'prefer_free_formats': opts.prefer_free_formats,
458         'verbose': opts.verbose,
459         'test': opts.test,
460         'keepvideo': opts.keepvideo,
461         'min_filesize': opts.min_filesize,
462         'max_filesize': opts.max_filesize
463         })
464
465     if opts.verbose:
466         fd.to_screen(u'[debug] youtube-dl version ' + __version__)
467         try:
468             sp = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
469                                   cwd=os.path.dirname(os.path.abspath(__file__)))
470             out, err = sp.communicate()
471             out = out.decode().strip()
472             if re.match('[0-9a-f]+', out):
473                 fd.to_screen(u'[debug] Git HEAD: ' + out)
474         except:
475             pass
476         fd.to_screen(u'[debug] Python version %s - %s' %(platform.python_version(), platform.platform()))
477         fd.to_screen(u'[debug] Proxy map: ' + str(proxy_handler.proxies))
478
479     for extractor in extractors:
480         fd.add_info_extractor(extractor)
481
482     # PostProcessors
483     if opts.extractaudio:
484         fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, nopostoverwrites=opts.nopostoverwrites))
485     if opts.recodevideo:
486         fd.add_post_processor(FFmpegVideoConvertor(preferedformat=opts.recodevideo))
487
488     # Update version
489     if opts.update_self:
490         update_self(fd.to_screen, opts.verbose, sys.argv[0])
491
492     # Maybe do nothing
493     if len(all_urls) < 1:
494         if not opts.update_self:
495             parser.error(u'you must provide at least one URL')
496         else:
497             sys.exit()
498
499     try:
500         retcode = fd.download(all_urls)
501     except MaxDownloadsReached:
502         fd.to_screen(u'--max-download limit reached, aborting.')
503         retcode = 101
504
505     # Dump cookie jar if requested
506     if opts.cookiefile is not None:
507         try:
508             jar.save()
509         except (IOError, OSError) as err:
510             sys.exit(u'ERROR: unable to save cookie jar')
511
512     sys.exit(retcode)
513
514 def main():
515     try:
516         _real_main()
517     except DownloadError:
518         sys.exit(1)
519     except SameFileError:
520         sys.exit(u'ERROR: fixed output name but more than one file to download')
521     except KeyboardInterrupt:
522         sys.exit(u'\nERROR: Interrupted by user')