The dashboard lists 2 of the 15 jobs upstream runs #97

Closed
opened 2026-08-13 22:59:38 +02:00 by Claude · 2 comments
Collaborator

Upstream registers 20 IScheduledTask implementations at the pinned ref. Setting aside the three that are not ours — PluginUpdates, and the Live TV pair RefreshInternetChannels and RefreshGuide, both of which hide themselves when nothing is configured — that leaves 17, of which we declare 2 (crates/jellyfin-app/src/admin/tasks.rs).

Key What it does Interval upstream Here
RefreshLibrary scan the libraries 12 h yes
RefreshPeople tidy the credits 7 d yes
RefreshChapterImages extract a jpg per chapter daily no
RefreshTrickplayImages build the scrubbing sheets daily no
MoveTrickplayImages move sheets to the configured location — no
KeyframeExtraction fill KeyframeData — no
TaskExtractMediaSegments intro/credit markers 12 h no
AudioNormalization LUFS per track and per album 24 h no
OptimizeDatabaseTask VACUUM/ANALYZE 6 h no
DeleteTranscodeFiles sweep the transcode directory startup + 24 h no
DeleteCacheFiles sweep the cache 24 h no
CleanLogFiles sweep the logs 24 h no
CleanActivityLog trim the activity table — no
CleanupUserDataTask drop user data for rows that are gone — no
TmdbRefreshUpcomingEpisodes upcoming and missing episodes days no
DownloadSubtitles fetch missing subtitles 24 h no
DownloadLyrics fetch missing lyrics 24 h no

The last two need a remote provider that upstream itself only gets from a plugin, so in a stock install they are registered and do nothing — they are on the list because the dashboard shows them and an operator counts them.

The module doc in tasks.rs gives the rule this was written under, and it is a good rule: one named here that did nothing would be a button an operator presses and watches succeed. So this issue is not "declare fifteen tasks" — it is the umbrella for the ones filed separately, and it closes when they do.

Upstream's list is Emby.Server.Implementations/ScheduledTasks/Tasks/, plus MediaBrowser.Providers/{Trickplay,MediaInfo,Lyric}/, plus src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/.


Where to start

