* feat(catalog): Latest Episodes sort — order series by newest episode file
Adds a latest_episode_added sort so users can see which shows received
new episodes. Today's recently-added surfaces reflect when the SERIES
was first added: linking a new episode file never bumps the series'
media_item_libraries.first_seen_at (ON CONFLICT DO NOTHING), so a
long-running show with a fresh episode sorts as stale (#202).
- New denorm media_items.latest_episode_added_at (migration + backfill
+ partial series index), mirroring the last_air_date_at precedent.
Source of truth is episode_libraries.first_seen_at; the three insert
paths (UpdateEpisodeLink, BulkLinkEpisodesBySeries, scanner folder
restore) bump the parent series atomically in the same statement,
monotonically via GREATEST, and only for genuinely new links.
- Sort registered in both frameworks: querySortDefs (sections + smart
collections + /v1/catalog pick it up automatically via
QuerySortFieldSet) and the browse buildOrderByPlan path.
- Jellyfin compat: SortBy=DateLastContentAdded now maps to the new sort
instead of silently collapsing to series creation date — Jellyfin
clients already send this for the TV "Latest" shelf, so they get the
correct behavior with no client changes. DatePlayed keeps its old
created_at mapping instead of piggybacking.
- Web sort picker gains "Latest Episode Added" (series scope).
Additive-only per v1 API rules: new sort value, no field/status changes.
Part of #202Fixes#202
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): include latest_episode_added in the api QuerySort field union
The picker-side QuerySortField gained the value but the api-layer
QuerySort union did not, breaking the production tsc build.
Part of #202
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(scanner): recompute latest_episode_added_at when episode memberships are removed
The denorm was only ever bumped upward (GREATEST) at insert time, but
UpdateEpisodeLink also deletes the old episode's library membership on
re-link, and reconciliation/path-prefix clears remove memberships too —
leaving a stale timestamp that kept the series sorting as recently
updated. All removal paths now run in a transaction and finish with a
shared full MAX() recompute (catalog.RecomputeSeriesLatestEpisodeAdded)
that also resets to NULL when no memberships remain, mirroring the
last_air_date_at maintenance pattern.
Sequential statements are load-bearing here: data-modifying CTEs are
invisible to reads in the same statement, which also silently no-op'd
the old path-prefix membership delete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(jellycompat): keep DateLastContentAdded scoped to series-only requests
mapSortBy runs for every /Items browse, so the latest_episode_added
mapping leaked into movie and untyped requests where the column is
always NULL, destroying the previous created_at ordering. The sort now
falls back to created_at unless IncludeItemTypes is exactly Series.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>