mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
A movie that exists in several imported Xtream playlists now shows a "Sources N" chip on its detail page and in the player. Switching playlist mid-film keeps the timecode, a preferred source can be pinned per movie, and a failed stream offers the alternatives instead of a dead end. The governing rule is that a guess is never presented as a fact. Every metadata value carries where it came from — `api` (the provider said so), `parsed` (inferred from the title) or `probe` (we contacted the stream). Facts render as plain tags, guesses are prefixed `~` in a warning colour, and an unknown value renders no tag at all plus a "check" affordance. Ranking and failover read through `factualOnly()`, so a filename claiming 4K is structurally unable to outrank a source that was actually reached. A probe that could not complete reports "unknown", never "unavailable". Scope is deliberately narrow: Xtream to Xtream, movies only, Electron only. Stalker never reaches the `content` table and M3U is a JSON blob whose search forces live content; both are additive later, since the candidate type already carries all three portal kinds. In the PWA every entry point is gated off and the chip renders nothing. Auto-failover is opt-in and off by default. Each source is tried at most once per session, so it terminates structurally, and the switch is never silent — the toast names the new playlist, offers an undo, and warns that the dub may differ only when both sides state an audio track as fact. Notable details: - Playlist names are routinely the pasted URL, credentials included. They are never rendered raw; a short host-only label is derived instead. - Quality is derived from pixel width, not height: a 2.39:1 1080p master is 1920x800, and bucketing that by height would publish "720p" as a fact. - Switching is a single `inlinePlayback.set()` so the player and engine survive and re-seek; the carried position is read before the 15s persistence throttle so it does not rewind. - Sources from one playlist collapse into a group, since the same film often appears there several times under different stream ids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
5.3 KiB
HTML
136 lines
5.3 KiB
HTML
<div class="sources-menu">
|
|
<div class="sources-menu__header">
|
|
<h3 class="sources-menu__title">
|
|
@if (title()) {
|
|
{{
|
|
'PORTALS.MULTI_SOURCE.SOURCES_FOR'
|
|
| translate: { title: title() }
|
|
}}
|
|
} @else {
|
|
{{ 'PORTALS.MULTI_SOURCE.SOURCE' | translate }}
|
|
}
|
|
</h3>
|
|
<span class="sources-menu__hint">
|
|
@if (resumeLabel(); as resume) {
|
|
{{ resume }}
|
|
} @else {
|
|
{{ matchKindLabelKey() | translate }}
|
|
}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="sources-menu__toolbar">
|
|
@if (showFilter()) {
|
|
<div class="sources-menu__filter">
|
|
<mat-icon class="sources-menu__filter-icon">search</mat-icon>
|
|
<input
|
|
type="text"
|
|
class="sources-menu__filter-input"
|
|
[ngModel]="filter()"
|
|
(ngModelChange)="filter.set($event)"
|
|
[attr.aria-label]="
|
|
'PORTALS.MULTI_SOURCE.FILTER_PLACEHOLDER' | translate
|
|
"
|
|
[placeholder]="
|
|
'PORTALS.MULTI_SOURCE.FILTER_PLACEHOLDER' | translate
|
|
"
|
|
/>
|
|
</div>
|
|
}
|
|
@if (uncheckedCount() > 0) {
|
|
<button
|
|
type="button"
|
|
class="sources-menu__check-all"
|
|
(click)="checkAllVisible()"
|
|
>
|
|
<mat-icon>refresh</mat-icon>
|
|
<span>{{ 'PORTALS.MULTI_SOURCE.CHECK_ALL' | translate }}</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
<div class="sources-menu__list">
|
|
@for (group of groups(); track group.key) {
|
|
<div class="sources-menu__group">
|
|
<app-vod-source-row
|
|
[source]="group.primary"
|
|
[showPin]="showPin()"
|
|
(playRequested)="playRequested.emit($event)"
|
|
(pinRequested)="pinRequested.emit($event)"
|
|
(checkRequested)="checkRequested.emit($event)"
|
|
/>
|
|
|
|
@if (group.count > 1) {
|
|
<button
|
|
type="button"
|
|
class="sources-menu__variants-toggle"
|
|
[attr.aria-expanded]="isExpanded(group.key)"
|
|
(click)="toggleGroup(group.key)"
|
|
>
|
|
<mat-icon>{{
|
|
isExpanded(group.key)
|
|
? 'expand_less'
|
|
: 'expand_more'
|
|
}}</mat-icon>
|
|
<span>{{
|
|
'PORTALS.MULTI_SOURCE.COPIES'
|
|
| translate: { count: group.count }
|
|
}}</span>
|
|
</button>
|
|
|
|
@if (isExpanded(group.key)) {
|
|
<div class="sources-menu__variants">
|
|
@for (
|
|
variant of group.sources;
|
|
track variant.id;
|
|
let i = $index
|
|
) {
|
|
@if (variant.id !== group.primary.id) {
|
|
<div class="sources-menu__variant">
|
|
<span
|
|
class="sources-menu__variant-label"
|
|
[title]="variantLabelFor(group, i)"
|
|
>{{ variantLabelFor(group, i) }}</span
|
|
>
|
|
<app-vod-source-row
|
|
[source]="variant"
|
|
[showPin]="showPin()"
|
|
(playRequested)="
|
|
playRequested.emit($event)
|
|
"
|
|
(pinRequested)="
|
|
pinRequested.emit($event)
|
|
"
|
|
(checkRequested)="
|
|
checkRequested.emit($event)
|
|
"
|
|
/>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
@if (visibleSources().length === 0) {
|
|
<p class="sources-menu__empty">
|
|
{{ 'PORTALS.MULTI_SOURCE.NO_MATCHES' | translate }}
|
|
</p>
|
|
}
|
|
</div>
|
|
|
|
<div class="sources-menu__footer">
|
|
<span class="sources-menu__footer-label">{{
|
|
'PORTALS.MULTI_SOURCE.AUTO_FAILOVER' | translate
|
|
}}</span>
|
|
<mat-slide-toggle
|
|
class="sources-menu__toggle"
|
|
[checked]="autoFailoverEnabled()"
|
|
(change)="onAutoFailoverChange($event)"
|
|
(click)="$event.stopPropagation()"
|
|
[attr.aria-label]="'PORTALS.MULTI_SOURCE.AUTO_FAILOVER' | translate"
|
|
/>
|
|
</div>
|
|
</div>
|