Fix cookie fallback breaking yt-dlp in Docker; add OAuth2 auth flow

- _cookie_args() no longer falls through to --cookies-from-browser when
  cookies_file is configured but missing. Firefox isn't installed in the
  Docker image, so that fallback caused yt-dlp to exit with empty stdout
  and every metadata fetch to return "Video not found on YouTube".
- fetch_video_metadata() now retries without auth args if the first call
  fails, so a broken cookie config can't block public video fetches.
- Add use_oauth2 setting + full device-auth flow (POST /settings/oauth2-init,
  GET /settings/oauth2-status) with OAuth2Section UI in Settings page.
- Add GET /settings/ytdlp-test diagnostics endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mattias Tall
2026-05-26 09:53:02 +02:00
parent b3284b35da
commit 98d986cd95
6 changed files with 336 additions and 13 deletions

View File

@@ -33,6 +33,7 @@ class SettingsOut(BaseModel):
feed_weight_recency: float = 5.0
feed_weight_affinity: float = 5.0
feed_weight_channel: float = 5.0
use_oauth2: bool = False
model_config = {"from_attributes": True}
@@ -53,6 +54,7 @@ class SettingsPatch(BaseModel):
feed_weight_recency: Optional[float] = Field(None, ge=0.0, le=10.0)
feed_weight_affinity: Optional[float] = Field(None, ge=0.0, le=10.0)
feed_weight_channel: Optional[float] = Field(None, ge=0.0, le=10.0)
use_oauth2: Optional[bool] = None
def _get_or_create(db: Session, user_id: int) -> UserSettings:
@@ -118,6 +120,9 @@ def update_settings(
s.feed_weight_affinity = body.feed_weight_affinity
if body.feed_weight_channel is not None:
s.feed_weight_channel = body.feed_weight_channel
if body.use_oauth2 is not None:
s.use_oauth2 = body.use_oauth2
ytdlp.set_oauth2(body.use_oauth2)
db.commit()
db.refresh(s)
@@ -165,3 +170,75 @@ def delete_cookies_file(
db.commit()
db.refresh(s)
return s
@router.post("/oauth2-init")
def oauth2_init(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Start a yt-dlp OAuth2 device-auth flow. Returns the device URL and code immediately."""
state = ytdlp.start_oauth2_flow()
return state
@router.get("/oauth2-status")
def oauth2_status(
current_user: User = Depends(get_current_user),
):
"""Poll the current OAuth2 flow status."""
return ytdlp.get_oauth2_status()
@router.post("/oauth2-enable", response_model=SettingsOut)
def oauth2_enable(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Mark OAuth2 as the active auth method after a successful flow."""
s = _get_or_create(db, current_user.id)
s.use_oauth2 = True
ytdlp.set_oauth2(True)
db.commit()
db.refresh(s)
return s
@router.post("/oauth2-disable", response_model=SettingsOut)
def oauth2_disable(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Disable OAuth2 auth, falling back to cookies."""
s = _get_or_create(db, current_user.id)
s.use_oauth2 = False
ytdlp.set_oauth2(False)
db.commit()
db.refresh(s)
return s
@router.get("/ytdlp-test")
def ytdlp_test(
current_user: User = Depends(get_current_user),
):
"""Run a quick yt-dlp metadata fetch on a public video and return raw output for diagnostics."""
import subprocess
cookie_args = ytdlp._cookie_args()
result = subprocess.run(
[
"yt-dlp",
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"--dump-json", "--no-download", "--no-playlist",
"--extractor-args", "youtube:player_client=tv,ios,web",
*cookie_args,
],
capture_output=True, text=True, timeout=30,
)
return {
"returncode": result.returncode,
"cookie_args": cookie_args,
"stdout_lines": result.stdout.splitlines()[:5],
"stderr_tail": result.stderr.splitlines()[-20:],
"success": result.returncode == 0,
}