Multiple output templates for different file types

Syntax: -o common_template -o type:type_template
Types supported: subtitle|thumbnail|description|annotation|infojson|pl_description|pl_infojson
This commit is contained in:
pukkandan 2021-02-03 19:06:09 +05:30
parent ff88a05cff
commit de6000d913
8 changed files with 136 additions and 98 deletions

View File

@ -333,16 +333,16 @@ ## Filesystem Options:
comments and ignored comments and ignored
-P, --paths TYPE:PATH The paths where the files should be -P, --paths TYPE:PATH The paths where the files should be
downloaded. Specify the type of file and downloaded. Specify the type of file and
the path separated by a colon ":" the path separated by a colon ":". All the
(supported: description|annotation|subtitle same types as --output are supported.
|infojson|thumbnail). Additionally, you can Additionally, you can also provide "home"
also provide "home" and "temp" paths. All and "temp" paths. All intermediary files
intermediary files are first downloaded to are first downloaded to the temp path and
the temp path and then the final files are then the final files are moved over to the
moved over to the home path after download home path after download is finished. This
is finished. Note that this option is option is ignored if --output is an
ignored if --output is an absolute path absolute path
-o, --output TEMPLATE Output filename template, see "OUTPUT -o, --output [TYPE:]TEMPLATE Output filename template, see "OUTPUT
TEMPLATE" for details TEMPLATE" for details
--output-na-placeholder TEXT Placeholder value for unavailable meta --output-na-placeholder TEXT Placeholder value for unavailable meta
fields in output filename template fields in output filename template
@ -751,7 +751,9 @@ # OUTPUT TEMPLATE
**tl;dr:** [navigate me to examples](#output-template-examples). **tl;dr:** [navigate me to examples](#output-template-examples).
The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `youtube-dlc -o funny_video.flv "https://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Additionally, date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses seperated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`. The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `youtube-dlc -o funny_video.flv "https://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Date/time fields can also be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses seperated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`.
Additionally, you can set different output templates for the various metadata files seperately from the general output template by specifying the type of file followed by the template seperated by a colon ":". The different filetypes supported are subtitle|thumbnail|description|annotation|infojson|pl_description|pl_infojson. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
The available fields are: The available fields are:
@ -860,7 +862,7 @@ #### Output template and Windows batch files
#### Output template examples #### Output template examples
Note that on Windows you may need to use double quotes instead of single. Note that on Windows you need to use double quotes instead of single.
```bash ```bash
$ youtube-dlc --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc $ youtube-dlc --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc

View File

@ -49,6 +49,7 @@
date_from_str, date_from_str,
DateRange, DateRange,
DEFAULT_OUTTMPL, DEFAULT_OUTTMPL,
OUTTMPL_TYPES,
determine_ext, determine_ext,
determine_protocol, determine_protocol,
DOT_DESKTOP_LINK_TEMPLATE, DOT_DESKTOP_LINK_TEMPLATE,
@ -182,7 +183,8 @@ class YoutubeDL(object):
format_sort_force: Force the given format_sort. see "Sorting Formats" for more details. format_sort_force: Force the given format_sort. see "Sorting Formats" for more details.
allow_multiple_video_streams: Allow multiple video streams to be merged into a single file allow_multiple_video_streams: Allow multiple video streams to be merged into a single file
allow_multiple_audio_streams: Allow multiple audio streams to be merged into a single file allow_multiple_audio_streams: Allow multiple audio streams to be merged into a single file
outtmpl: Template for output names. outtmpl: Dictionary of templates for output names. Allowed keys
are 'default' and the keys of OUTTMPL_TYPES (in utils.py)
outtmpl_na_placeholder: Placeholder for unavailable meta fields. outtmpl_na_placeholder: Placeholder for unavailable meta fields.
restrictfilenames: Do not allow "&" and spaces in file names restrictfilenames: Do not allow "&" and spaces in file names
trim_file_name: Limit length of filename (extension excluded) trim_file_name: Limit length of filename (extension excluded)
@ -493,10 +495,7 @@ def check_deprecated(param, option, suggestion):
'Set the LC_ALL environment variable to fix this.') 'Set the LC_ALL environment variable to fix this.')
self.params['restrictfilenames'] = True self.params['restrictfilenames'] = True
if isinstance(params.get('outtmpl'), bytes): self.outtmpl_dict = self.parse_outtmpl()
self.report_warning(
'Parameter outtmpl is bytes, but should be a unicode string. '
'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
self._setup_opener() self._setup_opener()
@ -732,8 +731,21 @@ def report_file_delete(self, file_name):
except UnicodeEncodeError: except UnicodeEncodeError:
self.to_screen('Deleting already existent file') self.to_screen('Deleting already existent file')
def prepare_filename(self, info_dict, warn=False): def parse_outtmpl(self):
"""Generate the output filename.""" outtmpl_dict = self.params.get('outtmpl', {})
if not isinstance(outtmpl_dict, dict):
outtmpl_dict = {'default': outtmpl_dict}
outtmpl_dict.update({
k: v for k, v in DEFAULT_OUTTMPL.items()
if not outtmpl_dict.get(k)})
for key, val in outtmpl_dict.items():
if isinstance(val, bytes):
self.report_warning(
'Parameter outtmpl is bytes, but should be a unicode string. '
'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
return outtmpl_dict
def _prepare_filename(self, info_dict, tmpl_type='default'):
try: try:
template_dict = dict(info_dict) template_dict = dict(info_dict)
@ -765,7 +777,8 @@ def prepare_filename(self, info_dict, warn=False):
na = self.params.get('outtmpl_na_placeholder', 'NA') na = self.params.get('outtmpl_na_placeholder', 'NA')
template_dict = collections.defaultdict(lambda: na, template_dict) template_dict = collections.defaultdict(lambda: na, template_dict)
outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL) outtmpl = self.outtmpl_dict.get(tmpl_type, self.outtmpl_dict['default'])
force_ext = OUTTMPL_TYPES.get(tmpl_type)
# For fields playlist_index and autonumber convert all occurrences # For fields playlist_index and autonumber convert all occurrences
# of %(field)s to %(field)0Nd for backward compatibility # of %(field)s to %(field)0Nd for backward compatibility
@ -835,6 +848,9 @@ def prepare_filename(self, info_dict, warn=False):
# title "Hello $PATH", we don't want `$PATH` to be expanded. # title "Hello $PATH", we don't want `$PATH` to be expanded.
filename = expand_path(outtmpl).replace(sep, '') % template_dict filename = expand_path(outtmpl).replace(sep, '') % template_dict
if force_ext is not None:
filename = replace_extension(filename, force_ext, template_dict.get('ext'))
# https://github.com/blackjack4494/youtube-dlc/issues/85 # https://github.com/blackjack4494/youtube-dlc/issues/85
trim_file_name = self.params.get('trim_file_name', False) trim_file_name = self.params.get('trim_file_name', False)
if trim_file_name: if trim_file_name:
@ -852,25 +868,28 @@ def prepare_filename(self, info_dict, warn=False):
filename = encodeFilename(filename, True).decode(preferredencoding()) filename = encodeFilename(filename, True).decode(preferredencoding())
filename = sanitize_path(filename) filename = sanitize_path(filename)
if warn and not self.__prepare_filename_warned:
if not self.params.get('paths'):
pass
elif filename == '-':
self.report_warning('--paths is ignored when an outputting to stdout')
elif os.path.isabs(filename):
self.report_warning('--paths is ignored since an absolute path is given in output template')
self.__prepare_filename_warned = True
return filename return filename
except ValueError as err: except ValueError as err:
self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')') self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
return None return None
def prepare_filepath(self, filename, dir_type=''): def prepare_filename(self, info_dict, dir_type='', warn=False):
if filename == '-': """Generate the output filename."""
return filename
paths = self.params.get('paths', {}) paths = self.params.get('paths', {})
assert isinstance(paths, dict) assert isinstance(paths, dict)
filename = self._prepare_filename(info_dict, dir_type or 'default')
if warn and not self.__prepare_filename_warned:
if not paths:
pass
elif filename == '-':
self.report_warning('--paths is ignored when an outputting to stdout')
elif os.path.isabs(filename):
self.report_warning('--paths is ignored since an absolute path is given in output template')
self.__prepare_filename_warned = True
if filename == '-' or not filename:
return filename
homepath = expand_path(paths.get('home', '').strip()) homepath = expand_path(paths.get('home', '').strip())
assert isinstance(homepath, compat_str) assert isinstance(homepath, compat_str)
subdir = expand_path(paths.get(dir_type, '').strip()) if dir_type else '' subdir = expand_path(paths.get(dir_type, '').strip()) if dir_type else ''
@ -1041,10 +1060,7 @@ def process_ie_result(self, ie_result, download=True, extra_info={}):
extract_flat = self.params.get('extract_flat', False) extract_flat = self.params.get('extract_flat', False)
if ((extract_flat == 'in_playlist' and 'playlist' in extra_info) if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
or extract_flat is True): or extract_flat is True):
self.__forced_printings( self.__forced_printings(ie_result, self.prepare_filename(ie_result), incomplete=True)
ie_result,
self.prepare_filepath(self.prepare_filename(ie_result)),
incomplete=True)
return ie_result return ie_result
if result_type == 'video': if result_type == 'video':
@ -1150,9 +1166,7 @@ def ensure_dir_exists(path):
return make_dir(path, self.report_error) return make_dir(path, self.report_error)
if self.params.get('writeinfojson', False): if self.params.get('writeinfojson', False):
infofn = replace_extension( infofn = self.prepare_filename(ie_copy, 'pl_infojson')
self.prepare_filepath(self.prepare_filename(ie_copy), 'infojson'),
'info.json', ie_result.get('ext'))
if not ensure_dir_exists(encodeFilename(infofn)): if not ensure_dir_exists(encodeFilename(infofn)):
return return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
@ -1168,9 +1182,7 @@ def ensure_dir_exists(path):
self.report_error('Cannot write playlist metadata to JSON file ' + infofn) self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
if self.params.get('writedescription', False): if self.params.get('writedescription', False):
descfn = replace_extension( descfn = self.prepare_filename(ie_copy, 'pl_description')
self.prepare_filepath(self.prepare_filename(ie_copy), 'description'),
'description', ie_result.get('ext'))
if not ensure_dir_exists(encodeFilename(descfn)): if not ensure_dir_exists(encodeFilename(descfn)):
return return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
@ -1370,7 +1382,7 @@ def can_merge():
and ( and (
not can_merge() not can_merge()
or info_dict.get('is_live', False) or info_dict.get('is_live', False)
or self.params.get('outtmpl', DEFAULT_OUTTMPL) == '-')) or self.outtmpl_dict['default'] == '-'))
return ( return (
'best/bestvideo+bestaudio' 'best/bestvideo+bestaudio'
@ -2032,10 +2044,10 @@ def process_info(self, info_dict):
info_dict = self.pre_process(info_dict) info_dict = self.pre_process(info_dict)
filename = self.prepare_filename(info_dict, warn=True) info_dict['_filename'] = full_filename = self.prepare_filename(info_dict, warn=True)
info_dict['_filename'] = full_filename = self.prepare_filepath(filename) temp_filename = self.prepare_filename(info_dict, 'temp')
temp_filename = self.prepare_filepath(filename, 'temp')
files_to_move = {} files_to_move = {}
skip_dl = self.params.get('skip_download', False)
# Forced printings # Forced printings
self.__forced_printings(info_dict, full_filename, incomplete=False) self.__forced_printings(info_dict, full_filename, incomplete=False)
@ -2047,7 +2059,7 @@ def process_info(self, info_dict):
# Do nothing else if in simulate mode # Do nothing else if in simulate mode
return return
if filename is None: if full_filename is None:
return return
def ensure_dir_exists(path): def ensure_dir_exists(path):
@ -2059,9 +2071,7 @@ def ensure_dir_exists(path):
return return
if self.params.get('writedescription', False): if self.params.get('writedescription', False):
descfn = replace_extension( descfn = self.prepare_filename(info_dict, 'description')
self.prepare_filepath(filename, 'description'),
'description', info_dict.get('ext'))
if not ensure_dir_exists(encodeFilename(descfn)): if not ensure_dir_exists(encodeFilename(descfn)):
return return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
@ -2078,9 +2088,7 @@ def ensure_dir_exists(path):
return return
if self.params.get('writeannotations', False): if self.params.get('writeannotations', False):
annofn = replace_extension( annofn = self.prepare_filename(info_dict, 'annotation')
self.prepare_filepath(filename, 'annotation'),
'annotations.xml', info_dict.get('ext'))
if not ensure_dir_exists(encodeFilename(annofn)): if not ensure_dir_exists(encodeFilename(annofn)):
return return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
@ -2116,10 +2124,11 @@ def dl(name, info, subtitle=False):
# ie = self.get_info_extractor(info_dict['extractor_key']) # ie = self.get_info_extractor(info_dict['extractor_key'])
for sub_lang, sub_info in subtitles.items(): for sub_lang, sub_info in subtitles.items():
sub_format = sub_info['ext'] sub_format = sub_info['ext']
sub_filename = subtitles_filename(temp_filename, sub_lang, sub_format, info_dict.get('ext')) sub_fn = self.prepare_filename(info_dict, 'subtitle')
sub_filename_final = subtitles_filename( sub_filename = subtitles_filename(
self.prepare_filepath(filename, 'subtitle'), temp_filename if not skip_dl else sub_fn,
sub_lang, sub_format, info_dict.get('ext')) sub_lang, sub_format, info_dict.get('ext'))
sub_filename_final = subtitles_filename(sub_fn, sub_lang, sub_format, info_dict.get('ext'))
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)):
self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format)) self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
files_to_move[sub_filename] = sub_filename_final files_to_move[sub_filename] = sub_filename_final
@ -2153,10 +2162,10 @@ def dl(name, info, subtitle=False):
(sub_lang, error_to_compat_str(err))) (sub_lang, error_to_compat_str(err)))
continue continue
if self.params.get('skip_download', False): if skip_dl:
if self.params.get('convertsubtitles', False): if self.params.get('convertsubtitles', False):
# subconv = FFmpegSubtitlesConvertorPP(self, format=self.params.get('convertsubtitles')) # subconv = FFmpegSubtitlesConvertorPP(self, format=self.params.get('convertsubtitles'))
filename_real_ext = os.path.splitext(filename)[1][1:] filename_real_ext = os.path.splitext(full_filename)[1][1:]
filename_wo_ext = ( filename_wo_ext = (
os.path.splitext(full_filename)[0] os.path.splitext(full_filename)[0]
if filename_real_ext == info_dict['ext'] if filename_real_ext == info_dict['ext']
@ -2176,9 +2185,7 @@ def dl(name, info, subtitle=False):
return return
if self.params.get('writeinfojson', False): if self.params.get('writeinfojson', False):
infofn = replace_extension( infofn = self.prepare_filename(info_dict, 'infojson')
self.prepare_filepath(filename, 'infojson'),
'info.json', info_dict.get('ext'))
if not ensure_dir_exists(encodeFilename(infofn)): if not ensure_dir_exists(encodeFilename(infofn)):
return return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
@ -2190,11 +2197,14 @@ def dl(name, info, subtitle=False):
except (OSError, IOError): except (OSError, IOError):
self.report_error('Cannot write video metadata to JSON file ' + infofn) self.report_error('Cannot write video metadata to JSON file ' + infofn)
return return
info_dict['__infojson_filepath'] = infofn info_dict['__infojson_filename'] = infofn
thumbdir = os.path.dirname(self.prepare_filepath(filename, 'thumbnail')) thumbfn = self.prepare_filename(info_dict, 'thumbnail')
for thumbfn in self._write_thumbnails(info_dict, temp_filename): thumb_fn_temp = temp_filename if not skip_dl else thumbfn
files_to_move[thumbfn] = os.path.join(thumbdir, os.path.basename(thumbfn)) for thumb_ext in self._write_thumbnails(info_dict, thumb_fn_temp):
thumb_filename_temp = replace_extension(thumb_fn_temp, thumb_ext, info_dict.get('ext'))
thumb_filename = replace_extension(thumbfn, thumb_ext, info_dict.get('ext'))
files_to_move[thumb_filename_temp] = info_dict['__thumbnail_filename'] = thumb_filename
# Write internet shortcut files # Write internet shortcut files
url_link = webloc_link = desktop_link = False url_link = webloc_link = desktop_link = False
@ -2247,7 +2257,7 @@ def _write_link_file(extension, template, newline, embed_filename):
# Download # Download
must_record_download_archive = False must_record_download_archive = False
if not self.params.get('skip_download', False): if not skip_dl:
try: try:
def existing_file(*filepaths): def existing_file(*filepaths):
@ -2327,7 +2337,7 @@ def correct_ext(filename):
new_info = dict(info_dict) new_info = dict(info_dict)
new_info.update(f) new_info.update(f)
fname = prepend_extension( fname = prepend_extension(
self.prepare_filepath(self.prepare_filename(new_info), 'temp'), self.prepare_filename(new_info, 'temp'),
'f%s' % f['format_id'], new_info['ext']) 'f%s' % f['format_id'], new_info['ext'])
if not ensure_dir_exists(fname): if not ensure_dir_exists(fname):
return return
@ -2357,7 +2367,7 @@ def correct_ext(filename):
self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded)) self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
return return
if success and filename != '-': if success and full_filename != '-':
# Fixup content # Fixup content
fixup_policy = self.params.get('fixup') fixup_policy = self.params.get('fixup')
if fixup_policy is None: if fixup_policy is None:
@ -2439,7 +2449,7 @@ def correct_ext(filename):
def download(self, url_list): def download(self, url_list):
"""Download a given list of URLs.""" """Download a given list of URLs."""
outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL) outtmpl = self.outtmpl_dict['default']
if (len(url_list) > 1 if (len(url_list) > 1
and outtmpl != '-' and outtmpl != '-'
and '%' not in outtmpl and '%' not in outtmpl
@ -2522,12 +2532,13 @@ def post_process(self, filename, ie_info, files_to_move={}):
"""Run all the postprocessors on the given file.""" """Run all the postprocessors on the given file."""
info = dict(ie_info) info = dict(ie_info)
info['filepath'] = filename info['filepath'] = filename
info['__files_to_move'] = {}
for pp in ie_info.get('__postprocessors', []) + self._pps['normal']: for pp in ie_info.get('__postprocessors', []) + self._pps['normal']:
files_to_move, info = self.run_pp(pp, info, files_to_move) files_to_move, info = self.run_pp(pp, info, files_to_move)
info = self.run_pp(MoveFilesAfterDownloadPP(self, files_to_move), info, files_to_move)[1] info = self.run_pp(MoveFilesAfterDownloadPP(self, files_to_move), info)[1]
for pp in self._pps['aftermove']: for pp in self._pps['aftermove']:
files_to_move, info = self.run_pp(pp, info, {}) info = self.run_pp(pp, info, {})[1]
def _make_archive_id(self, info_dict): def _make_archive_id(self, info_dict):
video_id = info_dict.get('id') video_id = info_dict.get('id')
@ -2878,7 +2889,7 @@ def get_encoding(self):
encoding = preferredencoding() encoding = preferredencoding()
return encoding return encoding
def _write_thumbnails(self, info_dict, filename): def _write_thumbnails(self, info_dict, filename): # return the extensions
if self.params.get('writethumbnail', False): if self.params.get('writethumbnail', False):
thumbnails = info_dict.get('thumbnails') thumbnails = info_dict.get('thumbnails')
if thumbnails: if thumbnails:
@ -2891,12 +2902,12 @@ def _write_thumbnails(self, info_dict, filename):
ret = [] ret = []
for t in thumbnails: for t in thumbnails:
thumb_ext = determine_ext(t['url'], 'jpg') thumb_ext = determine_ext(t['url'], 'jpg')
suffix = '_%s' % t['id'] if len(thumbnails) > 1 else '' suffix = '%s.' % t['id'] if len(thumbnails) > 1 else ''
thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else '' thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
t['filename'] = thumb_filename = replace_extension(filename + suffix, thumb_ext, info_dict.get('ext')) t['filename'] = thumb_filename = replace_extension(filename, suffix + thumb_ext, info_dict.get('ext'))
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
ret.append(thumb_filename) ret.append(suffix + thumb_ext)
self.to_screen('[%s] %s: Thumbnail %sis already present' % self.to_screen('[%s] %s: Thumbnail %sis already present' %
(info_dict['extractor'], info_dict['id'], thumb_display_id)) (info_dict['extractor'], info_dict['id'], thumb_display_id))
else: else:
@ -2906,7 +2917,7 @@ def _write_thumbnails(self, info_dict, filename):
uf = self.urlopen(t['url']) uf = self.urlopen(t['url'])
with open(encodeFilename(thumb_filename), 'wb') as thumbf: with open(encodeFilename(thumb_filename), 'wb') as thumbf:
shutil.copyfileobj(uf, thumbf) shutil.copyfileobj(uf, thumbf)
ret.append(thumb_filename) ret.append(suffix + thumb_ext)
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' % self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename)) (info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:

View File

@ -237,18 +237,21 @@ def parse_retries(retries):
if opts.allsubtitles and not opts.writeautomaticsub: if opts.allsubtitles and not opts.writeautomaticsub:
opts.writesubtitles = True opts.writesubtitles = True
outtmpl = ((opts.outtmpl is not None and opts.outtmpl) outtmpl = opts.outtmpl
or (opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s') if not outtmpl:
or (opts.format == '-1' and '%(id)s-%(format)s.%(ext)s') outtmpl = {'default': (
or (opts.usetitle and opts.autonumber and '%(autonumber)s-%(title)s-%(id)s.%(ext)s') '%(title)s-%(id)s-%(format)s.%(ext)s' if opts.format == '-1' and opts.usetitle
or (opts.usetitle and '%(title)s-%(id)s.%(ext)s') else '%(id)s-%(format)s.%(ext)s' if opts.format == '-1'
or (opts.useid and '%(id)s.%(ext)s') else '%(autonumber)s-%(title)s-%(id)s.%(ext)s' if opts.usetitle and opts.autonumber
or (opts.autonumber and '%(autonumber)s-%(id)s.%(ext)s') else '%(title)s-%(id)s.%(ext)s' if opts.usetitle
or DEFAULT_OUTTMPL) else '%(id)s.%(ext)s' if opts.useid
if not os.path.splitext(outtmpl)[1] and opts.extractaudio: else '%(autonumber)s-%(id)s.%(ext)s' if opts.autonumber
else None)}
outtmpl_default = outtmpl.get('default')
if outtmpl_default is not None and not os.path.splitext(outtmpl_default)[1] and opts.extractaudio:
parser.error('Cannot download a video and extract audio into the same' parser.error('Cannot download a video and extract audio into the same'
' file! Use "{0}.%(ext)s" instead of "{0}" as the output' ' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
' template'.format(outtmpl)) ' template'.format(outtmpl_default))
for f in opts.format_sort: for f in opts.format_sort:
if re.match(InfoExtractor.FormatSort.regex, f) is None: if re.match(InfoExtractor.FormatSort.regex, f) is None:
@ -413,7 +416,7 @@ def parse_retries(retries):
'playlistreverse': opts.playlist_reverse, 'playlistreverse': opts.playlist_reverse,
'playlistrandom': opts.playlist_random, 'playlistrandom': opts.playlist_random,
'noplaylist': opts.noplaylist, 'noplaylist': opts.noplaylist,
'logtostderr': opts.outtmpl == '-', 'logtostderr': outtmpl_default == '-',
'consoletitle': opts.consoletitle, 'consoletitle': opts.consoletitle,
'nopart': opts.nopart, 'nopart': opts.nopart,
'updatetime': opts.updatetime, 'updatetime': opts.updatetime,

View File

@ -16,6 +16,7 @@
from .utils import ( from .utils import (
expand_path, expand_path,
get_executable_path, get_executable_path,
OUTTMPL_TYPES,
preferredencoding, preferredencoding,
write_string, write_string,
) )
@ -831,19 +832,23 @@ def _dict_from_multiple_values_options_callback(
metavar='TYPE:PATH', dest='paths', default={}, type='str', metavar='TYPE:PATH', dest='paths', default={}, type='str',
action='callback', callback=_dict_from_multiple_values_options_callback, action='callback', callback=_dict_from_multiple_values_options_callback,
callback_kwargs={ callback_kwargs={
'allowed_keys': 'home|temp|config|description|annotation|subtitle|infojson|thumbnail', 'allowed_keys': 'home|temp|%s' % '|'.join(OUTTMPL_TYPES.keys()),
'process': lambda x: x.strip()}, 'process': lambda x: x.strip()},
help=( help=(
'The paths where the files should be downloaded. ' 'The paths where the files should be downloaded. '
'Specify the type of file and the path separated by a colon ":" ' 'Specify the type of file and the path separated by a colon ":". '
'(supported: description|annotation|subtitle|infojson|thumbnail). ' 'All the same types as --output are supported. '
'Additionally, you can also provide "home" and "temp" paths. ' 'Additionally, you can also provide "home" and "temp" paths. '
'All intermediary files are first downloaded to the temp path and ' 'All intermediary files are first downloaded to the temp path and '
'then the final files are moved over to the home path after download is finished. ' 'then the final files are moved over to the home path after download is finished. '
'Note that this option is ignored if --output is an absolute path')) 'This option is ignored if --output is an absolute path'))
filesystem.add_option( filesystem.add_option(
'-o', '--output', '-o', '--output',
dest='outtmpl', metavar='TEMPLATE', metavar='[TYPE:]TEMPLATE', dest='outtmpl', default={}, type='str',
action='callback', callback=_dict_from_multiple_values_options_callback,
callback_kwargs={
'allowed_keys': '|'.join(OUTTMPL_TYPES.keys()),
'default_key': 'default', 'process': lambda x: x.strip()},
help='Output filename template, see "OUTPUT TEMPLATE" for details') help='Output filename template, see "OUTPUT TEMPLATE" for details')
filesystem.add_option( filesystem.add_option(
'--output-na-placeholder', '--output-na-placeholder',

View File

@ -42,6 +42,7 @@ def __init__(self, downloader=None, already_have_thumbnail=False):
def run(self, info): def run(self, info):
filename = info['filepath'] filename = info['filepath']
temp_filename = prepend_extension(filename, 'temp') temp_filename = prepend_extension(filename, 'temp')
files_to_delete = []
if not info.get('thumbnails'): if not info.get('thumbnails'):
self.to_screen('There aren\'t any thumbnails to embed') self.to_screen('There aren\'t any thumbnails to embed')
@ -78,7 +79,7 @@ def is_webp(path):
escaped_thumbnail_jpg_filename = replace_extension(escaped_thumbnail_filename, 'jpg') escaped_thumbnail_jpg_filename = replace_extension(escaped_thumbnail_filename, 'jpg')
self.to_screen('Converting thumbnail "%s" to JPEG' % escaped_thumbnail_filename) self.to_screen('Converting thumbnail "%s" to JPEG' % escaped_thumbnail_filename)
self.run_ffmpeg(escaped_thumbnail_filename, escaped_thumbnail_jpg_filename, ['-bsf:v', 'mjpeg2jpeg']) self.run_ffmpeg(escaped_thumbnail_filename, escaped_thumbnail_jpg_filename, ['-bsf:v', 'mjpeg2jpeg'])
os.remove(encodeFilename(escaped_thumbnail_filename)) files_to_delete.append(escaped_thumbnail_filename)
thumbnail_jpg_filename = replace_extension(thumbnail_filename, 'jpg') thumbnail_jpg_filename = replace_extension(thumbnail_filename, 'jpg')
# Rename back to unescaped for further processing # Rename back to unescaped for further processing
os.rename(encodeFilename(escaped_thumbnail_jpg_filename), encodeFilename(thumbnail_jpg_filename)) os.rename(encodeFilename(escaped_thumbnail_jpg_filename), encodeFilename(thumbnail_jpg_filename))
@ -183,5 +184,9 @@ def is_webp(path):
if success and temp_filename != filename: if success and temp_filename != filename:
os.remove(encodeFilename(filename)) os.remove(encodeFilename(filename))
os.rename(encodeFilename(temp_filename), encodeFilename(filename)) os.rename(encodeFilename(temp_filename), encodeFilename(filename))
files_to_delete = [] if self._already_have_thumbnail else [thumbnail_filename] if self._already_have_thumbnail:
info['__files_to_move'][thumbnail_filename] = replace_extension(
info['__thumbnail_filename'], os.path.splitext(thumbnail_filename)[1][1:])
else:
files_to_delete.append(thumbnail_filename)
return files_to_delete, info return files_to_delete, info

View File

@ -578,7 +578,7 @@ def ffmpeg_escape(text):
in_filenames.append(metadata_filename) in_filenames.append(metadata_filename)
options.extend(['-map_metadata', '1']) options.extend(['-map_metadata', '1'])
if '__infojson_filepath' in info and info['ext'] in ('mkv', 'mka'): if '__infojson_filename' in info and info['ext'] in ('mkv', 'mka'):
old_stream, new_stream = self.get_stream_number( old_stream, new_stream = self.get_stream_number(
filename, ('tags', 'mimetype'), 'application/json') filename, ('tags', 'mimetype'), 'application/json')
if old_stream is not None: if old_stream is not None:
@ -586,7 +586,7 @@ def ffmpeg_escape(text):
new_stream -= 1 new_stream -= 1
options.extend([ options.extend([
'-attach', info['__infojson_filepath'], '-attach', info['__infojson_filename'],
'-metadata:s:%d' % new_stream, 'mimetype=application/json' '-metadata:s:%d' % new_stream, 'mimetype=application/json'
]) ])

View File

@ -25,6 +25,7 @@ def run(self, info):
dl_path, dl_name = os.path.split(encodeFilename(info['filepath'])) dl_path, dl_name = os.path.split(encodeFilename(info['filepath']))
finaldir = info.get('__finaldir', dl_path) finaldir = info.get('__finaldir', dl_path)
finalpath = os.path.join(finaldir, dl_name) finalpath = os.path.join(finaldir, dl_name)
self.files_to_move.update(info['__files_to_move'])
self.files_to_move[info['filepath']] = finalpath self.files_to_move[info['filepath']] = finalpath
for oldfile, newfile in self.files_to_move.items(): for oldfile, newfile in self.files_to_move.items():
@ -39,7 +40,7 @@ def run(self, info):
if os.path.exists(encodeFilename(newfile)): if os.path.exists(encodeFilename(newfile)):
if self.get_param('overwrites', True): if self.get_param('overwrites', True):
self.report_warning('Replacing existing file "%s"' % newfile) self.report_warning('Replacing existing file "%s"' % newfile)
os.path.remove(encodeFilename(newfile)) os.remove(encodeFilename(newfile))
else: else:
self.report_warning( self.report_warning(
'Cannot move file "%s" out of temporary directory since "%s" already exists. ' 'Cannot move file "%s" out of temporary directory since "%s" already exists. '

View File

@ -4169,7 +4169,18 @@ def q(qid):
return q return q
DEFAULT_OUTTMPL = '%(title)s [%(id)s].%(ext)s' DEFAULT_OUTTMPL = {
'default': '%(title)s [%(id)s].%(ext)s',
}
OUTTMPL_TYPES = {
'subtitle': None,
'thumbnail': None,
'description': 'description',
'annotation': 'annotations.xml',
'infojson': 'info.json',
'pl_description': 'description',
'pl_infojson': 'info.json',
}
def limit_length(s, length): def limit_length(s, length):