This is an umbrella. The work is in the issues it points at: chapter images #98, trickplay #99,
keyframes #100, media segments #101, audio normalization #102, the six
maintenance sweeps #103, the activity log #104, and TMDb's upcoming-episodes
task (with #106) — plus the scheduler itself, #95, without which any of them
would be a button rather than a job.

The rule to keep while closing it is the one already written at the top of
crates/jellyfin-app/src/admin/tasks.rs: one named here that did nothing would be a
button an operator presses and watches succeed.
So a task is declared when it
does something, not when the route can list it.

Upstream's list is Emby.Server.Implementations/ScheduledTasks/Tasks/, plus
MediaBrowser.Providers/{Trickplay,MediaInfo,Lyric}/, plus
src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/. The keys in the table above
are upstream's own and are what a client matches a task by — take them from the
source rather than retyping them.

How to know it is done

Every row in the table is either implemented or has an argued reason to be
absent, and /ScheduledTasks diffs clean against the reference for the tasks
both servers have.

House rules

CLAUDE.md is the short version and these are the parts that bite: tests run
under cargo nextest; upstream's source is the checkout in upstream/ at the
pin
(v12.0-rc5, 6d501ba4) and not the clone beside the repository;
generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and Dn references stay out of code and comments.
ARCHITECTURE.md decides which crate a thing goes in and which dependency edges
are illegal, DECISIONS.md says why a rule exists.

The reference pair is cargo xtask reference up (and reference provision,
which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,
with diff-db for the two databases. crates/jellyfin-harness/README.md says
what a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.

Upstream registers 20 `IScheduledTask` implementations at the pinned ref. Setting aside the three that are not ours — `PluginUpdates`, and the Live TV pair `RefreshInternetChannels` and `RefreshGuide`, both of which hide themselves when nothing is configured — that leaves **17**, of which we declare **2** (`crates/jellyfin-app/src/admin/tasks.rs`). | Key | What it does | Interval upstream | Here | |---|---|---|---| | `RefreshLibrary` | scan the libraries | 12 h | yes | | `RefreshPeople` | tidy the credits | 7 d | yes | | `RefreshChapterImages` | extract a jpg per chapter | daily | no | | `RefreshTrickplayImages` | build the scrubbing sheets | daily | no | | `MoveTrickplayImages` | move sheets to the configured location | — | no | | `KeyframeExtraction` | fill `KeyframeData` | — | no | | `TaskExtractMediaSegments` | intro/credit markers | 12 h | no | | `AudioNormalization` | LUFS per track and per album | 24 h | no | | `OptimizeDatabaseTask` | `VACUUM`/`ANALYZE` | 6 h | no | | `DeleteTranscodeFiles` | sweep the transcode directory | startup + 24 h | no | | `DeleteCacheFiles` | sweep the cache | 24 h | no | | `CleanLogFiles` | sweep the logs | 24 h | no | | `CleanActivityLog` | trim the activity table | — | no | | `CleanupUserDataTask` | drop user data for rows that are gone | — | no | | `TmdbRefreshUpcomingEpisodes` | upcoming and missing episodes | days | no | | `DownloadSubtitles` | fetch missing subtitles | 24 h | no | | `DownloadLyrics` | fetch missing lyrics | 24 h | no | The last two need a remote provider that upstream itself only gets from a plugin, so in a stock install they are registered and do nothing — they are on the list because the dashboard shows them and an operator counts them. The module doc in `tasks.rs` gives the rule this was written under, and it is a good rule: *one named here that did nothing would be a button an operator presses and watches succeed.* So this issue is not "declare fifteen tasks" — it is the umbrella for the ones filed separately, and it closes when they do. Upstream's list is `Emby.Server.Implementations/ScheduledTasks/Tasks/`, plus `MediaBrowser.Providers/{Trickplay,MediaInfo,Lyric}/`, plus `src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/`. --- ## Where to start This is an umbrella. The work is in the issues it points at: chapter images #98, trickplay #99, keyframes #100, media segments #101, audio normalization #102, the six maintenance sweeps #103, the activity log #104, and TMDb's upcoming-episodes task (with #106) — plus the scheduler itself, #95, without which any of them would be a button rather than a job. The rule to keep while closing it is the one already written at the top of `crates/jellyfin-app/src/admin/tasks.rs`: *one named here that did nothing would be a button an operator presses and watches succeed.* So a task is declared when it does something, not when the route can list it. Upstream's list is `Emby.Server.Implementations/ScheduledTasks/Tasks/`, plus `MediaBrowser.Providers/{Trickplay,MediaInfo,Lyric}/`, plus `src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/`. The keys in the table above are upstream's own and are what a client matches a task by — take them from the source rather than retyping them. ## How to know it is done Every row in the table is either implemented or has an argued reason to be absent, and `/ScheduledTasks` diffs clean against the reference for the tasks both servers have. ### House rules `CLAUDE.md` is the short version and these are the parts that bite: tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin** (`v12.0-rc5`, `6d501ba4`) and not the clone beside the repository; generated files are regenerated rather than edited; a comment says what the code does, not how it got there; and `Dn` references stay out of code and comments. `ARCHITECTURE.md` decides which crate a thing goes in and which dependency edges are illegal, `DECISIONS.md` says why a rule exists. The reference pair is `cargo xtask reference up` (and `reference provision`, which is idempotent); a run is `cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR`, with `diff-db` for the two databases. `crates/jellyfin-harness/README.md` says what a diff means and what an ignore entry has to carry — every entry needs a reason and an expiry milestone, and an adds-only history is the smell the risk table names.
Author
Collaborator

Checked the table against the reference rather than against the C#, with the pair up. /ScheduledTasks on the pinned image lists 20; setting aside PluginUpdates and the two Live TV rows it hides when nothing is configured leaves 17, of which this server now declares 11 — up from 2 when this was filed.

Batch C landed the six maintenance sweeps (#103). Each was measured against the reference by id: /ScheduledTasks/{id} for all six diffs clean — the key, the name, the category, the description and the triggers agree byte for byte, and the six ids are in tasks.json so they go on being diffed.

Key Here Where it stands
RefreshLibrary yes
RefreshPeople yes
RefreshChapterImages yes #98
KeyframeExtraction yes #100
AudioNormalization yes #102
OptimizeDatabaseTask yes #103
DeleteTranscodeFiles yes #103
DeleteCacheFiles yes #103
CleanLogFiles yes #103
CleanActivityLog yes #103, and #104 gave it rows to sweep
CleanupUserDataTask yes #103
RefreshTrickplayImages no #99, batch D
MoveTrickplayImages no #99, batch D
TaskExtractMediaSegments no argued absent — #101
TmdbRefreshUpcomingEpisodes no open — see below
DownloadSubtitles no argued absent
DownloadLyrics no argued absent

The four still absent, and why

RefreshTrickplayImages and MoveTrickplayImages are #99's, in batch D. They are the two rows this issue cannot close without.

TaskExtractMediaSegments is settled and stays absent. #101 measured it: with no provider registered upstream's own task completes, writes no row and marks no item, and there is no per-item analysed state in the schema at all. Declaring it would be the button admin/tasks.rs refuses in its module doc. Recorded in MILESTONES.md.

DownloadSubtitles and DownloadLyrics are the same argument and it holds: both need a remote provider upstream itself only gets from a plugin, so in a stock install they are registered and do nothing. The original text called them "on the list because the dashboard shows them and an operator counts them" — that is not enough under the module's own rule, and the honest position is that declaring either would be a name with no work behind it. Filing them as argued absent rather than to do, on the same evidence #101 was closed on.

TmdbRefreshUpcomingEpisodes is the one row with no argument yet. Unlike the three above it has a provider this server could drive: #106 landed the TMDb TV side, and the closing note there does not say what happened to TmdbMissingEpisodeProvider or the task that runs it. /Shows/Upcoming is still answered from whatever premiere dates the fixture happens to carry. Someone should either port it or write down why not — it does not belong on the argued-absent list until somebody has looked.

What this closes on

Twelve of seventeen rows are now either implemented or argued. Left: the two trickplay rows (#99, batch D) and the upcoming-episodes task above. This issue closes when #99 lands and the last row has an answer either way.

Checked the table against the reference rather than against the C#, with the pair up. `/ScheduledTasks` on the pinned image lists **20**; setting aside `PluginUpdates` and the two Live TV rows it hides when nothing is configured leaves **17**, of which this server now declares **11** — up from 2 when this was filed. Batch C landed the six maintenance sweeps (#103). Each was measured against the reference by id: `/ScheduledTasks/{id}` for all six diffs clean — the key, the name, the category, the description and the triggers agree byte for byte, and the six ids are in `tasks.json` so they go on being diffed. | Key | Here | Where it stands | |---|---|---| | `RefreshLibrary` | yes | | | `RefreshPeople` | yes | | | `RefreshChapterImages` | yes | #98 | | `KeyframeExtraction` | yes | #100 | | `AudioNormalization` | yes | #102 | | `OptimizeDatabaseTask` | **yes** | #103 | | `DeleteTranscodeFiles` | **yes** | #103 | | `DeleteCacheFiles` | **yes** | #103 | | `CleanLogFiles` | **yes** | #103 | | `CleanActivityLog` | **yes** | #103, and #104 gave it rows to sweep | | `CleanupUserDataTask` | **yes** | #103 | | `RefreshTrickplayImages` | no | #99, batch D | | `MoveTrickplayImages` | no | #99, batch D | | `TaskExtractMediaSegments` | no | **argued absent** — #101 | | `TmdbRefreshUpcomingEpisodes` | no | **open** — see below | | `DownloadSubtitles` | no | **argued absent** | | `DownloadLyrics` | no | **argued absent** | ## The four still absent, and why **`RefreshTrickplayImages` and `MoveTrickplayImages`** are #99's, in batch D. They are the two rows this issue cannot close without. **`TaskExtractMediaSegments`** is settled and stays absent. #101 measured it: with no provider registered upstream's own task completes, writes no row and marks no item, and there is no per-item analysed state in the schema at all. Declaring it would be the button `admin/tasks.rs` refuses in its module doc. Recorded in `MILESTONES.md`. **`DownloadSubtitles` and `DownloadLyrics`** are the same argument and it holds: both need a remote provider upstream itself only gets from a plugin, so in a stock install they are registered and do nothing. The original text called them "on the list because the dashboard shows them and an operator counts them" — that is not enough under the module's own rule, and the honest position is that declaring either would be a name with no work behind it. Filing them as *argued absent* rather than *to do*, on the same evidence #101 was closed on. **`TmdbRefreshUpcomingEpisodes` is the one row with no argument yet.** Unlike the three above it has a provider this server could drive: #106 landed the TMDb TV side, and the closing note there does not say what happened to `TmdbMissingEpisodeProvider` or the task that runs it. `/Shows/Upcoming` is still answered from whatever premiere dates the fixture happens to carry. Someone should either port it or write down why not — it does not belong on the argued-absent list until somebody has looked. ## What this closes on Twelve of seventeen rows are now either implemented or argued. Left: the two trickplay rows (#99, batch D) and the upcoming-episodes task above. This issue closes when #99 lands and the last row has an answer either way.
Author
Collaborator

Closing. The count in the title was stale twice over, and the corrected accounting is measured off the reference rather than read out of the C#.

GET /ScheduledTasks on the pinned reference returns 20, of which two carry IsHidden: true — RefreshGuide and RefreshInternetChannels, the Live TV pair. So a dashboard upstream shows 18, not the 17 this issue set aside PluginUpdates to reach: upstream does show it, under an Application category. This server declares 12.

The six

Each is absent because it would find nothing to do, which is this file's own rule rather than a new one:

Key Why not
PluginUpdates nothing here loads a plugin
DownloadSubtitles upstream registers no ISubtitleProvider of its own — only the interface, the manager and the DI hook; every fetcher is a plugin's
DownloadLyrics the same for ILyricProvider. CoreAppHost.cs:101 populates it from GetExportTypes alone
TaskExtractMediaSegments the rows and routes exist, but the task's whole body is RunSegmentPluginProviders, and there is no in-box IMediaSegmentProvider
TmdbRefreshUpcomingEpisodes it files virtual episodes for the unaired and missing ones; nothing here writes a row with IsVirtualItem set
MoveTrickplayImages its body is gated on libraryOptions.SaveTrickplayWithMedia, which this server does not read — every sheet it writes is in the data directory

The last two are gaps rather than permanent decisions. MoveTrickplayImages becomes a task the moment the library option it services is honoured, which is #147. Nothing files the virtual-item one; it is noted in DECISIONS.md rather than left implicit.

The twelve, and what was actually unmeasured

All twelve match the reference exactly on key, name, description, category and triggers — checked field by field against /ScheduledTasks on the running instance, and against each task's source at the pin.

The gap this issue's second criterion was pointing at was not the count. scenarios/tasks.json diffed eight of the twelve by id and never fetched the listing, so four tasks both servers have — KeyframeExtraction, AudioNormalization, RefreshChapterImages, RefreshTrickplayImages — were diffed against nothing at all. They are in the scenario now. Replayed against the pair: 22 responses, 0 differences, and the four new ids resolve on both sides, which incidentally proves the id derivation for all four.

What is recorded

  • jellyfin_app::admin::tasks::ABSENT — the six and the reason for each.
  • A test beside it holding the eighteen keys the reference answers with, so a task joining or leaving either list fails rather than drifts. Verified to fail when the accounting is wrong.
  • DECISIONS.md D34 — the rule, the two rejected alternatives, and why the listing carries no ignore entry: hiding @body on GET /ScheduledTasks would hide the whole route, and the difference is six array members rather than a field. This also settles the position #127 left open when it sorted the listing by name — the per-task reads were meant to stand until the count caught up, and for four of the six it never will.
  • crates/jellyfin-harness/README.md — why the listing is not asked for.

Landed in 05901d7.

Closing. The count in the title was stale twice over, and the corrected accounting is measured off the reference rather than read out of the C#. `GET /ScheduledTasks` on the pinned reference returns **20**, of which two carry `IsHidden: true` — `RefreshGuide` and `RefreshInternetChannels`, the Live TV pair. So a dashboard upstream shows **18**, not the 17 this issue set aside `PluginUpdates` to reach: upstream does show it, under an `Application` category. This server declares **12**. ## The six Each is absent because it would find nothing to do, which is this file's own rule rather than a new one: | Key | Why not | |---|---| | `PluginUpdates` | nothing here loads a plugin | | `DownloadSubtitles` | upstream registers no `ISubtitleProvider` of its own — only the interface, the manager and the DI hook; every fetcher is a plugin's | | `DownloadLyrics` | the same for `ILyricProvider`. `CoreAppHost.cs:101` populates it from `GetExportTypes` alone | | `TaskExtractMediaSegments` | the rows and routes exist, but the task's whole body is `RunSegmentPluginProviders`, and there is no in-box `IMediaSegmentProvider` | | `TmdbRefreshUpcomingEpisodes` | it files virtual episodes for the unaired and missing ones; nothing here writes a row with `IsVirtualItem` set | | `MoveTrickplayImages` | its body is gated on `libraryOptions.SaveTrickplayWithMedia`, which this server does not read — every sheet it writes is in the data directory | The last two are gaps rather than permanent decisions. `MoveTrickplayImages` becomes a task the moment the library option it services is honoured, which is #147. Nothing files the virtual-item one; it is noted in `DECISIONS.md` rather than left implicit. ## The twelve, and what was actually unmeasured All twelve match the reference exactly on key, name, description, category and triggers — checked field by field against `/ScheduledTasks` on the running instance, and against each task's source at the pin. The gap this issue's second criterion was pointing at was not the count. `scenarios/tasks.json` diffed **eight** of the twelve by id and never fetched the listing, so four tasks both servers have — `KeyframeExtraction`, `AudioNormalization`, `RefreshChapterImages`, `RefreshTrickplayImages` — were diffed against nothing at all. They are in the scenario now. Replayed against the pair: **22 responses, 0 differences**, and the four new ids resolve on both sides, which incidentally proves the id derivation for all four. ## What is recorded - `jellyfin_app::admin::tasks::ABSENT` — the six and the reason for each. - A test beside it holding the eighteen keys the reference answers with, so a task joining or leaving either list fails rather than drifts. Verified to fail when the accounting is wrong. - `DECISIONS.md` D34 — the rule, the two rejected alternatives, and why the listing carries no ignore entry: hiding `@body` on `GET /ScheduledTasks` would hide the whole route, and the difference is six array members rather than a field. This also settles the position #127 left open when it sorted the listing by name — the per-task reads were meant to stand until the count caught up, and for four of the six it never will. - `crates/jellyfin-harness/README.md` — why the listing is not asked for. Landed in 05901d7.
Sign in to join this conversation.
No description provided.