mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(playback): correlate global live-tab history by its session key
The unified Favorites/Recent live tab deferred its history write by stream URL only, so the same URL played from another playlist could confirm a failed selection, and a switch to catch-up before confirmation could never match. It now defers with the tab's playlist-scoped playbackSessionKey (the key its players confirm with), and the tab's radio player receives it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce92603981
commit
b0467791a4
6 files changed
+47
-3
No files matched your search
@@ -1150,7 +1150,8 @@ reaches history.
|
||||
same key — the same URL in two playlists must not let playback in one
|
||||
(inline, or in MPV/VLC) record a failed attempt in the other. Writes
|
||||
without one (portal resolvers, collection tabs) match any confirmation of
|
||||
their stream URL.
|
||||
their stream URL. The global live tab defers with its own
|
||||
playlist-scoped session key, which also survives a switch to catch-up.
|
||||
- `WebPlayerViewComponent` confirms its `playbackSessionKey`, `streamUrl`
|
||||
and `playback.streamUrl` once the owned engine's reported position has
|
||||
advanced by 2 seconds while playing (`PlaybackProgressConfirmation`).
|
||||
|
||||
+9
-2
@@ -11,6 +11,7 @@ import {
|
||||
import { PlaybackHistoryGate } from '@iptvnator/playback/data-access';
|
||||
import { ElectronStreamHeadersService } from '@iptvnator/ui/playback';
|
||||
import { UnifiedLiveTimeshift } from './unified-live-catchup';
|
||||
import { createUnifiedLivePlaybackSessionKey } from './unified-live-playback-session-key';
|
||||
import { UnifiedLiveSelectionGeneration } from './unified-live-selection-generation';
|
||||
|
||||
export interface UnifiedLiveSelection {
|
||||
@@ -247,9 +248,15 @@ export function createUnifiedLiveSelection(options: {
|
||||
}
|
||||
|
||||
// Selecting a channel is not watching it: the row moves to the
|
||||
// top of Recently Viewed once its stream has really played.
|
||||
// top of Recently Viewed once its stream has really played. The
|
||||
// tab's playlist-scoped session key — the one its players confirm
|
||||
// with — survives a switch to catch-up and does not match the
|
||||
// same URL listed in another playlist.
|
||||
historyGate.defer(
|
||||
{ streamUrls: [detail.playback.streamUrl] },
|
||||
{
|
||||
sessionKey: createUnifiedLivePlaybackSessionKey(item),
|
||||
streamUrls: [detail.playback.streamUrl],
|
||||
},
|
||||
() => void recordLivePlayback(item)
|
||||
);
|
||||
|
||||
|
||||
+1
@@ -58,6 +58,7 @@
|
||||
activeDetail()?.playback?.title ||
|
||||
''
|
||||
"
|
||||
[playbackSessionKey]="playbackSessionKey()"
|
||||
/>
|
||||
} @else {
|
||||
<app-web-player-view
|
||||
|
||||
+1
@@ -161,6 +161,7 @@ describe('UnifiedLiveTabComponent', () => {
|
||||
/** What the mounted player reports once the stream has really played. */
|
||||
async function playStream(streamUrl: string): Promise<void> {
|
||||
TestBed.inject(PlaybackHistoryGate).confirm({
|
||||
sessionKey: component.playbackSessionKey(),
|
||||
streamUrls: [streamUrl],
|
||||
});
|
||||
await fixture.whenStable();
|
||||
|
||||
+33
@@ -22,6 +22,7 @@ import {
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import { GlobalFavoritesListComponent } from '../global-favorites-list/global-favorites-list.component';
|
||||
import { UnifiedLiveTabComponent } from './unified-live-tab.component';
|
||||
import { createUnifiedLivePlaybackSessionKey } from './unified-live-playback-session-key';
|
||||
import {
|
||||
StubAudioPlayerComponent,
|
||||
StubEpgTimelineComponent,
|
||||
@@ -174,6 +175,37 @@ describe('UnifiedLiveTabComponent fullscreen channel panel', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('correlates a row with its session key, not its stream URL', async () => {
|
||||
const item = buildM3uLiveItem();
|
||||
streamResolver.resolveM3uPlaybackDetail.mockResolvedValue({
|
||||
epgMode: 'm3u',
|
||||
playback: { streamUrl: item.streamUrl, title: item.name },
|
||||
epgPrograms: [],
|
||||
});
|
||||
recentData.recordLivePlayback.mockResolvedValue(item);
|
||||
fixture.componentRef.setInput('items', [item]);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
await component.onChannelSelected(component.channelsForList()[0]);
|
||||
const gate = TestBed.inject(PlaybackHistoryGate);
|
||||
|
||||
// The same URL played from another playlist does not confirm it.
|
||||
gate.confirm({
|
||||
sessionKey: 'live:another-playlist:m3u-channel',
|
||||
streamUrls: [item.streamUrl],
|
||||
});
|
||||
await fixture.whenStable();
|
||||
expect(recentData.recordLivePlayback).not.toHaveBeenCalled();
|
||||
|
||||
// The row's own player does, even after switching to catch-up.
|
||||
gate.confirm({
|
||||
sessionKey: component.playbackSessionKey(),
|
||||
streamUrls: ['https://example.com/archive.m3u8'],
|
||||
});
|
||||
await fixture.whenStable();
|
||||
expect(recentData.recordLivePlayback).toHaveBeenCalledWith(item);
|
||||
});
|
||||
|
||||
it('moves a row that played to the top of Recently Viewed after another row was selected', async () => {
|
||||
const first = buildM3uLiveItem();
|
||||
const second: UnifiedCollectionItem = {
|
||||
@@ -209,6 +241,7 @@ describe('UnifiedLiveTabComponent fullscreen channel panel', () => {
|
||||
await component.onChannelSelected(row('M3U Live 2'));
|
||||
// The first row's playback is confirmed only now, after the switch.
|
||||
TestBed.inject(PlaybackHistoryGate).confirm({
|
||||
sessionKey: createUnifiedLivePlaybackSessionKey(first),
|
||||
streamUrls: [first.streamUrl],
|
||||
});
|
||||
await fixture.whenStable();
|
||||
|
||||
+1
@@ -101,6 +101,7 @@ export class StubAudioPlayerComponent {
|
||||
readonly icon = input('');
|
||||
readonly url = input.required<string>();
|
||||
readonly channelName = input('');
|
||||
readonly playbackSessionKey = input<string | null>(null);
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
Reference in new issue
Block a user