fix clip
This commit is contained in:
@@ -71,7 +71,7 @@
|
|||||||
"(wayupload\\.com/[a-z0-9]{12}\\.html)"
|
"(wayupload\\.com/[a-z0-9]{12}\\.html)"
|
||||||
],
|
],
|
||||||
"regexp": "(turbobit5?a?\\.(net|cc|com)/([a-z0-9]{12}))|(turbobif\\.(net|cc|com)/([a-z0-9]{12}))|(turb[o]?\\.(to|cc|pw)\\/([a-z0-9]{12}))|(turbobit\\.(net|cc)/download/free/([a-z0-9]{12}))|((trbbt|tourbobit|torbobit|tbit|turbobita|trbt)\\.(net|cc|com|to)/([a-z0-9]{12}))|((turbobit\\.cloud/turbo/[a-z0-9]+))|((wayupload\\.com/[a-z0-9]{12}\\.html))",
|
"regexp": "(turbobit5?a?\\.(net|cc|com)/([a-z0-9]{12}))|(turbobif\\.(net|cc|com)/([a-z0-9]{12}))|(turb[o]?\\.(to|cc|pw)\\/([a-z0-9]{12}))|(turbobit\\.(net|cc)/download/free/([a-z0-9]{12}))|((trbbt|tourbobit|torbobit|tbit|turbobita|trbt)\\.(net|cc|com|to)/([a-z0-9]{12}))|((turbobit\\.cloud/turbo/[a-z0-9]+))|((wayupload\\.com/[a-z0-9]{12}\\.html))",
|
||||||
"status": false
|
"status": true
|
||||||
},
|
},
|
||||||
"hitfile": {
|
"hitfile": {
|
||||||
"name": "hitfile",
|
"name": "hitfile",
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
"(filespace\\.com/[a-zA-Z0-9]{12})"
|
"(filespace\\.com/[a-zA-Z0-9]{12})"
|
||||||
],
|
],
|
||||||
"regexp": "(filespace\\.com/fd/([a-zA-Z0-9]{12}))|((filespace\\.com/[a-zA-Z0-9]{12}))",
|
"regexp": "(filespace\\.com/fd/([a-zA-Z0-9]{12}))|((filespace\\.com/[a-zA-Z0-9]{12}))",
|
||||||
"status": true
|
"status": false
|
||||||
},
|
},
|
||||||
"filezip": {
|
"filezip": {
|
||||||
"name": "filezip",
|
"name": "filezip",
|
||||||
@@ -595,7 +595,7 @@
|
|||||||
"(simfileshare\\.net/download/[0-9]+/)"
|
"(simfileshare\\.net/download/[0-9]+/)"
|
||||||
],
|
],
|
||||||
"regexp": "(simfileshare\\.net/download/[0-9]+/)",
|
"regexp": "(simfileshare\\.net/download/[0-9]+/)",
|
||||||
"status": true
|
"status": false
|
||||||
},
|
},
|
||||||
"streamtape": {
|
"streamtape": {
|
||||||
"name": "streamtape",
|
"name": "streamtape",
|
||||||
|
|||||||
23
CLI.py
23
CLI.py
@@ -1008,6 +1008,29 @@ class CmdletExecutor:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
cmd_fn = REGISTRY.get(cmd_name)
|
cmd_fn = REGISTRY.get(cmd_name)
|
||||||
|
try:
|
||||||
|
mod = import_cmd_module(cmd_name, reload_loaded=True)
|
||||||
|
data = getattr(mod, "CMDLET", None) if mod else None
|
||||||
|
if data and hasattr(data, "exec") and callable(getattr(data, "exec")):
|
||||||
|
run_fn = getattr(data, "exec")
|
||||||
|
registered_names = set()
|
||||||
|
raw_name = getattr(data, "name", None)
|
||||||
|
if raw_name:
|
||||||
|
registered_names.add(str(raw_name).replace("_", "-").lower())
|
||||||
|
registered_names.add(str(cmd_name).replace("_", "-").lower())
|
||||||
|
for alias_attr in ("alias", "aliases"):
|
||||||
|
alias_values = getattr(data, alias_attr, None)
|
||||||
|
if alias_values:
|
||||||
|
for alias in alias_values:
|
||||||
|
alias_text = str(alias or "").replace("_", "-").lower().strip()
|
||||||
|
if alias_text:
|
||||||
|
registered_names.add(alias_text)
|
||||||
|
for registered_name in registered_names:
|
||||||
|
REGISTRY[registered_name] = run_fn
|
||||||
|
cmd_fn = run_fn
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if not cmd_fn:
|
if not cmd_fn:
|
||||||
# Lazy-import module and register its CMDLET.
|
# Lazy-import module and register its CMDLET.
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from importlib import import_module
|
import sys
|
||||||
|
from importlib import import_module, reload as reload_module
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
import logging
|
import logging
|
||||||
@@ -68,7 +69,7 @@ def _normalize_mod_name(mod_name: str) -> str:
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def import_cmd_module(mod_name: str):
|
def import_cmd_module(mod_name: str, *, reload_loaded: bool = False):
|
||||||
"""Import a cmdlet/native module from cmdnat or cmdlet packages."""
|
"""Import a cmdlet/native module from cmdnat or cmdlet packages."""
|
||||||
normalized = _normalize_mod_name(mod_name)
|
normalized = _normalize_mod_name(mod_name)
|
||||||
if not normalized:
|
if not normalized:
|
||||||
@@ -81,12 +82,16 @@ def import_cmd_module(mod_name: str):
|
|||||||
# OSError if system libmpv is missing.
|
# OSError if system libmpv is missing.
|
||||||
if package is None and normalized == "mpv":
|
if package is None and normalized == "mpv":
|
||||||
try:
|
try:
|
||||||
|
if reload_loaded and "MPV" in sys.modules:
|
||||||
|
return reload_module(sys.modules["MPV"])
|
||||||
return import_module("MPV")
|
return import_module("MPV")
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
# Local MPV package not present; fall back to the normal bare import.
|
# Local MPV package not present; fall back to the normal bare import.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
qualified = f"{package}.{normalized}" if package else normalized
|
qualified = f"{package}.{normalized}" if package else normalized
|
||||||
|
if reload_loaded and qualified in sys.modules:
|
||||||
|
return reload_module(sys.modules[qualified])
|
||||||
return import_module(qualified)
|
return import_module(qualified)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
# Module not available in this package prefix; try the next.
|
# Module not available in this package prefix; try the next.
|
||||||
|
|||||||
@@ -2944,6 +2944,29 @@ class PipelineExecutor:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
cmd_fn = REGISTRY.get(cmd_name)
|
cmd_fn = REGISTRY.get(cmd_name)
|
||||||
|
try:
|
||||||
|
mod = import_cmd_module(cmd_name, reload_loaded=True)
|
||||||
|
data = getattr(mod, "CMDLET", None) if mod else None
|
||||||
|
if data and hasattr(data, "exec") and callable(getattr(data, "exec")):
|
||||||
|
run_fn = getattr(data, "exec")
|
||||||
|
registered_names = set()
|
||||||
|
raw_name = getattr(data, "name", None)
|
||||||
|
if raw_name:
|
||||||
|
registered_names.add(str(raw_name).replace("_", "-").lower())
|
||||||
|
registered_names.add(str(cmd_name).replace("_", "-").lower())
|
||||||
|
for alias_attr in ("alias", "aliases"):
|
||||||
|
alias_values = getattr(data, alias_attr, None)
|
||||||
|
if alias_values:
|
||||||
|
for alias in alias_values:
|
||||||
|
alias_text = str(alias or "").replace("_", "-").lower().strip()
|
||||||
|
if alias_text:
|
||||||
|
registered_names.add(alias_text)
|
||||||
|
for registered_name in registered_names:
|
||||||
|
REGISTRY[registered_name] = run_fn
|
||||||
|
cmd_fn = run_fn
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if not cmd_fn:
|
if not cmd_fn:
|
||||||
try:
|
try:
|
||||||
mod = import_cmd_module(cmd_name)
|
mod = import_cmd_module(cmd_name)
|
||||||
|
|||||||
@@ -1531,6 +1531,25 @@ class Download_File(Cmdlet):
|
|||||||
):
|
):
|
||||||
actual_format = "bestaudio"
|
actual_format = "bestaudio"
|
||||||
|
|
||||||
|
if clip_sections_spec and mode != "audio":
|
||||||
|
clip_format_basis = actual_format
|
||||||
|
if not clip_format_basis or str(clip_format_basis).strip().lower() in {
|
||||||
|
"bestvideo+bestaudio/best",
|
||||||
|
"bestvideo+bestaudio",
|
||||||
|
"best",
|
||||||
|
"best/b",
|
||||||
|
"best/best",
|
||||||
|
"b",
|
||||||
|
}:
|
||||||
|
preferred_clip_format = str(getattr(ytdlp_tool.defaults, "format", "") or "").strip()
|
||||||
|
if preferred_clip_format and preferred_clip_format.lower() != "audio":
|
||||||
|
clip_format_basis = preferred_clip_format
|
||||||
|
else:
|
||||||
|
clip_format_basis = ytdlp_tool.default_format("video")
|
||||||
|
clip_safe_format = ytdlp_tool.resolve_clip_safe_format(clip_format_basis)
|
||||||
|
if clip_safe_format:
|
||||||
|
actual_format = clip_safe_format
|
||||||
|
|
||||||
# DEBUG: Render config panel for tracking pipeline state
|
# DEBUG: Render config panel for tracking pipeline state
|
||||||
if is_debug_enabled():
|
if is_debug_enabled():
|
||||||
try:
|
try:
|
||||||
@@ -1563,6 +1582,7 @@ class Download_File(Cmdlet):
|
|||||||
actual_format
|
actual_format
|
||||||
and isinstance(actual_format, str)
|
and isinstance(actual_format, str)
|
||||||
and mode != "audio"
|
and mode != "audio"
|
||||||
|
and not clip_sections_spec
|
||||||
and "+" not in actual_format
|
and "+" not in actual_format
|
||||||
and "/" not in actual_format
|
and "/" not in actual_format
|
||||||
and "[" not in actual_format
|
and "[" not in actual_format
|
||||||
@@ -1727,7 +1747,12 @@ class Download_File(Cmdlet):
|
|||||||
try:
|
try:
|
||||||
vcodec = str(only.get("vcodec", "none"))
|
vcodec = str(only.get("vcodec", "none"))
|
||||||
acodec = str(only.get("acodec", "none"))
|
acodec = str(only.get("acodec", "none"))
|
||||||
if vcodec != "none" and acodec == "none" and fallback_format:
|
if (
|
||||||
|
not clip_sections_spec
|
||||||
|
and vcodec != "none"
|
||||||
|
and acodec == "none"
|
||||||
|
and fallback_format
|
||||||
|
):
|
||||||
selection_format_id = f"{fallback_format}+bestaudio"
|
selection_format_id = f"{fallback_format}+bestaudio"
|
||||||
except Exception:
|
except Exception:
|
||||||
selection_format_id = fallback_format
|
selection_format_id = fallback_format
|
||||||
|
|||||||
252
tool/ytdlp.py
252
tool/ytdlp.py
@@ -345,6 +345,14 @@ def _get_extractors() -> List[Any]:
|
|||||||
return _EXTRACTOR_CACHE
|
return _EXTRACTOR_CACHE
|
||||||
|
|
||||||
|
|
||||||
|
def _yt_dlp_cli_prefix() -> List[str]:
|
||||||
|
"""Return a subprocess argv prefix that uses the same yt-dlp implementation as this process."""
|
||||||
|
python_exe = str(sys.executable or "").strip()
|
||||||
|
if python_exe:
|
||||||
|
return [python_exe, "-m", "yt_dlp"]
|
||||||
|
return ["yt-dlp"]
|
||||||
|
|
||||||
|
|
||||||
def is_url_supported_by_ytdlp(url: str) -> bool:
|
def is_url_supported_by_ytdlp(url: str) -> bool:
|
||||||
"""Return True if yt-dlp has a non-generic extractor for the URL."""
|
"""Return True if yt-dlp has a non-generic extractor for the URL."""
|
||||||
|
|
||||||
@@ -802,6 +810,45 @@ class YtDlpTool:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def resolve_clip_safe_format(self, format_str: Optional[str]) -> Optional[str]:
|
||||||
|
"""Normalize clip downloads to a single-stream selector yt-dlp can section-download reliably."""
|
||||||
|
if format_str is None or not isinstance(format_str, str):
|
||||||
|
return format_str
|
||||||
|
|
||||||
|
raw = format_str.strip()
|
||||||
|
if not raw:
|
||||||
|
return raw
|
||||||
|
|
||||||
|
low = raw.lower()
|
||||||
|
|
||||||
|
if low in {"bestvideo+bestaudio/best", "bestvideo+bestaudio", "bv+ba/b", "bv+ba"}:
|
||||||
|
return "best/b"
|
||||||
|
|
||||||
|
if low in {"best", "best/b", "best/best", "b"}:
|
||||||
|
return "best/b"
|
||||||
|
|
||||||
|
# Height preferences like 720/1080p should prefer a progressive single-file stream for clips.
|
||||||
|
height_match = re.fullmatch(r"(?i)\s*(\d{3,4})p?\s*", raw)
|
||||||
|
if height_match:
|
||||||
|
try:
|
||||||
|
height = int(height_match.group(1))
|
||||||
|
except Exception:
|
||||||
|
height = 0
|
||||||
|
if height > 0:
|
||||||
|
return f"best[height<={height}]/best"
|
||||||
|
|
||||||
|
merged_height_match = re.fullmatch(
|
||||||
|
r"bestvideo(?:\[height<=?(\d+)\])?\+bestaudio(?:/best(?:\[height<=?(\d+)\])?)?",
|
||||||
|
low,
|
||||||
|
)
|
||||||
|
if merged_height_match:
|
||||||
|
height = merged_height_match.group(1) or merged_height_match.group(2)
|
||||||
|
if height:
|
||||||
|
return f"best[height<={height}]/best"
|
||||||
|
return "best/b"
|
||||||
|
|
||||||
|
return raw
|
||||||
|
|
||||||
def _load_defaults(self) -> YtDlpDefaults:
|
def _load_defaults(self) -> YtDlpDefaults:
|
||||||
cfg = self._config
|
cfg = self._config
|
||||||
|
|
||||||
@@ -1047,6 +1094,24 @@ class YtDlpTool:
|
|||||||
|
|
||||||
default_fmt = self.default_format(opts.mode)
|
default_fmt = self.default_format(opts.mode)
|
||||||
fmt = ytdl_format or default_fmt
|
fmt = ytdl_format or default_fmt
|
||||||
|
if opts.clip_sections and opts.mode != "audio":
|
||||||
|
clip_basis = fmt
|
||||||
|
if isinstance(ytdl_format, str):
|
||||||
|
incoming = ytdl_format.strip().lower()
|
||||||
|
if incoming in {
|
||||||
|
"bestvideo+bestaudio/best",
|
||||||
|
"bestvideo+bestaudio",
|
||||||
|
"best",
|
||||||
|
"best/b",
|
||||||
|
"best/best",
|
||||||
|
"b",
|
||||||
|
} and default_fmt:
|
||||||
|
clip_basis = default_fmt
|
||||||
|
clip_safe_fmt = self.resolve_clip_safe_format(clip_basis)
|
||||||
|
if clip_safe_fmt:
|
||||||
|
if clip_safe_fmt != fmt:
|
||||||
|
debug(f"[ytdlp] clip format normalized: {fmt} -> {clip_safe_fmt}")
|
||||||
|
fmt = clip_safe_fmt
|
||||||
debug(
|
debug(
|
||||||
"[ytdlp] build options: "
|
"[ytdlp] build options: "
|
||||||
f"mode={opts.mode}, ytdl_format={ytdl_format}, default_format={default_fmt}, final_format={fmt}, "
|
f"mode={opts.mode}, ytdl_format={ytdl_format}, default_format={default_fmt}, final_format={fmt}, "
|
||||||
@@ -1154,7 +1219,7 @@ class YtDlpTool:
|
|||||||
|
|
||||||
This is primarily for debug output or subprocess execution.
|
This is primarily for debug output or subprocess execution.
|
||||||
"""
|
"""
|
||||||
argv: List[str] = ["yt-dlp"]
|
argv: List[str] = _yt_dlp_cli_prefix()
|
||||||
if quiet:
|
if quiet:
|
||||||
argv.extend(["--quiet", "--no-warnings"])
|
argv.extend(["--quiet", "--no-warnings"])
|
||||||
argv.append("--no-progress")
|
argv.append("--no-progress")
|
||||||
@@ -1606,7 +1671,7 @@ def _download_with_sections_via_cli(
|
|||||||
section_outtmpl = str(output_dir_path / filename_tmpl)
|
section_outtmpl = str(output_dir_path / filename_tmpl)
|
||||||
|
|
||||||
if section_idx == 1:
|
if section_idx == 1:
|
||||||
metadata_cmd = ["yt-dlp", "--dump-json", "--skip-download"]
|
metadata_cmd = _yt_dlp_cli_prefix() + ["--dump-json", "--skip-download"]
|
||||||
if ytdl_options.get("cookiefile"):
|
if ytdl_options.get("cookiefile"):
|
||||||
cookies_path = ytdl_options["cookiefile"].replace("\\", "/")
|
cookies_path = ytdl_options["cookiefile"].replace("\\", "/")
|
||||||
metadata_cmd.extend(["--cookies", cookies_path])
|
metadata_cmd.extend(["--cookies", cookies_path])
|
||||||
@@ -1628,7 +1693,7 @@ def _download_with_sections_via_cli(
|
|||||||
if not quiet:
|
if not quiet:
|
||||||
debug(f"Error extracting metadata: {exc}")
|
debug(f"Error extracting metadata: {exc}")
|
||||||
|
|
||||||
cmd = ["yt-dlp"]
|
cmd = _yt_dlp_cli_prefix()
|
||||||
if quiet:
|
if quiet:
|
||||||
cmd.append("--no-warnings")
|
cmd.append("--no-warnings")
|
||||||
# Emit line-oriented progress and capture it for real clip activity tracking.
|
# Emit line-oriented progress and capture it for real clip activity tracking.
|
||||||
@@ -1698,6 +1763,163 @@ def _download_with_sections_via_cli(
|
|||||||
return session_id, first_section_info or {}
|
return session_id, first_section_info or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_remote_section_ffmpeg_failure(exc: Exception) -> bool:
|
||||||
|
parts: List[str] = []
|
||||||
|
for candidate in (exc, getattr(exc, "__cause__", None), getattr(exc, "__context__", None)):
|
||||||
|
if candidate is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
parts.append(str(candidate))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
detail = "\n".join(parts)
|
||||||
|
return (
|
||||||
|
"Error opening input file" in detail
|
||||||
|
or "Error opening input files" in detail
|
||||||
|
or "Connection to tcp://" in detail
|
||||||
|
or "Error number -138 occurred" in detail
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_ffmpeg_binary(ytdl_options: Dict[str, Any]) -> Optional[str]:
|
||||||
|
location = str(ytdl_options.get("ffmpeg_location") or "").strip()
|
||||||
|
if location:
|
||||||
|
ffmpeg_path = Path(location)
|
||||||
|
candidates = [ffmpeg_path]
|
||||||
|
if ffmpeg_path.is_dir():
|
||||||
|
candidates = [ffmpeg_path / "ffmpeg.exe", ffmpeg_path / "ffmpeg"]
|
||||||
|
for candidate in candidates:
|
||||||
|
try:
|
||||||
|
if candidate.exists() and candidate.is_file():
|
||||||
|
return str(candidate)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for name in ("ffmpeg", "ffmpeg.exe"):
|
||||||
|
resolved = shutil.which(name)
|
||||||
|
if resolved:
|
||||||
|
return resolved
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_section_bounds(section_text: str) -> Optional[tuple[float, float]]:
|
||||||
|
match = re.search(r"\*?(\d{2}:\d{2}:\d{2})-(\d{2}:\d{2}:\d{2})", str(section_text or "").strip())
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _to_seconds(value: str) -> float:
|
||||||
|
hours, minutes, seconds = value.split(":", 2)
|
||||||
|
return (int(hours) * 3600) + (int(minutes) * 60) + float(seconds)
|
||||||
|
|
||||||
|
start = _to_seconds(match.group(1))
|
||||||
|
end = _to_seconds(match.group(2))
|
||||||
|
if end <= start:
|
||||||
|
return None
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
|
||||||
|
def _trim_sections_from_local_file(
|
||||||
|
*,
|
||||||
|
source_path: Path,
|
||||||
|
output_dir: Path,
|
||||||
|
sections: List[str],
|
||||||
|
session_id: str,
|
||||||
|
ytdl_options: Dict[str, Any],
|
||||||
|
quiet: bool,
|
||||||
|
) -> List[Path]:
|
||||||
|
ffmpeg_bin = _resolve_ffmpeg_binary(ytdl_options)
|
||||||
|
if not ffmpeg_bin:
|
||||||
|
raise DownloadError("ffmpeg not found for local clip fallback")
|
||||||
|
|
||||||
|
generated: List[Path] = []
|
||||||
|
for index, section_text in enumerate(sections, 1):
|
||||||
|
bounds = _parse_section_bounds(section_text)
|
||||||
|
if bounds is None:
|
||||||
|
raise DownloadError(f"Invalid clip section: {section_text}")
|
||||||
|
start_seconds, end_seconds = bounds
|
||||||
|
duration_seconds = end_seconds - start_seconds
|
||||||
|
output_path = output_dir / f"{session_id}_{index}{source_path.suffix or '.mp4'}"
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
ffmpeg_bin,
|
||||||
|
"-y",
|
||||||
|
"-ss",
|
||||||
|
f"{start_seconds:.3f}",
|
||||||
|
"-i",
|
||||||
|
str(source_path),
|
||||||
|
"-t",
|
||||||
|
f"{duration_seconds:.3f}",
|
||||||
|
"-map_metadata",
|
||||||
|
"0",
|
||||||
|
]
|
||||||
|
|
||||||
|
if source_path.suffix.lower() in {".mp3", ".m4a", ".aac", ".opus", ".ogg", ".wav", ".flac"}:
|
||||||
|
cmd.extend(["-c", "copy"])
|
||||||
|
else:
|
||||||
|
cmd.extend([
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
"veryfast",
|
||||||
|
"-crf",
|
||||||
|
"18",
|
||||||
|
"-c:a",
|
||||||
|
"aac",
|
||||||
|
"-b:a",
|
||||||
|
"192k",
|
||||||
|
"-movflags",
|
||||||
|
"+faststart",
|
||||||
|
])
|
||||||
|
|
||||||
|
cmd.append(str(output_path))
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
stderr_text = str(result.stderr or "").strip()
|
||||||
|
raise DownloadError(
|
||||||
|
f"ffmpeg local clip fallback failed for section {section_text}: {stderr_text}"
|
||||||
|
)
|
||||||
|
if not quiet:
|
||||||
|
debug(f"Local clip fallback wrote: {output_path.name}")
|
||||||
|
generated.append(output_path)
|
||||||
|
|
||||||
|
return generated
|
||||||
|
|
||||||
|
|
||||||
|
def _download_with_sections_via_local_trim(
|
||||||
|
url: str,
|
||||||
|
ytdl_options: Dict[str, Any],
|
||||||
|
sections: List[str],
|
||||||
|
quiet: bool = False,
|
||||||
|
) -> tuple[Optional[str], Dict[str, Any]]:
|
||||||
|
download_options = dict(ytdl_options)
|
||||||
|
download_options.pop("download_sections", None)
|
||||||
|
download_options.pop("force_keyframes_at_cuts", None)
|
||||||
|
|
||||||
|
assert yt_dlp is not None
|
||||||
|
with yt_dlp.YoutubeDL(download_options) as ydl: # type: ignore[arg-type]
|
||||||
|
info = cast(Dict[str, Any], ydl.extract_info(url, download=True))
|
||||||
|
|
||||||
|
output_dir = Path(str(download_options.get("outtmpl") or "%(_title)s.%(ext)s")).parent
|
||||||
|
entry, media_path = _resolve_entry_and_path(info, output_dir)
|
||||||
|
session_id = hashlib.md5((url + str(time.time()) + "localtrim").encode()).hexdigest()[:12]
|
||||||
|
generated = _trim_sections_from_local_file(
|
||||||
|
source_path=media_path,
|
||||||
|
output_dir=output_dir,
|
||||||
|
sections=sections,
|
||||||
|
session_id=session_id,
|
||||||
|
ytdl_options=ytdl_options,
|
||||||
|
quiet=quiet,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
media_path.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
first_section_info = entry if isinstance(entry, dict) else info
|
||||||
|
return session_id, first_section_info or {}
|
||||||
|
|
||||||
|
|
||||||
def _iter_download_entries(info: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
|
def _iter_download_entries(info: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
|
||||||
queue: List[Dict[str, Any]] = [info]
|
queue: List[Dict[str, Any]] = [info]
|
||||||
seen: set[int] = set()
|
seen: set[int] = set()
|
||||||
@@ -1950,12 +2172,24 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
|
|||||||
if ytdl_options.get("download_sections"):
|
if ytdl_options.get("download_sections"):
|
||||||
live_ui, _ = PipelineProgress(pipeline_context).ui_and_pipe_index()
|
live_ui, _ = PipelineProgress(pipeline_context).ui_and_pipe_index()
|
||||||
quiet_sections = bool(opts.quiet) or (live_ui is not None)
|
quiet_sections = bool(opts.quiet) or (live_ui is not None)
|
||||||
session_id, first_section_info = _download_with_sections_via_cli(
|
section_list = ytdl_options.get("download_sections", [])
|
||||||
opts.url,
|
try:
|
||||||
ytdl_options,
|
session_id, first_section_info = _download_with_sections_via_cli(
|
||||||
ytdl_options.get("download_sections", []),
|
opts.url,
|
||||||
quiet=quiet_sections,
|
ytdl_options,
|
||||||
)
|
section_list,
|
||||||
|
quiet=quiet_sections,
|
||||||
|
)
|
||||||
|
except Exception as clip_exc:
|
||||||
|
if not _is_remote_section_ffmpeg_failure(clip_exc):
|
||||||
|
raise
|
||||||
|
debug("[yt-dlp] remote section clipping failed; retrying via local trim fallback")
|
||||||
|
session_id, first_section_info = _download_with_sections_via_local_trim(
|
||||||
|
opts.url,
|
||||||
|
ytdl_options,
|
||||||
|
section_list,
|
||||||
|
quiet=quiet_sections,
|
||||||
|
)
|
||||||
info = None
|
info = None
|
||||||
else:
|
else:
|
||||||
with yt_dlp.YoutubeDL(ytdl_options) as ydl: # type: ignore[arg-type]
|
with yt_dlp.YoutubeDL(ytdl_options) as ydl: # type: ignore[arg-type]
|
||||||
|
|||||||
Reference in New Issue
Block a user