mirror of
https://github.com/alexta69/metube.git
synced 2026-06-17 00:30:07 +00:00
Replace custom template substitution with yt-dlp's evaluate_outtmpl
Replace the hand-rolled _outtmpl_substitute_field() / _compile_outtmpl_pattern()
with a new _resolve_outtmpl_fields() that delegates to yt-dlp's
YoutubeDL.evaluate_outtmpl(). This gives playlist/channel output templates
access to yt-dlp's full template syntax: defaults (%(field|fallback)s),
conditional formatting (%(field&prefix {})s), math (%(field+N)d),
datetime formatting (%(field>%Y-%m-%d)s), and more.
Only field references whose root name matches the targeted prefix (e.g.
"playlist" or "channel") are resolved; all other references remain as
template placeholders for yt-dlp to fill during the actual download.
Agent-Logs-Url: https://github.com/alexta69/metube/sessions/0ae5ff34-540f-4fc8-a81c-358fb92b7c15
Co-authored-by: alexta69 <7450369+alexta69@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
483575d24a
commit
c1b5540332
+37
-40
@@ -13,7 +13,6 @@ import logging
|
||||
import re
|
||||
import types
|
||||
from typing import Any, Optional
|
||||
from functools import lru_cache
|
||||
|
||||
import yt_dlp.networking.impersonate
|
||||
from yt_dlp.utils import STR_FORMAT_RE_TMPL, STR_FORMAT_TYPES
|
||||
@@ -24,13 +23,6 @@ from state_store import AtomicJsonStore, from_json_compatible, read_legacy_shelf
|
||||
log = logging.getLogger('ytdl')
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _compile_outtmpl_pattern(field: str) -> re.Pattern:
|
||||
"""Compile a regex pattern to match a specific field in an output template, including optional format specifiers."""
|
||||
conversion_types = f"[{re.escape(STR_FORMAT_TYPES)}]"
|
||||
return re.compile(STR_FORMAT_RE_TMPL.format(re.escape(field), conversion_types))
|
||||
|
||||
|
||||
# Characters that are invalid in Windows/NTFS path components. These are pre-
|
||||
# sanitised when substituting playlist/channel titles into output templates so
|
||||
# that downloads do not fail on NTFS-mounted volumes or Windows Docker hosts.
|
||||
@@ -41,44 +33,51 @@ def _sanitize_path_component(value: Any) -> Any:
|
||||
"""Replace characters that are invalid in Windows path components with '_'.
|
||||
|
||||
Non-string values (int, float, None, …) are passed through unchanged so
|
||||
that ``_outtmpl_substitute_field`` can still coerce them with format specs
|
||||
(e.g. ``%(playlist_index)02d``). Only string values are sanitised because
|
||||
Windows-invalid characters are only a concern for human-readable strings
|
||||
(titles, channel names, etc.) that may end up as directory names.
|
||||
that numeric format specs (e.g. ``%(playlist_index)02d``) still work.
|
||||
Only string values are sanitised because Windows-invalid characters are
|
||||
only a concern for human-readable strings (titles, channel names, etc.)
|
||||
that may end up as directory names.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
return _WINDOWS_INVALID_PATH_CHARS.sub('_', value)
|
||||
|
||||
|
||||
def _outtmpl_substitute_field(template: str, field: str, value: Any) -> str:
|
||||
"""Substitute a single field in an output template, applying any format specifiers to the value."""
|
||||
pattern = _compile_outtmpl_pattern(field)
|
||||
# Regex matching yt-dlp output-template field references, e.g. ``%(title)s``
|
||||
# or ``%(playlist_index)03d``. Built from yt-dlp's own ``STR_FORMAT_RE_TMPL``
|
||||
# so that it stays in sync with upstream changes to the template syntax.
|
||||
_OUTTMPL_FIELD_RE = re.compile(
|
||||
STR_FORMAT_RE_TMPL.format('[^)]+', f'[{STR_FORMAT_TYPES}ljhqBUDS]')
|
||||
)
|
||||
|
||||
def replacement(match: re.Match) -> str:
|
||||
if match.group("has_key") is None:
|
||||
return match.group(0)
|
||||
|
||||
prefix = match.group("prefix") or ""
|
||||
format_spec = match.group("format")
|
||||
def _resolve_outtmpl_fields(template: str, info_dict: dict, prefixes: tuple[str, ...]) -> str:
|
||||
"""Resolve specific fields in an output template using yt-dlp's template engine.
|
||||
|
||||
if not format_spec:
|
||||
return f"{prefix}{value}"
|
||||
Only field references whose root name starts with one of *prefixes* are
|
||||
evaluated. All other references are left untouched so that yt-dlp can
|
||||
resolve them later during the actual download.
|
||||
|
||||
conversion_type = format_spec[-1]
|
||||
try:
|
||||
if conversion_type in "diouxX":
|
||||
coerced_value = int(value)
|
||||
elif conversion_type in "eEfFgG":
|
||||
coerced_value = float(value)
|
||||
else:
|
||||
coerced_value = value
|
||||
This delegates to ``YoutubeDL.evaluate_outtmpl`` for each targeted field
|
||||
reference, giving access to the full yt-dlp template syntax (defaults,
|
||||
conditional formatting, math operations, datetime formatting, etc.).
|
||||
"""
|
||||
matches = list(_OUTTMPL_FIELD_RE.finditer(template))
|
||||
if not matches:
|
||||
return template
|
||||
|
||||
return f"{prefix}{('%' + format_spec) % coerced_value}"
|
||||
except (ValueError, TypeError):
|
||||
return f"{prefix}{value}"
|
||||
with yt_dlp.YoutubeDL({'quiet': True}) as ydl:
|
||||
for match in reversed(matches):
|
||||
key = match.group('key')
|
||||
if key is None:
|
||||
continue
|
||||
root = re.match(r'\w+', key)
|
||||
if root is None or not root.group(0).startswith(prefixes):
|
||||
continue
|
||||
resolved = ydl.evaluate_outtmpl(match.group(0), info_dict)
|
||||
template = template[:match.start()] + resolved + template[match.end():]
|
||||
|
||||
return pattern.sub(replacement, template)
|
||||
return template
|
||||
|
||||
_MAX_ENTRY_SANITIZE_DEPTH = 64
|
||||
|
||||
@@ -818,15 +817,13 @@ class DownloadQueue:
|
||||
if entry is not None and entry.get('playlist_index') is not None:
|
||||
if len(self.config.OUTPUT_TEMPLATE_PLAYLIST):
|
||||
output = self.config.OUTPUT_TEMPLATE_PLAYLIST
|
||||
for property, value in entry.items():
|
||||
if property.startswith("playlist"):
|
||||
output = _outtmpl_substitute_field(output, property, _sanitize_path_component(value))
|
||||
sanitized = {k: _sanitize_path_component(v) for k, v in entry.items()}
|
||||
output = _resolve_outtmpl_fields(output, sanitized, ('playlist',))
|
||||
if entry is not None and entry.get('channel_index') is not None:
|
||||
if len(self.config.OUTPUT_TEMPLATE_CHANNEL):
|
||||
output = self.config.OUTPUT_TEMPLATE_CHANNEL
|
||||
for property, value in entry.items():
|
||||
if property.startswith("channel"):
|
||||
output = _outtmpl_substitute_field(output, property, _sanitize_path_component(value))
|
||||
sanitized = {k: _sanitize_path_component(v) for k, v in entry.items()}
|
||||
output = _resolve_outtmpl_fields(output, sanitized, ('channel',))
|
||||
ytdl_options = dict(self.config.YTDL_OPTIONS)
|
||||
playlist_item_limit = getattr(dl, 'playlist_item_limit', 0)
|
||||
if playlist_item_limit > 0:
|
||||
|
||||
Reference in New Issue
Block a user