diff --git a/README.md b/README.md index 47e686f84..5ee529a3b 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,9 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo --get-filename Simulate, quiet but print output filename --get-format Simulate, quiet but print output format + -O, --print TEMPLATE Simulate, quiet but print the given fields. + Either a field name or similar formatting + as the output template can be used -j, --dump-json Simulate, quiet but print JSON information. See the "OUTPUT TEMPLATE" for a description of available keys. @@ -620,6 +623,12 @@ Available for the media that is a track or a part of a music album: - `disc_number` (numeric): Number of the disc or other physical medium the track belongs to - `release_year` (numeric): Year (YYYY) when the album was released +Available only when used in `--print`: + + - `urls` (string): The URLs of all requested formats, one in each line + - `duration_string` (string): Length of the video (HH:mm:ss) + - `filename` (string): Name of the video file. Note that the actual filename may be different due to post-processing. Use `--exec echo` to get the name after all postprocessing is complete + Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default). For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `youtube-dl test video` and id `BaW_jenozKcj`, this will result in a `youtube-dl test video-BaW_jenozKcj.mp4` file created in the current directory. diff --git a/youtube_dl/YoutubeDL.py b/youtube_dl/YoutubeDL.py index 9e5620eef..b200cfd48 100755 --- a/youtube_dl/YoutubeDL.py +++ b/youtube_dl/YoutubeDL.py @@ -1912,25 +1912,49 @@ class YoutubeDL(object): return subs def __forced_printings(self, info_dict, filename, incomplete): + FIELD_ALIASES = {} + def print_mandatory(field): + actual_field = FIELD_ALIASES.get(field, field) if (self.params.get('force%s' % field, False) - and (not incomplete or info_dict.get(field) is not None)): - self.to_stdout(info_dict[field]) + and (not incomplete or info_dict.get(actual_field) is not None)): + self.to_stdout(info_dict[actual_field]) def print_optional(field): if (self.params.get('force%s' % field, False) and info_dict.get(field) is not None): self.to_stdout(info_dict[field]) + info_dict = info_dict.copy() + info_dict['duration_string'] = ( # %(duration>%H-%M-%S)s is wrong if duration > 24hrs + formatSeconds(info_dict['duration']) + if info_dict.get('duration', None) is not None + else None) + if info_dict.get('resolution') is None: + info_dict['resolution'] = self.format_resolution(info_dict, default=None) + if filename is not None: + info_dict['filename'] = filename + if info_dict.get('requested_formats') is not None: + # For RTMP URLs, also include the playpath + info_dict['urls'] = '\n'.join(f['url'] + f.get('play_path', '') for f in info_dict['requested_formats']) + elif 'url' in info_dict: + info_dict['urls'] = info_dict['url'] + info_dict.get('play_path', '') + if 'urls' in info_dict: + FIELD_ALIASES['url'] = 'urls' + + for tmpl in self.params.get('forceprint', []): + if re.match(r'\w+$', tmpl): + tmpl = '%({0})s'.format(tmpl) + try: + out_txt = tmpl % info_dict + except KeyError: + self.report_warning('Skipping invalid print string "%s"' % (tmpl, )) + continue + self.to_stdout(out_txt) + print_mandatory('title') print_mandatory('id') - if self.params.get('forceurl', False) and not incomplete: - if info_dict.get('requested_formats') is not None: - for f in info_dict['requested_formats']: - self.to_stdout(f['url'] + f.get('play_path', '')) - else: - # For RTMP URLs, also include the playpath - self.to_stdout(info_dict['url'] + info_dict.get('play_path', '')) + print_mandatory('url') print_optional('thumbnail') print_optional('description') if self.params.get('forcefilename', False) and filename is not None: @@ -1938,6 +1962,7 @@ class YoutubeDL(object): if self.params.get('forceduration', False) and info_dict.get('duration') is not None: self.to_stdout(formatSeconds(info_dict['duration'])) print_mandatory('format') + if self.params.get('forcejson', False): self.to_stdout(json.dumps(self.sanitize_info(info_dict))) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 06bdfb689..2851ef9a8 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -245,7 +245,7 @@ def _real_main(argv=None): ' file! Use "{0}.%(ext)s" instead of "{0}" as the output' ' template'.format(outtmpl)) - any_getting = 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 + any_getting = opts.print_ or 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 any_printing = opts.print_json download_archive_fn = expand_path(opts.download_archive) if opts.download_archive is not None else opts.download_archive @@ -335,6 +335,7 @@ def _real_main(argv=None): 'forceduration': opts.getduration, 'forcefilename': opts.getfilename, 'forceformat': opts.getformat, + 'forceprint': opts.print_, 'forcejson': opts.dumpjson or opts.print_json, 'dump_single_json': opts.dump_single_json, 'simulate': opts.simulate or any_getting, diff --git a/youtube_dl/options.py b/youtube_dl/options.py index 61705d1f0..5a86d0dc5 100644 --- a/youtube_dl/options.py +++ b/youtube_dl/options.py @@ -13,6 +13,7 @@ from .compat import ( compat_kwargs, compat_open as open, compat_shlex_split, + compat_str, ) from .utils import ( preferredencoding, @@ -109,6 +110,14 @@ def parseOpts(overrideArguments=None): def _comma_separated_values_options_callback(option, opt_str, value, parser): setattr(parser.values, option.dest, value.split(',')) + def _list_from_options_callback(option, opt_str, value, parser, append=True, delim=',', process=compat_str.strip): + # append can be True, False or -1 (prepend) + current = list(getattr(parser.values, option.dest)) if append else [] + value = list(filter(None, [process(value)] if delim is None else map(process, value.split(delim)))) + setattr( + parser.values, option.dest, + current + value if append is True else value + current) + # No need to wrap help messages if we're on a wide console columns = compat_get_terminal_size().columns max_width = columns if columns else 80 @@ -594,6 +603,13 @@ def parseOpts(overrideArguments=None): '--skip-download', action='store_true', dest='skip_download', default=False, help='Do not download the video') + verbosity.add_option( + '-O', '--print', metavar='TEMPLATE', + action='callback', dest='print_', type='str', default=[], + callback=_list_from_options_callback, callback_kwargs={'delim': None}, + help=( + 'Simulate, quiet but print the given fields. Either a field name ' + 'or similar formatting as the output template can be used')) verbosity.add_option( '-g', '--get-url', action='store_true', dest='geturl', default=False,