Skip to content

_get_formats swallows yt-dlp failures into an empty dropdown with no reason #67

Description

@prekabreki

Context

_get_formats() returns {"video_formats": [], "audio_formats": []} on any
yt-dlp non-zero exit — bot check, JS challenge, expired cookies, network blip,
private video — with no message and no logging. The UI faithfully renders that
as two empty dropdowns, so the user sees "no formats" and has no way to learn
why. stderr is captured on the line above and then discarded.

Found alongside #66. It is how the bad format pick there went unnoticed: the
playlist resolve silently returned no formats, the audio dropdown stayed
populated with a stale 140-drc from an earlier single-video fetch, and
that itag then failed 110 queue items one by one.

Acceptance criteria

  • _get_formats() returns a third key on failure, error (a short reason
    string built from yt-dlp's stderr), alongside the two empty lists.
  • On success error is absent or empty — no behaviour change to the happy
    path, and the existing video_formats/audio_formats shapes are untouched.
  • The failure is logged server-side via the module logger (warning level)
    including the URL and the reason.
  • GET /video-info passes error through to the client for both call sites
    (the playlist branch at server.py:2068 and the single-video branch at 2079).
  • When the response carries error, the frontend shows it (reuse the
    existing log(msg, 'error') path) instead of leaving the user with a
    silently empty dropdown.
  • On a resolve that returns no formats, the format <select> elements and
    _lastAudioFormats are cleared, so a stale pick from a previous fetch
    can never be submitted with a new URL.
  • A unit test covers the failure path: non-zero yt-dlp exit → empty lists
    plus a non-empty error.

Files / where this lives

  • fetchforge/server.py:2090_get_formats(); the silent return is line 2108
  • fetchforge/server.py:2068 — playlist call site (resolves the FIRST entry)
  • fetchforge/server.py:2079 — single-video call site
  • fetchforge/index.html:2975info.audio_formats.forEach(...) populates the
    dropdown; _lastAudioFormats is set at 2982
  • tests/test_server.py — add the unit test here

How to verify

  • .venv/bin/python -m unittest discover -s tests — all pass (257 at time of
    writing, plus your new test)
  • npm --prefix tests/frontend test — all 25 pass
  • Manual: GET /video-info?url=https://www.youtube.com/watch?v=00000000000
    (a nonexistent id) must return a populated error, not a silent empty pair.

Constraints

  • Preserve the success-path response shape exactly — video_formats /
    audio_formats are consumed in several places in index.html.
  • Do not make a failed format fetch raise or 500. An empty-with-reason response
    is the contract; the endpoint must stay non-fatal.
  • Match the existing convention for this: /check-cookies already returns an
    optional reason field alongside its status. Mirror that naming and shape.
  • Do not paste raw yt-dlp stderr wholesale into the response — trim to the last
    meaningful line or two. It can be long and it reaches the DOM.
  • Frontend: textContent only, never innerHTML — the reason string contains
    URLs and untrusted text.

⚠ Danger zones (review focus)

  • The success path must not regress. The easy mistake is adding error on
    every return, or reordering the dict so a caller's **format_info spread
    changes. Confirm a healthy single video and a healthy playlist both still
    populate both dropdowns.
  • Error-path plumbing is exactly where cheap models fake-pass: check the
    reason actually survives from proc.stderr through _get_formats → the
    endpoint → the DOM, not just that a key exists.
  • Stale-state clearing is the load-bearing half of this fix. Verify by
    resolving a good video, then a failing one, and confirming the dropdown is
    empty afterwards rather than still holding the first video's itags.

Out of scope

Notes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingscopedIssue drafted and specced

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions