Nothing files a virtual item, so every missing-episode setting and route is unreachable #196

Closed
opened 2026-09-12 08:50:58 +02:00 by Claude · 1 comment
Collaborator

The read side of virtual items is finished and nothing ever writes one, so the
whole feature is unreachable.

What is already there:

  • crates/ephyra-db/src/query/filters.rs:390 — Virtual::Excluded and
    Virtual::Only spell IsVirtualItem = 0 and = 1.
  • crates/ephyra-http/src/routes/library/tv_shows.rs:424 — a request naming
    IsMissing decides for itself, and without one the account's
    DisplayMissingEpisodes decides.
  • crates/ephyra-db/src/items/writes.rs:331 — BaseItems carries the column.

What is not there: any writer. The scan files a row per file on disk, so
IsVirtualItem is 0 on every row this server has ever written.
DisplayMissingEpisodes can therefore never change what a season lists, and
/Shows/Upcoming — which is served, tv_shows.rs:32 — can only answer out of
rows that have a file.

The task that would file them is permanently absent, and the reason is recorded
in the code:

// crates/ephyra-app/src/admin/tasks.rs:95
const ALWAYS_ABSENT: &[(&str, &str)] = &[
    ...
    (
        "TmdbRefreshUpcomingEpisodes",
        "it files virtual episodes for the ones not aired or missing, and this \
         server files no virtual item",
    ),
];

What upstream does

A virtual episode is written by the TMDb provider rather than by the scan: the
series' episode list comes back from the database, the episodes with no file
are filed as rows with IsVirtualItem = 1, and DisplayMissingEpisodes /
DisplayUnairedEpisodes decide per account whether a listing keeps them. The
scheduled task re-runs it for series whose next episode is near.

Where it would go here

crates/ephyra-app/src/metadata/tmdb/tv.rs already fetches a season
(season, line 91) and an episode (episode, line 120) by number, so the
episode list a season answers with is most of the input. The writing end is
crates/ephyra-app/src/scan/ingest.rs, and the task would join the declared
set in crates/ephyra-app/src/admin/tasks.rs — removing its ALWAYS_ABSENT
entry is part of the change, and absent()'s test asserts the list.

Open

  • Whether this is wanted at all. It is the one gap on this list that adds
    rows upstream users routinely turn off. The cheap half is honest: without it,
    two account settings and one route are dead weight.
  • Which fixture proves it. Filing a virtual episode is a network answer
    turned into a row, so it is D27's rule — the requests go in the cassette
    (crates/ephyra-app/corpus/fetched.json) and the rows upstream wrote go in
    the answer key (enriched.json), both regenerated together with
    cargo xtask reference gen-enriched-corpus. The enriched set is films; a
    series with a gap in it would have to join it.
The read side of virtual items is finished and nothing ever writes one, so the whole feature is unreachable. What is already there: - `crates/ephyra-db/src/query/filters.rs:390` — `Virtual::Excluded` and `Virtual::Only` spell `IsVirtualItem = 0` and `= 1`. - `crates/ephyra-http/src/routes/library/tv_shows.rs:424` — a request naming `IsMissing` decides for itself, and without one the account's `DisplayMissingEpisodes` decides. - `crates/ephyra-db/src/items/writes.rs:331` — `BaseItems` carries the column. What is not there: any writer. The scan files a row per file on disk, so `IsVirtualItem` is `0` on every row this server has ever written. `DisplayMissingEpisodes` can therefore never change what a season lists, and `/Shows/Upcoming` — which is served, `tv_shows.rs:32` — can only answer out of rows that have a file. The task that would file them is permanently absent, and the reason is recorded in the code: ```rust // crates/ephyra-app/src/admin/tasks.rs:95 const ALWAYS_ABSENT: &[(&str, &str)] = &[ ... ( "TmdbRefreshUpcomingEpisodes", "it files virtual episodes for the ones not aired or missing, and this \ server files no virtual item", ), ]; ``` ## What upstream does A virtual episode is written by the TMDb provider rather than by the scan: the series' episode list comes back from the database, the episodes with no file are filed as rows with `IsVirtualItem = 1`, and `DisplayMissingEpisodes` / `DisplayUnairedEpisodes` decide per account whether a listing keeps them. The scheduled task re-runs it for series whose next episode is near. ## Where it would go here `crates/ephyra-app/src/metadata/tmdb/tv.rs` already fetches a season (`season`, line 91) and an episode (`episode`, line 120) by number, so the episode list a season answers with is most of the input. The writing end is `crates/ephyra-app/src/scan/ingest.rs`, and the task would join the declared set in `crates/ephyra-app/src/admin/tasks.rs` — removing its `ALWAYS_ABSENT` entry is part of the change, and `absent()`'s test asserts the list. ## Open - **Whether this is wanted at all.** It is the one gap on this list that adds rows upstream users routinely turn off. The cheap half is honest: without it, two account settings and one route are dead weight. - **Which fixture proves it.** Filing a virtual episode is a network answer turned into a row, so it is D27's rule — the requests go in the cassette (`crates/ephyra-app/corpus/fetched.json`) and the rows upstream wrote go in the answer key (`enriched.json`), both regenerated together with `cargo xtask reference gen-enriched-corpus`. The enriched set is films; a series with a gap in it would have to join it.
Author
Collaborator

Done in 9576c87f…d984b844, and the scope question the issue asked has an
answer: build it, because the expensive half was already paid.

reference/scripts/make-sample-media.sh already lays down Firefly with 2 of
season 1's 11 episodes and Twin Peaks with 1 of season 1's 8 — so growing the
image's library, the part that redraws every snapshot cut against it, was
already done. Only the provider had to be turned on. What it cost was 58 rows
and about 15 MB of answer key.

Against that: Virtual::Excluded/Only, DisplayMissingEpisodes,
DisplayUnairedEpisodes and /Shows/Upcoming were all unreachable — two
account settings and a route that report a choice being honoured and could
not be.

What it is

crates/ephyra-app/src/scan/run/announced.rs is upstream's
TmdbMissingEpisodeProvider: the season numbers and the episode list a pass
already asks for, the episodes with no file behind them filed through
Write::without_a_file(), and IsVirtualItem bound in the scan's row.
TmdbRefreshUpcomingEpisodes leaves the absent list and joins the declared
tasks where a library asked for them.

One thing the mutations found

Gating the grace period on known.contains(id) was wrong and could never
fire
: the reader is WHERE Path IS NOT NULL, so a placeholder is never read
back. The id is named unconditionally now — a pass removes what it did not
name, so naming one that is not there removes nothing.

The cassette could not be extended

--extend failed with nothing offered for 8 of the pictures is the file
upstream kept
: the committed key was cut on the 27th of August, the local
snapshot on the 1st of September, and the database had moved between them. Both
halves were re-cut from one moment instead, which is what D28 prescribes.

Two committed tests had to follow the re-cut, and one of them was a real
finding rather than drift: a film's blob expected "DefaultVideoStreamIndex": null, and upstream's serializer omits a null member. The fixture settles it —
a physical episode carries 0 and a virtual one omits it — so the port omits
it too.

Divergences, all recorded in D44

The day is an input rather than the clock, so two runs of a scan cannot
disagree; season zero is not asked for unless specials are wanted; the
per-library opt-in is a library setting rather than a plugin's list; the
refresh interval is a task trigger rather than a second place to store the
same number; pruning is the sweep a pass already does; and the task runs a
pass over the libraries that asked rather than refreshing series one at a
time.

For whoever regenerates next

reference/snapshots/enriched is re-cut and now matches the committed corpus.
The stale one it replaced was eleven days older than the key — a regeneration
against it would have produced a wrong answer key without saying so.

Done in `9576c87f`…`d984b844`, and the scope question the issue asked has an answer: **build it, because the expensive half was already paid.** `reference/scripts/make-sample-media.sh` already lays down Firefly with 2 of season 1's 11 episodes and Twin Peaks with 1 of season 1's 8 — so growing the image's library, the part that redraws every snapshot cut against it, was already done. Only the provider had to be turned on. What it cost was 58 rows and about 15 MB of answer key. Against that: `Virtual::Excluded`/`Only`, `DisplayMissingEpisodes`, `DisplayUnairedEpisodes` and `/Shows/Upcoming` were all unreachable — two account settings and a route that report a choice being honoured and could not be. ### What it is `crates/ephyra-app/src/scan/run/announced.rs` is upstream's `TmdbMissingEpisodeProvider`: the season numbers and the episode list a pass already asks for, the episodes with no file behind them filed through `Write::without_a_file()`, and `IsVirtualItem` bound in the scan's row. `TmdbRefreshUpcomingEpisodes` leaves the absent list and joins the declared tasks where a library asked for them. ### One thing the mutations found Gating the grace period on `known.contains(id)` was wrong and **could never fire**: the reader is `WHERE Path IS NOT NULL`, so a placeholder is never read back. The id is named unconditionally now — a pass removes what it did not name, so naming one that is not there removes nothing. ### The cassette could not be extended `--extend` failed with *nothing offered for 8 of the pictures is the file upstream kept*: the committed key was cut on the 27th of August, the local snapshot on the 1st of September, and the database had moved between them. Both halves were re-cut from one moment instead, which is what D28 prescribes. Two committed tests had to follow the re-cut, and one of them was a real finding rather than drift: a film's blob expected `"DefaultVideoStreamIndex": null`, and upstream's serializer omits a null member. The fixture settles it — a physical episode carries `0` and a virtual one omits it — so the port omits it too. ### Divergences, all recorded in D44 The day is an input rather than the clock, so two runs of a scan cannot disagree; season zero is not asked for unless specials are wanted; the per-library opt-in is a library setting rather than a plugin's list; the refresh interval is a task trigger rather than a second place to store the same number; pruning is the sweep a pass already does; and the task runs a pass over the libraries that asked rather than refreshing series one at a time. ### For whoever regenerates next `reference/snapshots/enriched` is re-cut and now matches the committed corpus. The stale one it replaced was eleven days older than the key — a regeneration against it would have produced a wrong answer key without saying so.
Sign in to join this conversation.
No description provided.