mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
feat(playlist): open recognized M3U movies in the VOD detail view (#1420)
M3U entries recognized as movie files now open in the portals' two-state VOD detail view, fed by TMDB metadata instead of the empty EPG zone. Watch-first: activation still plays immediately, with plot, cast, rating and artwork below the player; Escape reveals the Browse hero. Recognition is a synchronous URL-shape heuristic (movie container extension or an Xtream-style /movie/ path; radio, DASH, /series/ paths and episode-marker names keep today's live layout), gated on TMDB enrichment plus the new default-on Settings.m3uVodDetails toggle. Works in Electron and the PWA. Review follow-ups included: the playback payload no longer carries TMDB fields (its identity is the player's source-application key), the persisted volume reaches the player and survives Browse → Play, the enrichment guard keys on the full lookup identity, and the saved engine mounts first time instead of briefly falling back to Video.js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82a2948ffd
commit
f7bb3a13db
49 files changed
+2344
-146
No files matched your search
@@ -1062,6 +1062,76 @@ bypass Playwright interception) and
|
||||
`electron-backend-e2e:src/dash-clearkey.e2e.ts` (real ClearKey EME in
|
||||
Electron).
|
||||
|
||||
## Movie Recognition (VOD Detail View)
|
||||
|
||||
M3U playlists routinely mix live channels with movie FILES ("Movies"/"VOD"
|
||||
groups, Xtream-derived exports). A recognized movie swaps the player + EPG
|
||||
zone for the same two-state VOD detail shell the portals use, fed by TMDB
|
||||
metadata. Works in both Electron and the PWA.
|
||||
|
||||
**Gate** (`VideoPlayerComponent.showMovieDetail`): the branch activates only
|
||||
when ALL hold — the URL-shape heuristic recognizes the channel, TMDB
|
||||
enrichment is enabled (`TmdbEnrichmentService.isEnabled()`), and
|
||||
`Settings.m3uVodDetails` is not `false` (default on; the checkbox lives in
|
||||
Settings → Metadata (TMDB) under the master toggle). The decision is
|
||||
**synchronous** — layout is chosen at activation; the async TMDB lookup only
|
||||
patches metadata into an already-mounted view, so a missed match never causes
|
||||
a layout jump.
|
||||
|
||||
**Detection** (`isLikelyM3uMovie` in
|
||||
`libs/shared/m3u-utils/src/lib/m3u-vod-detection.util.ts`): a movie-file
|
||||
container extension (`mkv`, `mp4`, `avi`, … — deliberately NOT `ts`/`m3u8`/
|
||||
`m4s` which deliver live streams, and not `mpd` which has its own DASH path)
|
||||
OR an Xtream-style `/movie/`, `/movies/`, `/vod/` path segment. Exclusions
|
||||
fail toward today's live layout: `radio="true"`, DASH URLs, `/series/` paths,
|
||||
and names carrying an episode marker (`S01E02`, `1x02`, "2 серия",
|
||||
"Season 3" — the season word list is shared with the title normalizer).
|
||||
Known accepted cost: "Star Wars: Episode 1" is skipped.
|
||||
|
||||
**Flow** (`m3u-vod-detail/` in `libs/playlist/m3u/feature-player`):
|
||||
|
||||
- `M3uVodDetailComponent` hosts `PortalDetailShellComponent` +
|
||||
`PortalInlinePlayerComponent`. **Watch-first**: clicking a channel keeps
|
||||
M3U zapping semantics — playback starts immediately, the About block
|
||||
renders below where the EPG zone would be; Escape/close reveals the Browse
|
||||
hero, Play re-enters watch. The hero back arrow is hidden (the channel
|
||||
sidebar next to the detail IS the navigation).
|
||||
- The parent passes its already-built `embeddedPlayback()`
|
||||
(`ResolvedPortalPlayback` with headers/EXTVLCOPT resolved) plus its shared
|
||||
persisted `volume()`; the host only overrides `isLive: false` (seek
|
||||
bar/VOD semantics). The payload's OBJECT IDENTITY is the player's
|
||||
source-application key (`createWebPlayerApplicationState` mints a new
|
||||
source revision for any new payload), so it must never depend on TMDB
|
||||
signals — folding the resolved title/poster in there restarted the movie
|
||||
the moment enrichment landed. Metadata lives in the hero/About
|
||||
presentation only.
|
||||
- `PortalInlinePlayerComponent` gained an optional `volume` input
|
||||
(default `1`) for this host: the M3U player owns a persisted volume shared
|
||||
across its channels, while the portals keep the engines' own default. The
|
||||
parent re-reads that bus per channel AND on the host's `playbackStarted`
|
||||
output, because Browse → Play remounts the engine without a channel change
|
||||
and the engines only ever write to the bus.
|
||||
|
||||
**Coverage:** `apps/web-e2e/src/m3u-movie-details.e2e.ts` drives the workflow
|
||||
against the real composition with a routed TMDB mock — recognition, async
|
||||
metadata, the live-channel fallback, and the volume the remount must keep.
|
||||
|
||||
- `M3uVodMetadataService` (component-provided) calls
|
||||
`TmdbEnrichmentService.enrichMovie({ title: channel.name, year:
|
||||
releaseTagYear(channel.name) })` — the resolver already normalizes titles
|
||||
and caches verdicts; the service only adds the release-year hint and a
|
||||
staleness guard keyed on `channel.id` so zapping mid-request drops the
|
||||
stale resolution.
|
||||
- No TMDB match keeps the thin provider presentation (entry name + logo) in
|
||||
the detail layout rather than jumping back to the EPG view.
|
||||
- External MPV/VLC users keep Browse: the m3u-state effects launch the
|
||||
external player on activation exactly as before, and
|
||||
`inlinePlayerAvailable=false` means the host never mounts an inline player.
|
||||
|
||||
**Out of scope (v1)**: series episodes (detection skips them), playback
|
||||
position persistence for M3U movies, clickable cast chips (no
|
||||
`actor/:personId` route in the M3U tree), and downloads.
|
||||
|
||||
## Interfaces
|
||||
|
||||
### Channel Interface
|
||||
|
||||
@@ -17,6 +17,12 @@ Related:
|
||||
cast, director, genres, rating, artwork) via a **field-level merge** — the
|
||||
provider stays authoritative for stream-related data and for any field
|
||||
TMDB cannot fill.
|
||||
- The M3U player is a consumer too: an entry recognized as a movie file opens
|
||||
in the VOD detail shell fed purely by an `enrichMovie` lookup (there is no
|
||||
provider payload to merge into). Gated by the additional
|
||||
`Settings.m3uVodDetails` toggle (default on) below the master switch; see
|
||||
"Movie Recognition (VOD Detail View)" in
|
||||
`docs/architecture/m3u-playlist-module.md`.
|
||||
- Enrichment is **opt-in** via `Settings > Metadata (TMDB)` because it sends
|
||||
movie/series titles to a third-party API. Default: disabled.
|
||||
- The detail view renders provider data **immediately**; enrichment runs
|
||||
@@ -109,12 +115,13 @@ Wrong metadata is worse than no metadata, so id resolution is conservative:
|
||||
"chapter two"), so a name mismatch alone says more about our inputs
|
||||
than about the id.
|
||||
|
||||
A 404 is the one hard verdict: the id is recorded as dead
|
||||
(`badProviderId:<id>` row), skipped next time, and the title search
|
||||
takes over. Transient failures (auth, rate limit, 5xx, offline) neither
|
||||
disable the id nor trigger a search — that request would hit the same
|
||||
outage, and a title match that did come back would be weaker evidence
|
||||
than the id already in hand.
|
||||
A 404 is the one hard verdict: the id is recorded as dead
|
||||
(`badProviderId:<id>` row), skipped next time, and the title search
|
||||
takes over. Transient failures (auth, rate limit, 5xx, offline) neither
|
||||
disable the id nor trigger a search — that request would hit the same
|
||||
outage, and a title match that did come back would be weaker evidence
|
||||
than the id already in hand.
|
||||
|
||||
2. Otherwise `/search/movie` (or `/search/tv`) runs with the normalized
|
||||
title. Normalization strips bracketed tags, quality markers (`4K`,
|
||||
`1080p`, `MULTI`, …), leading language prefixes (`EN - `), diacritics,
|
||||
@@ -407,7 +414,10 @@ supports CORS so the PWA calls the API directly).
|
||||
`Settings.tmdb?: { enabled: boolean; apiKey?: string }`
|
||||
(`libs/shared/interfaces/src/lib/tmdb.interface.ts`). The settings page has
|
||||
a "Metadata (TMDB)" section: enable toggle, optional API key override with a
|
||||
"check key" button (validates against `/configuration`), and a cache panel
|
||||
"check key" button (validates against `/configuration`), the M3U
|
||||
movie-recognition toggle (root-level `Settings.m3uVodDetails`, shown only
|
||||
while TMDB is enabled and bound via `[formControl]` because it is not part of
|
||||
the `tmdb` form group), and a cache panel
|
||||
showing the stored row count plus payload size with a button that drops the
|
||||
lot. Sizing is a full table scan, so it runs only once that section is the
|
||||
active one, and a failed read or clear says so instead of showing an empty
|
||||
@@ -544,27 +554,27 @@ since shipped.)
|
||||
position (no TMDB involved); the watch-progress bar is limited to
|
||||
movie/series heroes.
|
||||
|
||||
The query is built to **match what the detail view searched with**, not
|
||||
just what the card displays. A title alone is weaker identity than the
|
||||
detail page had: without a year `pickConfidentMatch` requires a single
|
||||
exact title match, which common titles never satisfy, and the miss lands
|
||||
in the negative cache under a lookup key the detail view's hit can never
|
||||
be found at. Stalker rows carry those facts —
|
||||
`extractStalkerItemTmdbHints` (`libs/shared/interfaces`) reads
|
||||
`info.name`, `info.o_name`, `info.releasedate` and `info.tmdb_id` off the
|
||||
stored entry, mirroring `enrichStalkerSelectionWithTmdb` field for field.
|
||||
A `'movie'` verdict gets a second attempt under `'tv'` — `'movie'` is what
|
||||
every row falls back to when nothing says otherwise, and an embedded-VOD
|
||||
series is a `'movie'` activity row but a show on TMDB. That retry **drops
|
||||
the id**, which is only ever valid for the media type it was resolved
|
||||
under: `/movie/<tv id>` resolves to an unrelated film. A `'tv'` verdict
|
||||
gets no retry — it is reached only on positive evidence (series category,
|
||||
`is_series`, or a non-empty episode array), and the gate cannot tell an
|
||||
adaptation sharing its show's name and year from the show itself.
|
||||
The query is built to **match what the detail view searched with**, not
|
||||
just what the card displays. A title alone is weaker identity than the
|
||||
detail page had: without a year `pickConfidentMatch` requires a single
|
||||
exact title match, which common titles never satisfy, and the miss lands
|
||||
in the negative cache under a lookup key the detail view's hit can never
|
||||
be found at. Stalker rows carry those facts —
|
||||
`extractStalkerItemTmdbHints` (`libs/shared/interfaces`) reads
|
||||
`info.name`, `info.o_name`, `info.releasedate` and `info.tmdb_id` off the
|
||||
stored entry, mirroring `enrichStalkerSelectionWithTmdb` field for field.
|
||||
A `'movie'` verdict gets a second attempt under `'tv'` — `'movie'` is what
|
||||
every row falls back to when nothing says otherwise, and an embedded-VOD
|
||||
series is a `'movie'` activity row but a show on TMDB. That retry **drops
|
||||
the id**, which is only ever valid for the media type it was resolved
|
||||
under: `/movie/<tv id>` resolves to an unrelated film. A `'tv'` verdict
|
||||
gets no retry — it is reached only on positive evidence (series category,
|
||||
`is_series`, or a non-empty episode array), and the gate cannot tell an
|
||||
adaptation sharing its show's name and year from the show itself.
|
||||
|
||||
Note the id in a stored Stalker entry is not a provider claim. Stalker
|
||||
portals never send one; its only source is a match this app already made
|
||||
and gated.
|
||||
Note the id in a stored Stalker entry is not a provider claim. Stalker
|
||||
portals never send one; its only source is a match this app already made
|
||||
and gated.
|
||||
|
||||
### Stalker backdrops on activity rows
|
||||
|
||||
@@ -588,7 +598,7 @@ negative-match TTL.
|
||||
|
||||
Only a **confirmed 404** is recorded. Transient failures (401, 429, 5xx,
|
||||
offline) leave no marker — they say nothing about the id. Neither does a
|
||||
title mismatch: that id exists and may be correct for a *different* item,
|
||||
title mismatch: that id exists and may be correct for a _different_ item,
|
||||
and since the row is keyed by id alone and shared across playlists,
|
||||
recording per-item mismatches here would deny the direct lookup to every
|
||||
other item that legitimately uses the same id.
|
||||
Reference in new issue
Block a user