mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 10:54:00 -06:00
Fix YouTube 403: cookie injection, configurable extractor_args, better errors
- Wire up get_cookie_path_for_session() in download opts — session
cookies.txt is now passed to yt-dlp as cookiefile when present
- Add YtdlpConfig with extractor_args field, configurable via
config.yaml or MEDIARIP__YTDLP__EXTRACTOR_ARGS env var
(e.g. {"youtube": {"player_client": ["web_safari"]}})
- Inject extractor_args into all three yt-dlp call sites:
_enqueue_single, _extract_info, _extract_url_info
- Enhance 403 error messages with actionable guidance directing
users to upload cookies.txt
This commit is contained in:
parent
245ec0e567
commit
eada1028fd
2 changed files with 44 additions and 5 deletions
|
|
@ -55,6 +55,19 @@ class DownloadsConfig(BaseModel):
|
|||
default_template: str = "%(title)s.%(ext)s"
|
||||
|
||||
|
||||
class YtdlpConfig(BaseModel):
|
||||
"""yt-dlp tuning — operator-level knobs for YouTube and other extractors.
|
||||
|
||||
``extractor_args`` maps extractor names to dicts of arg lists, e.g.:
|
||||
youtube:
|
||||
player_client: ["web_safari", "android_vr"]
|
||||
|
||||
These are passed through to yt-dlp as ``extractor_args``.
|
||||
"""
|
||||
|
||||
extractor_args: dict[str, dict[str, list[str]]] = {}
|
||||
|
||||
|
||||
class SessionConfig(BaseModel):
|
||||
"""Session management settings."""
|
||||
|
||||
|
|
@ -128,6 +141,7 @@ class AppConfig(BaseSettings):
|
|||
purge: PurgeConfig = PurgeConfig()
|
||||
ui: UIConfig = UIConfig()
|
||||
admin: AdminConfig = AdminConfig()
|
||||
ytdlp: YtdlpConfig = YtdlpConfig()
|
||||
themes_dir: str = "./themes"
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from app.models.job import (
|
|||
JobStatus,
|
||||
ProgressEvent,
|
||||
)
|
||||
from app.routers.cookies import get_cookie_path_for_session
|
||||
from app.services.output_template import resolve_template
|
||||
|
||||
logger = logging.getLogger("mediarip.download")
|
||||
|
|
@ -196,6 +197,17 @@ class DownloadService:
|
|||
"preferedformat": "mp4",
|
||||
}]
|
||||
|
||||
# Inject session cookies if uploaded
|
||||
cookie_path = get_cookie_path_for_session(
|
||||
self._config.server.data_dir, session_id,
|
||||
)
|
||||
if cookie_path:
|
||||
opts["cookiefile"] = cookie_path
|
||||
|
||||
# Operator-configured extractor_args (e.g. YouTube player_client)
|
||||
if self._config.ytdlp.extractor_args:
|
||||
opts["extractor_args"] = self._config.ytdlp.extractor_args
|
||||
|
||||
self._loop.run_in_executor(
|
||||
self._executor,
|
||||
self._run_download,
|
||||
|
|
@ -443,11 +455,20 @@ class DownloadService:
|
|||
logger.info("Job %s completed", job_id)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
# Enhance 403 errors with actionable guidance
|
||||
if "403" in error_msg:
|
||||
error_msg = (
|
||||
f"{error_msg}\n\n"
|
||||
"This usually means the site is blocking the download request. "
|
||||
"Try uploading a cookies.txt file (Account menu → Upload cookies) "
|
||||
"from a logged-in browser session."
|
||||
)
|
||||
logger.error("Job %s failed: %s", job_id, e, exc_info=True)
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
update_job_status(
|
||||
self._db, job_id, JobStatus.failed.value, str(e)
|
||||
self._db, job_id, JobStatus.failed.value, error_msg
|
||||
),
|
||||
self._loop,
|
||||
).result(timeout=10)
|
||||
|
|
@ -455,7 +476,7 @@ class DownloadService:
|
|||
"event": "job_update",
|
||||
"data": {"job_id": job_id, "status": "failed", "percent": 0,
|
||||
"speed": None, "eta": None, "filename": None,
|
||||
"error_message": str(e)},
|
||||
"error_message": error_msg},
|
||||
})
|
||||
# Log to error_log table for admin visibility
|
||||
from app.core.database import log_download_error
|
||||
|
|
@ -463,7 +484,7 @@ class DownloadService:
|
|||
log_download_error(
|
||||
self._db,
|
||||
url=url,
|
||||
error=str(e),
|
||||
error=error_msg,
|
||||
session_id=session_id,
|
||||
format_id=opts.get("format"),
|
||||
media_type=opts.get("_media_type"),
|
||||
|
|
@ -478,11 +499,13 @@ class DownloadService:
|
|||
|
||||
def _extract_info(self, url: str) -> dict | None:
|
||||
"""Run yt-dlp extract_info synchronously (called from thread pool)."""
|
||||
opts = {
|
||||
opts: dict = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
}
|
||||
if self._config.ytdlp.extractor_args:
|
||||
opts["extractor_args"] = self._config.ytdlp.extractor_args
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||
return ydl.extract_info(url, download=False)
|
||||
|
|
@ -492,13 +515,15 @@ class DownloadService:
|
|||
|
||||
def _extract_url_info(self, url: str) -> dict | None:
|
||||
"""Extract URL metadata including playlist detection."""
|
||||
opts = {
|
||||
opts: dict = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
"extract_flat": "in_playlist",
|
||||
"noplaylist": False,
|
||||
}
|
||||
if self._config.ytdlp.extractor_args:
|
||||
opts["extractor_args"] = self._config.ytdlp.extractor_args
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||
return ydl.extract_info(url, download=False)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue