From 1fe2692f77fc71574e75884e70af87d124452a3b Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 31 Jul 2026 17:27:46 +0200 Subject: [PATCH] docs(downloads): document offline details flow --- .changes/downloads-manager-mvp.md | 9 ++- CLAUDE.md | 32 ++++++++- apps/web/src/assets/i18n/ar.json | 11 +++ apps/web/src/assets/i18n/ary.json | 11 +++ apps/web/src/assets/i18n/by.json | 11 +++ apps/web/src/assets/i18n/de.json | 11 +++ apps/web/src/assets/i18n/el.json | 11 +++ apps/web/src/assets/i18n/en.json | 11 +++ apps/web/src/assets/i18n/es.json | 11 +++ apps/web/src/assets/i18n/fr.json | 11 +++ apps/web/src/assets/i18n/hu.json | 11 +++ apps/web/src/assets/i18n/it.json | 11 +++ apps/web/src/assets/i18n/ja.json | 11 +++ apps/web/src/assets/i18n/ko.json | 11 +++ apps/web/src/assets/i18n/nl.json | 11 +++ apps/web/src/assets/i18n/pl.json | 11 +++ apps/web/src/assets/i18n/pt.json | 11 +++ apps/web/src/assets/i18n/ru.json | 11 +++ apps/web/src/assets/i18n/tr.json | 11 +++ apps/web/src/assets/i18n/zh.json | 11 +++ apps/web/src/assets/i18n/zhtw.json | 11 +++ docs/architecture/download-manager.md | 72 +++++++++++-------- docs/architecture/portal-detail-navigation.md | 34 +++++++++ docs/architecture/stalker-portal.md | 12 ++++ 24 files changed, 333 insertions(+), 35 deletions(-) diff --git a/.changes/downloads-manager-mvp.md b/.changes/downloads-manager-mvp.md index 43c73c335..6d0b75ca8 100644 --- a/.changes/downloads-manager-mvp.md +++ b/.changes/downloads-manager-mvp.md @@ -3,8 +3,7 @@ type: feature area: downloads --- -Downloads now separate active transfers from a cover-size-aware offline -library and group completed episodes by series. Missing files move to Needs -attention with a safe Download again action, while ready cards open provider -details and keep local Play explicit. Card source details now live in the -overflow menu. +Downloads now open movies and series in focused offline details with saved +metadata and optional TMDB enrichment. Series show only episodes available +locally, while View in portal opens the complete online catalog and provider +playback. diff --git a/CLAUDE.md b/CLAUDE.md index a01743a2e..9630dac95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -491,7 +491,11 @@ See `docs/architecture/m3u-playlist-module.md` for complete documentation. - Stalker portal: `/workspace/stalker/:id` (children: `itv`, `vod`, `radio`, `series`, `favorites`, `recent`, `search`, `actor/:personId`, `downloads`) — `libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts` - Global collections: `/workspace/global-favorites`, `/workspace/global-recent` - Global search: `/workspace/search` (Electron-only; a guard redirects the PWA to `/workspace/sources`) -- Downloads: `/workspace/downloads` +- Downloads: `/workspace/downloads` with focused + `/workspace/downloads/:downloadId`; source-scoped equivalents are + `/workspace/xtreams/:id/downloads/:downloadId` and + `/workspace/stalker/:id/downloads/:downloadId`. Focused download details hide + the workspace context panel. - Settings: `/workspace/settings` (`/settings` redirects there) **Service Architecture** (Factory Pattern): @@ -897,6 +901,32 @@ engine` (restart required) or chrome, with browser-dependent subtitles. AirPlay, Cast, Document PiP, a PiP keyboard shortcut, and Embedded MPV popup/native support are out of scope. +**Download Manager**: + +- The desktop-only manager shares one global download store across the global, + Xtream-scoped, and Stalker-scoped routes. Completed movie and grouped-series + cards use the global Small/Medium/Large cover-grid tokens; missing completed + files move to Needs attention instead of remaining in Ready to watch. +- Ready movie and grouped-series cards open a focused local detail. Movies play + the finalized local file; series list only locally available episode rows and + every episode action targets its own downloaded file. Focused routes disable + route search and use `contextPanel: 'none'`. +- Downloads capture a versioned metadata snapshot from the rendered Xtream or + Stalker movie/episode detail at start time, including already-merged TMDB + fields. Legacy, sparse, stale, or wrong-language snapshots are safely + backfilled from row/provider metadata and optional TMDB enrichment when the + focused detail opens. +- `View in portal` resolves an exact source target and opens the provider's + normal detail in one-shot `provider-only` presentation. Provider playback and + the complete catalog remain available, while Offline/local/download actions + are hidden. This applies to Xtream movies/series and regular, embedded + `series[]`, and Ministra `is_series=1` Stalker flows. +- If a finalized file disappears while a focused detail is open, the + authoritative download list refreshes and returns to the manager. A failed + redirect leaves an actionable missing-file state with Back and Retry. +- Canonical contract: `docs/architecture/download-manager.md`; provider handoff: + `docs/architecture/portal-detail-navigation.md`. + **VOD/Series Detail Pages (two-state layout)**: - Xtream and Stalker detail pages use the shared `PortalDetailShellComponent` (`libs/ui/components/src/lib/portal-detail-shell/`) with two states: **Browse** (hero with poster/metadata/actions, episodes below) and **Watch** (hero collapses with a ~300ms morph, the inline player takes the full content width, metadata moves to an About block below the episodes) diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 650591f2a..985a404b2 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index be4f3da89..063ce9b93 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index b39e92710..de8a54ec2 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 1da6c2f96..79ba1139e 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 6766a232d..60460a449 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index a8e7fb43b..7821ca2f8 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 54562a378..1c678c123 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 5e4f78682..2806f61cd 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/hu.json b/apps/web/src/assets/i18n/hu.json index 382e635aa..8800b87a5 100644 --- a/apps/web/src/assets/i18n/hu.json +++ b/apps/web/src/assets/i18n/hu.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 65fe47395..37e6aad3e 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 5965dee1f..ac99cd73b 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index c89826023..eb96e5988 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 0976d1d35..304037f57 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 505ac7c78..b12587b2d 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 7e5a87c9b..568d7c8d1 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 2389eeb68..7d7d470a5 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -1128,6 +1128,17 @@ "SEASON": "Сезон {{season}}", "SEASON_RANGE": "Сезоны {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Скачанные эпизоды", + "OFFLINE_DETAIL": { + "AVAILABLE": "Доступно офлайн", + "PLAY": "Воспроизвести офлайн", + "VIEW_IN_PORTAL": "Открыть в портале", + "VIEW_IN_PORTAL_UNAVAILABLE": "Контент больше недоступен в исходном портале", + "DOWNLOADED_EPISODES": "Скачано эпизодов: {{count}}", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Загрузка не найдена", + "NOT_FOUND_BODY": "Эта загрузка больше недоступна.", + "BACK": "Назад к загрузкам" + }, "ARIA": { "OPEN": "Открыть {{title}}", "POSTER_UNAVAILABLE": "Обложка для {{title}} недоступна", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 2b1ec5033..41defae57 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 46d588637..1734eb2ec 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 96c4dce98..ac83eed93 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -1128,6 +1128,17 @@ "SEASON": "Season {{season}}", "SEASON_RANGE": "Seasons {{first}}–{{last}}", "DOWNLOADED_EPISODES": "Downloaded episodes", + "OFFLINE_DETAIL": { + "AVAILABLE": "Available offline", + "PLAY": "Play offline", + "VIEW_IN_PORTAL": "View in portal", + "VIEW_IN_PORTAL_UNAVAILABLE": "The source item is no longer available", + "DOWNLOADED_EPISODES": "{{count}} downloaded episodes", + "FILE_SIZE": "{{size}}", + "NOT_FOUND_TITLE": "Download not found", + "NOT_FOUND_BODY": "This download is no longer available.", + "BACK": "Back to Downloads" + }, "ARIA": { "OPEN": "Open {{title}}", "POSTER_UNAVAILABLE": "Artwork unavailable for {{title}}", diff --git a/docs/architecture/download-manager.md b/docs/architecture/download-manager.md index 3421bb331..78117c502 100644 --- a/docs/architecture/download-manager.md +++ b/docs/architecture/download-manager.md @@ -97,36 +97,45 @@ variants, contextual buttons, and theme-aware styling. playlist scope when the page is opened under a source route. VOD and episode detail views continue to render a paused download as an active Resume button (`DownloadsService.isPaused()` / `resumeDownloadByContent()`). Artwork and - titles on completed movie and grouped-series cards open the provider detail - page; the explicit Play action starts the local file. A legacy standalone - episode without a usable series id stays directly playable because there is - no reliable detail route to open. Missing completed rows show `File missing` - with `Download again`; Play and Show in folder are withheld. A file-action - race that returns `File not found` refreshes the authoritative list before - pending state clears. + titles on completed movie and grouped-series cards open the focused offline + detail for that download; the explicit card Play action still starts the + local file. A legacy standalone episode without a usable series id stays + directly playable because there is no reliable series detail to build. + Missing completed rows show `File missing` under Needs attention with + `Download again`; Play and Show in folder are withheld. A file-action race + that returns `File not found` refreshes the authoritative list and returns + the user from focused details to the manager. -## Offline detail playback +## Focused offline details -- A completed VOD with an available finalized file renders an Offline tag on - both rich and fallback detail shells. Its primary action plays the downloaded - file, while a neutral - “Play from this source” action, when a usable provider source is available, - preserves the existing provider, pinned-source, resume, and restart path. -- A completed row whose file is missing does not advertise Offline or local - playback on details; the provider playback path remains primary. -- Managed MPV/VLC state remains authoritative: Opening disables conflicting - playback actions, and Stop closes the matched external session before any - local or provider choice can run. -- Download rows carry a versioned, display-only metadata snapshot, so the - offline detail resolver can work without the owning Xtream or Stalker - provider. Sparse, stale, or wrong-language snapshots are refreshed from the - typed provider catalog and optional TMDB enrichment. A successful refresh - advances its freshness timestamp; transient TMDB failures may retain improved - provider fields without advancing the timestamp or language, so a later visit - retries. Legitimately sparse successful results are remembered in a bounded, - language-scoped runtime throttle for the same TTL, preventing persistence - reloads from starting an enrichment loop. Concurrent movie and grouped-series - refreshes are request-ordered so only the newest generation may persist. +- Ready movies and grouped series open a local-focused detail view rather than + a provider catalog page. A movie exposes local Play and Show in folder. A + series projects only completed episode rows whose finalized files are still + available, grouped into seasons; every episode Play and Show in folder action + targets that row's local file. The provider's other seasons and episodes are + deliberately absent from this view. +- `View in portal` resolves the exact Xtream or Stalker source item before the + action becomes available. It then leaves the offline view and opens the + provider's normal detail host in explicit `provider-only` presentation: the + complete online catalog and provider playback remain available, while local, + Offline, and download actions are hidden. Regular provider navigation does + not inherit this one-shot presentation state. +- A completed row that is no longer locally available is never rendered as a + ready offline detail. Direct or stale detail URLs return to the manager; if + navigation fails, the detail shell shows the missing-file error with Back and + Retry. Invalid or removed download ids render a focused not-found state. +- Movie and episode downloads capture a versioned, display-only metadata + snapshot at start time from the already-rendered Xtream or Stalker detail, + including any TMDB fields already present. The snapshot keeps provider + identity/category separately from presentation metadata and lets the offline + view render even when the source portal is unavailable. +- Legacy rows and sparse, stale, or wrong-language snapshots are backfilled + when focused details open: row metadata supplies a safe local fallback, + provider data is merged when it can be resolved, and opt-in TMDB enrichment + uses the same app language and merge rules as provider details. Successful + refreshes persist to the representative row; transient failures keep safe + improvements without falsely marking the snapshot fresh, and concurrent + refreshes are request-ordered so only the latest generation may write. - Snapshot artwork accepts only safe HTTP(S) image URLs. Renderer normalization and main-process persistence independently reject credential-shaped path and query keys, including `username`, so provider credentials cannot be cached in @@ -144,7 +153,12 @@ variants, contextual buttons, and theme-aware styling. `/workspace/xtreams/:id/downloads` and `/workspace/stalker/:id/downloads`. All three routes render the same global store; the `:id` routes derive a view-only playlist scope. -- Each scope exposes an offline detail child at `downloads/:downloadId`. +- Each scope exposes the same focused child route: + `/workspace/downloads/:downloadId`, + `/workspace/xtreams/:id/downloads/:downloadId`, and + `/workspace/stalker/:id/downloads/:downloadId`. The workspace shell treats + all three as focused content: route search is disabled and the context panel + is `none`, so provider categories are not shown beside a local-only item. - Downloads navigation is data-driven: `libs/portal/shared/util/src/lib/navigation/portal-rail-links.ts` emits a `downloads` section link (`path: [...root, 'downloads']`) for both portals, so they reuse the same download page. ## Queuing, persistence, and UX notes diff --git a/docs/architecture/portal-detail-navigation.md b/docs/architecture/portal-detail-navigation.md index 8b2d08717..863781a95 100644 --- a/docs/architecture/portal-detail-navigation.md +++ b/docs/architecture/portal-detail-navigation.md @@ -29,6 +29,15 @@ Related: opening the series from the collection grid itself remains detail-only. If the positions load fails, the target stays unconsumed and the handoff degrades to detail-only rather than starting the episode at offset zero. +- Ready Download Manager cards open one of the three focused + `downloads/:downloadId` routes. These local details hide the workspace + context panel, play only finalized local files, and show only locally + available episodes for a series. +- `View in portal` is the explicit bridge from a focused offline detail to the + source catalog. It resolves an exact provider target and passes the one-shot + `detailPresentation: 'provider-only'` navigation state. The destination keeps + provider playback and the complete provider episode catalog, but hides + Offline/local/download presentation. - Do not force both portals into the same browse/detail behavior unless the full portal detail architecture is being changed. @@ -95,6 +104,19 @@ Search behavior to preserve: - Selecting an Xtream item from search should still navigate to the canonical Xtream content type/category/item route when the item is not a live stream. +Download handoff behavior: + +- An Xtream movie handoff requires its exact VOD category and item route; a + series handoff requires its exact series category and item route. The + download snapshot's provider category is preferred, with the typed catalog + used to recover it for legacy rows. The action stays unavailable rather than + opening a bare collection route when the target cannot be resolved. +- Provider-only mode is read by the normal VOD/series detail components. It + preserves provider Play/Resume and every provider episode, while suppressing + the local Offline state and download controls. Reusing the route component + for another item must clear the mode unless that navigation explicitly + carries the marker. + ## Stalker Stalker details are represented by store state and inline detail rendering on the current screen. @@ -135,6 +157,18 @@ Behavior to preserve: - Back from the collection-owned detail should restore the previous collection tab and scope instead of resetting the collection screen to its defaults. +Download handoff behavior: + +- `View in portal` preserves Stalker's inline/store-state architecture. The + download target carries `openStalkerItem` into the normal VOD or series + category host instead of inventing a canonical item route. +- The provider-only marker follows the exact selected item through regular + series, embedded VOD `series[]`, and lazy Ministra VOD `is_series=1` flows. + All provider seasons/episodes and provider playback remain available, while + the shared VOD or series UI suppresses local Offline and download controls. + Consuming the marker is identity-scoped so a later ordinary item open returns + to normal provider presentation. + ## Decision Rule For Future Changes When deciding how a favorites/recent/search click should behave: diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md index 5b5351365..90ff6ae91 100644 --- a/docs/architecture/stalker-portal.md +++ b/docs/architecture/stalker-portal.md @@ -41,6 +41,8 @@ Primary route tree lives in - `/stalker/:id/search` - `/stalker/:id/actor/:personId` - `/stalker/:id/downloads` (shared `DownloadsComponent` from `@iptvnator/portal/downloads/feature`) +- `/stalker/:id/downloads/:downloadId` (focused local movie/series detail with + no category context panel) ## Runtime Architecture @@ -361,6 +363,16 @@ The VOD-series contract is cross-surface: - The dashboard resolves episode progress by the parent `seriesXtreamId` and renders the saved season/episode metadata. It does not infer episode numbers from provider payloads. +- Episode downloads from regular series, embedded VOD `series[]`, and lazy + Ministra VOD `is_series=1` capture the rendered parent/episode metadata and + provider category in a versioned offline snapshot. The focused Download + Manager detail uses only locally available episode rows; it does not reuse + the provider season resource as an offline availability list. +- `View in portal` hands the stored Stalker identity back to the normal + category/inline detail host with identity-scoped provider-only presentation. + All three series modes retain their complete provider seasons, episodes, and + provider playback, while Offline/local/download controls stay hidden for + that handoff. A normal Stalker item open remains unchanged. Core decision logic and normalization are centralized in: