Skip to content

fix: report YouTube videos that never load instead of keeping the previous one - #590

Draft
FelipeDefensor wants to merge 2 commits into
devfrom
fix/youtube-silent-load-failure
Draft

fix: report YouTube videos that never load instead of keeping the previous one#590
FelipeDefensor wants to merge 2 commits into
devfrom
fix/youtube-silent-load-failure

Conversation

@FelipeDefensor

Copy link
Copy Markdown
Collaborator

The bug

Reported while testing #450, but pre-existing on dev — the code involved is untouched by that PR.

Open a file with a valid YouTube URL, then open a file whose URL points at a video that cannot be played. No error appears, the file looks like it opened, and the previous video is still loaded and playing — while TiLiA reports the new URL as the loaded media. The duration query then picks up the old video's duration and applies it to the new file.

Why it happens

Two independent things:

  • _engine_load_media fires loadVideo() into the page and return True unconditionally. Loading is asynchronous, so Player.load_media always takes the success path: on_media_load_done sets media_path, posts PLAYER_URL_CHANGED and sets is_media_loaded = True before anything knows whether the video exists.
  • YouTube reports some failures through onError, but not all of them. Loading an unplayable video over one that is already playing leaves the previous video in place and raises nothing. On a fresh session the same file does produce an error, which is why the failure only shows up on the second open.

So the load has to be checked, not assumed.

The fix

getLoadState() reports which video the player actually holds alongside its duration. Every load polls it (500 ms, up to ~10 s) until that video is the requested one. A load that never arrives is reported to the user and unloads the media, so the player stops claiming a video it does not have.

media_path is deliberately restored after unload_media(): it is what the file declares, and clearing it would drop the URL from the file on the next save.

Two things fall out of routing every load through the poll:

  • PlayerTracker no longer queries the duration on UNSTARTED. That query could not detect a failed load — a stale video never changes state — and would now race the poll into a duplicate error. It loses the page, on_duration_available and get_video_id constructor arguments with it.
  • The loadFinished connection for a deferred load is single-shot. A plain connect left load_video attached and re-ran it on every later page load, which would now also re-start its poll.

Repro bundle

Following the guideline in #580 (not merged yet, but this PR is a fix that wants human testing).

One-time setup — not part of the launch line, since re-running it aborts when the branch is checked out in another worktree and --force discards local edits:

gh pr checkout <this PR>

Then, from the repo root:

uv run tilia "$PWD/repro/yt-ok.tla"

The reported scenario: with yt-ok.tla playing, File → Open repro/yt-missing-video.tla.

  • Was broken: no error, the previous video keeps playing, the new URL is reported as loaded.
  • Correct: within ~10 s, "Could not load this video. It may have been removed, made private, or the video ID may be wrong.", the old video stops, controls return to their no-media state.

The two other criteria — a fresh session with the missing video, and a regression check that a video which does load still loads — are in repro/REPRO.md. Both fixtures come out of TiLiA's own CLI via repro/build.sh; no .tla JSON is hand-authored. Delete repro/*.tla before merge.

Base-branch check: the same two files on dev show the broken behavior. The fixtures are branch-agnostic, so git switch dev and relaunch is the whole comparison.

Tests

tests/player/test_youtube_player.py, 10 tests. The player needs a real QWebEngineView, the IFrame API and a network connection, so they stand in a stub view and drive the load-outcome logic: failure is reported, media is unloaded, media_path survives, a slow load is not mistaken for a failure, and a poll for a superseded video is ignored.

Full suite passes serially. Under -n auto 13 unrelated tests fail on this branch and pass on their own — the known local xdist flakiness, not this change.

Related

fix/youtube-error-messages rewrites the wording of the onError messages. It touches a different part of youtube.html and does not overlap with this.

…vious one

Loading is fire-and-forget: _engine_load_media fires loadVideo() into the
page and returns True unconditionally, so on_media_load_done always runs
and TiLiA records the new URL as loaded. YouTube reports some failures
through onError, but not all of them -- loading an unplayable video over
one that is already playing leaves the previous video in place and raises
nothing. The file then appears to open cleanly while the old video keeps
playing under the new URL, and the duration query happily picks up the old
video's duration.

The load is now checked rather than assumed. getLoadState() reports which
video the player actually holds alongside its duration, and each load polls
it until that video is the requested one. A load that never arrives is
reported to the user and unloads the media, so the player stops claiming a
video it does not have.

media_path is deliberately restored after unload_media(): it is what the
file declares, and clearing it would drop the URL from the file on the next
save.

Two things fall out of routing every load through the poll:

- PlayerTracker no longer queries the duration on UNSTARTED. That query
  could not detect a failed load (a stale video never changes state) and
  would now race the poll into a duplicate error. It loses the page,
  on_duration_available and get_video_id constructor arguments with it.
- The loadFinished connection for a deferred load is single-shot. A plain
  connect left load_video attached and re-ran it on every later page load,
  which would now also re-start its poll.

The player needs a real QWebEngineView, the IFrame API and a network
connection, so the tests stand in a stub view and drive the load-outcome
logic. The user-visible scenario is in the repro bundle.
Two fixtures and their generator, plus the acceptance criteria. Delete the
.tla files before merge -- the generator is the durable part.
@FelipeDefensor
FelipeDefensor marked this pull request as draft August 27, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant