mirror of
https://github.com/darkzoul5/YoutubePlaylistSync.git
synced 2026-07-04 04:53:58 +03:00
fix download modes
This commit is contained in:
@@ -20,7 +20,7 @@ Local-first YouTube playlist synchronization client.
|
|||||||
|
|
||||||
- Python 3.10+
|
- Python 3.10+
|
||||||
- `yt-dlp` (pip)
|
- `yt-dlp` (pip)
|
||||||
- `ffmpeg` (for audio extraction)
|
- `ffmpeg` (only needed for audio extraction / "both" mode)
|
||||||
|
|
||||||
Install:
|
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
|
## Run
|
||||||
|
|
||||||
- Compute-only:
|
- Compute-only:
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if msg not in seen_errors:
|
if msg not in seen_errors:
|
||||||
seen_errors.add(msg)
|
seen_errors.add(msg)
|
||||||
# Friendly hint for missing ffmpeg
|
# 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.")
|
print("ERROR: ffmpeg not found. Install ffmpeg or set 'ffmpeg_path' in config.")
|
||||||
else:
|
else:
|
||||||
print(f"ERROR: {msg}")
|
print(f"ERROR: {msg}")
|
||||||
|
|||||||
@@ -52,38 +52,19 @@ class Downloader:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
outtmpl = str(job.output_path)
|
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
|
# All modes download a single muxed mp4 when possible.
|
||||||
if job.ffmpeg_path:
|
# This avoids any ffmpeg-driven merging during the download step, satisfying:
|
||||||
ydl_opts["ffmpeg_location"] = job.ffmpeg_path
|
# - video: "original file, no processing"
|
||||||
elif self.ffmpeg_path:
|
# - audio/both: extraction is done separately after download
|
||||||
ydl_opts["ffmpeg_location"] = self.ffmpeg_path
|
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]
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[attr-defined]
|
||||||
ydl.download([job.url])
|
ydl.download([job.url])
|
||||||
|
|||||||
@@ -55,9 +55,11 @@ class ActionExecutor:
|
|||||||
# yt-dlp is required for any download job (Python API usage)
|
# yt-dlp is required for any download job (Python API usage)
|
||||||
ensure_yt_dlp_available()
|
ensure_yt_dlp_available()
|
||||||
|
|
||||||
# ffmpeg/ffprobe are required for merges and audio extraction; check once up-front
|
# ffmpeg is only required when we will extract audio (audio/both modes)
|
||||||
ffmpeg_hint = playlist_cfg.get("ffmpeg_path", "ffmpeg")
|
needs_audio = any((a.to_name or "").lower().endswith(".mp3") for a in actions if a.type == SyncActionType.DOWNLOAD)
|
||||||
ensure_ffmpeg_available(str(ffmpeg_hint) if ffmpeg_hint is not None else None)
|
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:
|
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", "")
|
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.
|
- Otherwise, treat tool_hint as a command and fall back to PATH resolution.
|
||||||
"""
|
"""
|
||||||
if tool_hint:
|
if tool_hint:
|
||||||
hint = Path(tool_hint)
|
hint_str = str(tool_hint).strip().strip('"').strip("'")
|
||||||
|
hint = Path(hint_str)
|
||||||
# Expand envvars (%FFMPEG%) etc.
|
# Expand envvars (%FFMPEG%) etc.
|
||||||
expanded = Path(os.path.expandvars(str(hint)))
|
expanded = Path(os.path.expandvars(str(hint)))
|
||||||
if expanded.is_dir():
|
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]:
|
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"
|
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)
|
ffmpeg_path, ffmpeg_dir = _resolve_tool_paths(ffmpeg_hint, ffmpeg_exe)
|
||||||
if not ffmpeg_path:
|
if not ffmpeg_path:
|
||||||
raise DependencyError("ffmpeg not found. Install ffmpeg or set 'ffmpeg_path' in config.")
|
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)
|
# Smoke test (fast)
|
||||||
try:
|
try:
|
||||||
subprocess.run([ffmpeg_path, "-version"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
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:
|
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