From bf57bd77e666befdb52a109d5373a7939b3ea786 Mon Sep 17 00:00:00 2001 From: DARKZOUL5 Date: Fri, 15 May 2026 22:57:40 +0300 Subject: [PATCH] fix download modes --- README.md | 7 ++++- src/app/cli.py | 2 +- src/app/core/download/downloader.py | 43 ++++++++--------------------- src/app/core/sync/executor.py | 8 ++++-- src/app/core/utils/deps.py | 26 ++++++----------- 5 files changed, 33 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 340bf75..ac81d7a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Local-first YouTube playlist synchronization client. - Python 3.10+ - `yt-dlp` (pip) -- `ffmpeg` (for audio extraction) +- `ffmpeg` (only needed for audio extraction / "both" mode) Install: @@ -45,6 +45,11 @@ Create/edit `config/yt-playlist-config.json`: } ``` +`download_mode`: +- `video`: download playlist videos as muxed `.mp4` (no ffmpeg processing) +- `audio`: download muxed `.mp4`, extract `.mp3`, delete the `.mp4` +- `both`: download muxed `.mp4`, extract `.mp3`, keep both files + ## Run - Compute-only: diff --git a/src/app/cli.py b/src/app/cli.py index 425131a..c4647e5 100644 --- a/src/app/cli.py +++ b/src/app/cli.py @@ -51,7 +51,7 @@ def main(argv: list[str] | None = None) -> int: if msg not in seen_errors: seen_errors.add(msg) # Friendly hint for missing ffmpeg - if "ffprobe and ffmpeg not found" in msg.lower(): + if "ffmpeg not found" in msg.lower(): print("ERROR: ffmpeg not found. Install ffmpeg or set 'ffmpeg_path' in config.") else: print(f"ERROR: {msg}") diff --git a/src/app/core/download/downloader.py b/src/app/core/download/downloader.py index 82ad039..a422586 100644 --- a/src/app/core/download/downloader.py +++ b/src/app/core/download/downloader.py @@ -52,38 +52,19 @@ class Downloader: pass outtmpl = str(job.output_path) - if job.mode == "audio": - ydl_opts = { - "format": "bestaudio/best", - "outtmpl": outtmpl, - "postprocessors": [ - { - "key": "FFmpegExtractAudio", - "preferredcodec": "mp3", - "preferredquality": "0", - } - ], - "noplaylist": True, - "quiet": True, - "no_warnings": True, - "logger": _QuietLogger(), - } - else: # video - ydl_opts = { - "format": "bestvideo+bestaudio/best", - "merge_output_format": "mp4", - "outtmpl": outtmpl, - "noplaylist": True, - "quiet": True, - "no_warnings": True, - "logger": _QuietLogger(), - } - # Prefer job-provided path first - if job.ffmpeg_path: - ydl_opts["ffmpeg_location"] = job.ffmpeg_path - elif self.ffmpeg_path: - ydl_opts["ffmpeg_location"] = self.ffmpeg_path + # All modes download a single muxed mp4 when possible. + # This avoids any ffmpeg-driven merging during the download step, satisfying: + # - video: "original file, no processing" + # - audio/both: extraction is done separately after download + ydl_opts = { + "format": "best[ext=mp4][acodec!=none][vcodec!=none]/best[ext=mp4]", + "outtmpl": outtmpl, + "noplaylist": True, + "quiet": True, + "no_warnings": True, + "logger": _QuietLogger(), + } with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[attr-defined] ydl.download([job.url]) diff --git a/src/app/core/sync/executor.py b/src/app/core/sync/executor.py index 08151ee..b0d4f0b 100644 --- a/src/app/core/sync/executor.py +++ b/src/app/core/sync/executor.py @@ -55,9 +55,11 @@ class ActionExecutor: # yt-dlp is required for any download job (Python API usage) ensure_yt_dlp_available() - # ffmpeg/ffprobe are required for merges and audio extraction; check once up-front - ffmpeg_hint = playlist_cfg.get("ffmpeg_path", "ffmpeg") - ensure_ffmpeg_available(str(ffmpeg_hint) if ffmpeg_hint is not None else None) + # ffmpeg is only required when we will extract audio (audio/both modes) + needs_audio = any((a.to_name or "").lower().endswith(".mp3") for a in actions if a.type == SyncActionType.DOWNLOAD) + if needs_audio: + ffmpeg_hint = playlist_cfg.get("ffmpeg_path", "ffmpeg") + ensure_ffmpeg_available(str(ffmpeg_hint) if ffmpeg_hint is not None else None) async def _apply_renames(self, actions: Iterable[SyncAction], audio_root: Path, video_root: Path, playlist_cfg: dict) -> None: playlist_id = extract_playlist_id(playlist_cfg.get("url", "")) or playlist_cfg.get("url", "") diff --git a/src/app/core/utils/deps.py b/src/app/core/utils/deps.py index 20d223a..41a0587 100644 --- a/src/app/core/utils/deps.py +++ b/src/app/core/utils/deps.py @@ -52,7 +52,8 @@ def _resolve_tool_paths(tool_hint: Optional[str], exe_name: str) -> Tuple[Option - Otherwise, treat tool_hint as a command and fall back to PATH resolution. """ if tool_hint: - hint = Path(tool_hint) + hint_str = str(tool_hint).strip().strip('"').strip("'") + hint = Path(hint_str) # Expand envvars (%FFMPEG%) etc. expanded = Path(os.path.expandvars(str(hint))) if expanded.is_dir(): @@ -71,31 +72,22 @@ def _resolve_tool_paths(tool_hint: Optional[str], exe_name: str) -> Tuple[Option def ensure_ffmpeg_available(ffmpeg_hint: Optional[str]) -> Tuple[str, str]: """ - Ensures both ffmpeg and ffprobe are runnable. Returns (ffmpeg_path, ffprobe_path). + Ensures ffmpeg is runnable. Returns (ffmpeg_path, ffmpeg_path). + + Note: ffprobe is intentionally not required. This project uses ffmpeg directly for + audio extraction, and yt-dlp can still function for muxed downloads without ffprobe. """ ffmpeg_exe = "ffmpeg.exe" if sys.platform.startswith("win") else "ffmpeg" - ffprobe_exe = "ffprobe.exe" if sys.platform.startswith("win") else "ffprobe" ffmpeg_path, ffmpeg_dir = _resolve_tool_paths(ffmpeg_hint, ffmpeg_exe) if not ffmpeg_path: raise DependencyError("ffmpeg not found. Install ffmpeg or set 'ffmpeg_path' in config.") - # For ffprobe prefer the same directory if we have one - ffprobe_path = None - if ffmpeg_dir: - cand = Path(ffmpeg_dir) / ffprobe_exe - if cand.exists(): - ffprobe_path = str(cand) - if not ffprobe_path: - ffprobe_path, _ = _resolve_tool_paths(None, ffprobe_exe) - if not ffprobe_path: - raise DependencyError("ffprobe not found (usually ships with ffmpeg). Install ffmpeg or fix 'ffmpeg_path'.") - # Smoke test (fast) try: subprocess.run([ffmpeg_path, "-version"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.run([ffprobe_path, "-version"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception as exc: - raise DependencyError("ffmpeg/ffprobe exist but are not runnable. Check permissions/architecture/path.") from exc + raise DependencyError("ffmpeg exists but is not runnable. Check permissions/architecture/path.") from exc - return ffmpeg_path, ffprobe_path + # Keep return shape stable for existing callers + return ffmpeg_path, ffmpeg_path