Commit Graph
450 Commits
Author SHA1 Message Date
4gray b0558bb168 merge: retain canonical TMDB guidance from master 2026-09-21 08:32:38 +02:00
4grayandClaude Opus 5 b02d79805b fix(tmdb): a new series no longer matches its older, better-known namesake (#1648)
Metadata is looked up in the app's own language, so an unrelated older foreign
series can come back under exactly the same localized name as a recent
local-language one. `pickConfidentMatch` admitted the older row through the
series "premiered earlier" tolerance — portals report the running season's
year while TMDB reports the premiere — and then let `pickMostPopular` decide
across every admitted candidate, discarding the year evidence that had just
admitted them. The better-known show won on votes, and the newer series
rendered its poster, cast, genres and rating.

Rank admitted candidates by year evidence first (`yearEvidenceTier`: the
provider's exact year, then a year off by one, then the series tolerance) and
let popularity break ties only inside the strongest tier any candidate
reached. The tolerance stays — three of eight real lookups from one install
depend on it — but it is a last resort, not an equal. Measured over 400
Cyrillic series titles sampled from a real catalog, 20 normalized keys had a
same-titled older series and 16 of those were the more popular row.

The mirror case is accepted knowingly: a long-running show whose stated
season year happens to BE another same-titled show's premiere year now
resolves to the newer show. Only the older show's season air dates could
separate the two and a search response does not carry them, while that shape
needs three coincidences at once against one that needs none.

Search cache keys move to `|v4` with a matching startup cleanup, because a
positive row naming the wrong show stays fresh for 30 days.

Merged with `Build on windows x64` red: the checked-in Windows Embedded MPV
runtime pin points at an upstream release whose retention expired, so that
job fails repository-wide on a cold cache. Unrelated to this change; tracked
separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:18:56 +02:00
4gray cbca0383d2 merge: preserve compact guidance alongside latest master contracts 2026-09-21 08:08:49 +02:00
4gray 6d2ba2c5f2 fix(agents): reject nonportable Windows drive links 2026-09-21 07:57:13 +02:00
4gray 1cdb2019d1 ci: allow manual CodeQL validation of PR branches 2026-09-21 07:45:28 +02:00
42b41ebabc fix(epg): advance the Xtream channel list's current programme as time passes (#1647)
The "current programme" under each Xtream Live TV row was written only on
scroll-into-view, on a new EPG result, or on an EPG offset change. Nothing
re-evaluated it as wall-clock time passed, so once a programme ended the row
stayed on it until the category was left and re-entered. The progress bar
under the row never moved either.

The rows on screen now re-check themselves once a minute. A programme still on
air only has its progress bar advanced, at no request cost; once it ends the
row is re-picked, and only a programme on air or upcoming may replace it, so a
guide that has run out can never walk the row backwards. An exhausted cache is
dropped and refetched at most once per cache lifetime per channel, while an
empty answer from the provider is left alone. What is on screen stays there
until a replacement arrives, so a refreshing row never blanks out.

A programme occupies [start, stop) in every comparison, the visible slice is
read from the viewport rather than remembered, and the two services behind
this are root-provided because a live layout mounts the channel list more than
once over a single EPG queue: EpgRefillLimiter is the floor on refetching an
exhausted guide, keyed by playlist since stream ids are provider-local, and
EpgRefreshCoordinator owns the one timer and merges every mounted list's
request, because the queue is latest-wins and separate timers would cancel
each other on exactly the boundaries that matter.

Contract: docs/architecture/m3u-playlist-module.md ("Xtream channel-row
programme refresh").

Fixes #767. Supersedes #1610.

Co-Authored-By: Justin Willhite <5132924+thejdubb02@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 07:40:44 +02:00
4gray a5e24f25c1 fix(agents): exclude www autolinks from import scanning 2026-09-21 06:24:22 +02:00
4gray 9aa8339d98 fix(agents): separate parenthetical prose from handles 2026-09-21 06:09:37 +02:00
4gray 2a19b5f57f fix(agents): normalize possessive federated handles 2026-09-21 05:58:01 +02:00
4gray d3e58ca7fe fix(agents): normalize Unicode punctuation on handles 2026-09-21 05:46:30 +02:00
4gray 0fda973f09 fix(agents): normalize closing punctuation on federated handles 2026-09-21 05:34:42 +02:00
4gray 71999580dd fix(agents): handle declared package names handles and SVG use 2026-09-21 05:22:34 +02:00
4gray 91d04d94cb fix(agents): validate SVG images and conventional guides 2026-09-21 05:10:12 +02:00
4gray 87a27b5458 fix(agents): distinguish URL quotes and cover guidance variants 2026-09-21 04:58:10 +02:00
4gray 7b4a49821b fix(agents): reject file bases and preserve quoted URL boundaries 2026-09-21 04:45:54 +02:00
4gray 802d3dc43c fix(agents): reject pathless media and direct file URLs 2026-09-21 04:33:33 +02:00
4gray 47cfc1ba4a fix(agents): scan nested imports and share document suffixes 2026-09-21 04:21:52 +02:00
4gray 673d3fd791 fix(agents): handle opening prose delimiters 2026-09-21 04:09:46 +02:00
4gray 71b7204ca0 fix(agents): restrict opaque URI exemptions to parsed links 2026-09-21 03:57:50 +02:00
4gray 8d798e050b fix(agents): keep import tokens outside URI scheme matches 2026-09-21 03:45:46 +02:00
4gray 3ea051532a fix(agents): exclude opaque URI prose from import scans 2026-09-21 03:45:04 +02:00
4gray ad5050ca78 fix(agents): preserve imports after bare URL punctuation 2026-09-21 03:33:08 +02:00
4gray 5a8b233840 fix(agents): convert base file URLs to native paths 2026-09-21 03:21:08 +02:00
4gray 464d829538 fix(agents): honor HTML bases and preserve adjacent imports 2026-09-21 03:09:22 +02:00
4gray 3dbd522d70 fix(agents): validate srcdoc references and empty srcset 2026-09-21 02:56:53 +02:00
4gray a7150d1e72 fix(agents): reject empty media and ignore URL at-signs 2026-09-21 02:44:36 +02:00
4gray 6558245e12 fix(agents): normalize internal HTML URL whitespace 2026-09-21 02:32:59 +02:00
4gray 3f8b5be06d fix(agents): preserve block boundaries and validate embeds 2026-09-21 02:21:11 +02:00
4gray 6bc5688001 fix(agents): require files for media references 2026-09-21 02:09:24 +02:00
4gray 576e70fc05 fix(agents): cover document families and guidance basenames 2026-09-21 01:57:30 +02:00
4gray a2d1997e29 fix(agents): validate document formats and trim HTML URLs 2026-09-21 01:45:35 +02:00
4gray 8f76a2c456 fix(agents): handle wildcard versions and alternate documents 2026-09-21 01:33:29 +02:00
4gray 22b2d7a2ac fix(agents): unify Markdown suffix and encoded import guards 2026-09-21 01:21:42 +02:00
4gray 1dd0ec2285 fix(agents): inspect document suffix before URL fragments 2026-09-21 01:09:57 +02:00
4gray 5da10082e5 fix(agents): validate iframe document references 2026-09-21 00:58:25 +02:00
4gray 82d582bb74 fix(agents): preserve filenames across prose punctuation 2026-09-21 00:46:30 +02:00
4gray 4a73cf3828 fix(agents): normalize punctuation for extensionless imports 2026-09-21 00:34:28 +02:00
4gray d10bceab7b fix(agents): delimit package prose at Unicode punctuation 2026-09-21 00:22:37 +02:00
4gray bbbaebce83 fix(agents): count all Markdown line endings in budgets 2026-09-21 00:10:57 +02:00
4gray b863d8e4d4 fix(agents): validate image-map navigation targets 2026-09-20 23:59:28 +02:00
4gray 900050be2e fix(agents): retain visible backticks and explicit path punctuation 2026-09-20 23:47:48 +02:00
4gray 505fa356ab fix(agents): validate extensionless imports and version comparators 2026-09-20 23:35:50 +02:00
4gray cb42c74f43 fix(agents): recognize possessive package mentions 2026-09-20 23:23:11 +02:00
4gray 0ac3421594 fix(agents): decode full HTML entities and media assets 2026-09-20 23:03:17 +02:00
4gray f80a21beef feat(collections): open a Stalker live favorite inside its portal (#1639)
Live channels in Favorites and Recently viewed now offer "Open in <playlist>"
for Stalker portals, the counterpart of the VOD "View in portal" action. The
chip in the programme panel and the channel's context menu jump to the channel
inside its portal's Live TV, with its genre selected and the channel playing.
Radio stays hidden: it is a separate legacy-paged section with no
open-on-arrival contract.

The handoff refuses to guess. The store records which genre the rendered ITV
list belongs to, so the deferred play waits for a list that can actually serve
the channel instead of inferring it from array identity. An id claimed by one
channel as its `id` and another as its `stream_id` is ambiguous, so neither is
played and the user still lands in the right genre. The handoff is abandoned
when the user changes genre, search, portal or section first.

Two pre-existing defects surfaced during review and are fixed here:

- A blank provider id shadowed a valid one. `a ?? b` keeps an empty string, so
  a channel with a blank `stream_id` was stored with no identity at all and
  could not be selected, played or found again. Six writers had that shape and
  three private copies of the skip-the-blank rule; all now go through one
  `firstNonBlankStalkerId` helper.
- Route-session readiness could publish before the store held the portal's
  row. The constructor starts one sync and the first navigation starts another,
  and the second skipped the bootstrap because the playlist id was claimed
  before the awaits that install it. Arrivals are now serialized, the id is
  claimed only after the store write resolves, and only the newest sync
  publishes readiness.

Both fixes carry regression tests that fail on the old behavior.
2026-09-20 22:55:29 +02:00
4gray 6578e4073c feat(dashboard): portal EPG on the live rails, loaded lazily per visible card (#1638)
Xtream and Stalker live cards on the dashboard carried no XMLTV key, so the
rails never asked anything for them and showed only the LIVE chip. Their
programme now comes from the portal, one card at a time and only once the card
is on screen.

- `lib-dashboard-rail` reports the cards inside its viewport through an
  IntersectionObserver rooted at the track; `DashboardLiveEpgPresenter` unions
  them with the pinned hero row and hands the set to
  `DashboardPortalLiveEpgPresenter`.
- `DashboardPortalLiveEpgService` runs the bounded queue — two requests in
  flight, 200 ms apart, one card each — through
  `StreamResolverService.loadEpgForItems`, publishing every answer the moment
  it lands, so the page never waits and a slow portal delays no other card. A
  card scrolled past before its turn is never requested.
- A programme is trusted for 60 s, an empty answer for 30 s (the resolver
  reports a dead portal and a guide-less channel identically), and a
  completion captures both the display offset and the EPG source revision,
  requeueing itself when either moved.
- The presenter hands its wanted set back on destroy. Desktop only: the shared
  collection resolver is gated on the local XMLTV bridge.
- A shimmer placeholder shows only before a card's first portal answer; M3U
  cards keep the batched XMLTV lookup.
2026-09-20 22:53:55 +02:00
4gray a73538994a fix(agents): validate media and hyphenated literal paths 2026-09-20 22:51:15 +02:00
4gray b54dc4c405 fix(agents): validate visible headings and spaced paths 2026-09-20 22:39:02 +02:00
4gray b0cf310074 fix(agents): retain rendered context for Markdown references 2026-09-20 22:26:34 +02:00
4grayandClaude Opus 5 4e575a810e feat(dashboard): say where you left off instead of which provider it came from (#1646)
Every dashboard title carried a "Xtream · Series" / "Stalker · Movie" subtitle. Provider kind and content kind are the app's own taxonomy, not a property of the title, and they are identical on every card in a rail — so the one line that could tell two cards apart said nothing.

The hero now shows the source name alone, through `playlistDisplayLabel` (a stored playlist name is routinely the pasted URL with credentials, or a MAC). Continue Watching cards show what actually varies: the "S1·E5" chip plus "12 min left". Favorites keep the title alone, Recently Added keeps the source name, and a meta row with nothing in it is no longer rendered.

Stalker shows filed under Movies had no badge, no progress and no resume. An embedded-VOD row announces its episodes through a `series[]` array and carries no `is_series` flag, so `extractStalkerItemType` reports `movie` on purpose — the item must keep routing to the VOD catalog — while its progress is a set of episode positions keyed by the parent id, which the dashboard was looking up as a single `vod` row and never finding.

Split the two questions: `PortalActivityItem.watch_kind` records the progress model when it differs from the routing type, and every reader that has to choose goes through `resolvePortalActivityWatchKind` instead of `type`. That makes the resume handoff reachable for Stalker, so wire it through `STALKER_SERIES_RESUME_TARGET`: consumed once after the series positions are read, hydrating a lazy Ministra season first with a bounded two-attempt retry, and playing nothing at all when the position read failed rather than restarting the episode from zero.

Also fixes the global-recent route template, which bound `[seriesResume]` only on its Xtream branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 22:19:47 +02:00