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"
|
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):
|
class SessionConfig(BaseModel):
|
||||||
"""Session management settings."""
|
"""Session management settings."""
|
||||||
|
|
||||||
|
|
@ -128,6 +141,7 @@ class AppConfig(BaseSettings):
|
||||||
purge: PurgeConfig = PurgeConfig()
|
purge: PurgeConfig = PurgeConfig()
|
||||||
ui: UIConfig = UIConfig()
|
ui: UIConfig = UIConfig()
|
||||||
admin: AdminConfig = AdminConfig()
|
admin: AdminConfig = AdminConfig()
|
||||||
|
ytdlp: YtdlpConfig = YtdlpConfig()
|
||||||
themes_dir: str = "./themes"
|
themes_dir: str = "./themes"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ from app.models.job import (
|
||||||
JobStatus,
|
JobStatus,
|
||||||
ProgressEvent,
|
ProgressEvent,
|
||||||
)
|
)
|
||||||
|
from app.routers.cookies import get_cookie_path_for_session
|
||||||
from app.services.output_template import resolve_template
|
from app.services.output_template import resolve_template
|
||||||
|
|
||||||
logger = logging.getLogger("mediarip.download")
|
logger = logging.getLogger("mediarip.download")
|
||||||
|
|
@ -196,6 +197,17 @@ class DownloadService:
|
||||||
"preferedformat": "mp4",
|
"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._loop.run_in_executor(
|
||||||
self._executor,
|
self._executor,
|
||||||
self._run_download,
|
self._run_download,
|
||||||
|
|
@ -443,11 +455,20 @@ class DownloadService:
|
||||||
logger.info("Job %s completed", job_id)
|
logger.info("Job %s completed", job_id)
|
||||||
|
|
||||||
except Exception as e:
|
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)
|
logger.error("Job %s failed: %s", job_id, e, exc_info=True)
|
||||||
try:
|
try:
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
update_job_status(
|
update_job_status(
|
||||||
self._db, job_id, JobStatus.failed.value, str(e)
|
self._db, job_id, JobStatus.failed.value, error_msg
|
||||||
),
|
),
|
||||||
self._loop,
|
self._loop,
|
||||||
).result(timeout=10)
|
).result(timeout=10)
|
||||||
|
|
@ -455,7 +476,7 @@ class DownloadService:
|
||||||
"event": "job_update",
|
"event": "job_update",
|
||||||
"data": {"job_id": job_id, "status": "failed", "percent": 0,
|
"data": {"job_id": job_id, "status": "failed", "percent": 0,
|
||||||
"speed": None, "eta": None, "filename": None,
|
"speed": None, "eta": None, "filename": None,
|
||||||
"error_message": str(e)},
|
"error_message": error_msg},
|
||||||
})
|
})
|
||||||
# Log to error_log table for admin visibility
|
# Log to error_log table for admin visibility
|
||||||
from app.core.database import log_download_error
|
from app.core.database import log_download_error
|
||||||
|
|
@ -463,7 +484,7 @@ class DownloadService:
|
||||||
log_download_error(
|
log_download_error(
|
||||||
self._db,
|
self._db,
|
||||||
url=url,
|
url=url,
|
||||||
error=str(e),
|
error=error_msg,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
format_id=opts.get("format"),
|
format_id=opts.get("format"),
|
||||||
media_type=opts.get("_media_type"),
|
media_type=opts.get("_media_type"),
|
||||||
|
|
@ -478,11 +499,13 @@ class DownloadService:
|
||||||
|
|
||||||
def _extract_info(self, url: str) -> dict | None:
|
def _extract_info(self, url: str) -> dict | None:
|
||||||
"""Run yt-dlp extract_info synchronously (called from thread pool)."""
|
"""Run yt-dlp extract_info synchronously (called from thread pool)."""
|
||||||
opts = {
|
opts: dict = {
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"no_warnings": True,
|
"no_warnings": True,
|
||||||
"skip_download": True,
|
"skip_download": True,
|
||||||
}
|
}
|
||||||
|
if self._config.ytdlp.extractor_args:
|
||||||
|
opts["extractor_args"] = self._config.ytdlp.extractor_args
|
||||||
try:
|
try:
|
||||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||||
return ydl.extract_info(url, download=False)
|
return ydl.extract_info(url, download=False)
|
||||||
|
|
@ -492,13 +515,15 @@ class DownloadService:
|
||||||
|
|
||||||
def _extract_url_info(self, url: str) -> dict | None:
|
def _extract_url_info(self, url: str) -> dict | None:
|
||||||
"""Extract URL metadata including playlist detection."""
|
"""Extract URL metadata including playlist detection."""
|
||||||
opts = {
|
opts: dict = {
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"no_warnings": True,
|
"no_warnings": True,
|
||||||
"skip_download": True,
|
"skip_download": True,
|
||||||
"extract_flat": "in_playlist",
|
"extract_flat": "in_playlist",
|
||||||
"noplaylist": False,
|
"noplaylist": False,
|
||||||
}
|
}
|
||||||
|
if self._config.ytdlp.extractor_args:
|
||||||
|
opts["extractor_args"] = self._config.ytdlp.extractor_args
|
||||||
try:
|
try:
|
||||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||||
return ydl.extract_info(url, download=False)
|
return ydl.extract_info(url, download=False)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue