mirror of
https://github.com/darkzoul5/YoutubePlaylistSync.git
synced 2026-07-03 04:23:59 +03:00
fix download modes
This commit is contained in:
@@ -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:
|
||||
|
||||
+1
-1
@@ -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}")
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user