mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(settings): discard stale hidden-category lookups and key withheld Stalker rows by their real identity
- A hidden-category lookup that lands after a later playback (same provider id, another playlist) no longer overwrites the newer channel's category; resolutions are generation- and playlist-checked. - Withheld Stalker rows are keyed by id, stream_id, movie_id, series_id or the row's cmd/name, so id-less rows no longer collapse onto one key and stall paging past locked pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4bd4bf2fc2
commit
870a2ec7fd
7 files changed
+82
-4
No files matched your search
@@ -195,7 +195,8 @@ on either side.
|
||||
id (mapped from the SQLite row id on Electron — through the visible
|
||||
category list, else through the unfiltered rows, since search can play a
|
||||
channel of a HIDDEN category; until that lookup lands the id is unknown
|
||||
and a relock stops the channel) and drops `activePlayback` on a `version`
|
||||
and a relock stops the channel, and a lookup that lands after a later
|
||||
playback — same provider id, other playlist — is discarded) and drops `activePlayback` on a `version`
|
||||
change that locks it, because the player is gated on that
|
||||
signal, not on the store selection — which an ordinary category switch
|
||||
also clears while the channel keeps playing.
|
||||
@@ -230,6 +231,11 @@ on either side.
|
||||
passing as unlocked. Electron routes carry SQLite row ids, so
|
||||
the Xtream guard maps them through the unfiltered category read; Stalker
|
||||
routes already carry the genre id.
|
||||
- **Stalker withheld-row bookkeeping:** the paging logic that advances past
|
||||
a page made only of withheld rows keys those rows by
|
||||
`stalkerWithheldRowKey` (`id`, else `stream_id`/`movie_id`/`series_id`,
|
||||
else the row's `cmd`/`name`), never by a shared `''`, so a page of new
|
||||
locked rows is not mistaken for a stalled portal.
|
||||
- **Stalker search staleness:** portal requests are not aborted, so a
|
||||
search page issued before a relock can finish after it, filtered with the
|
||||
pre-relock withheld set; `isStalkerSearchRequestCurrent` keys the
|
||||
|
||||
+2
-1
@@ -37,6 +37,7 @@ import {
|
||||
filterItvChannelsByGenre,
|
||||
toStalkerContentItem,
|
||||
toStalkerItvChannel,
|
||||
stalkerWithheldRowKey,
|
||||
withoutWithheldStalkerItems,
|
||||
} from '../utils';
|
||||
|
||||
@@ -739,7 +740,7 @@ export function withStalkerContent() {
|
||||
if (kept.has(item)) {
|
||||
continue;
|
||||
}
|
||||
const id = String(item.id ?? '');
|
||||
const id = stalkerWithheldRowKey(item);
|
||||
if (!withheldSeenIds.has(id)) {
|
||||
withheldSeenIds.add(id);
|
||||
newWithheldCount += 1;
|
||||
|
||||
+13
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
isStalkerItemWithheld,
|
||||
stalkerWithheldRowKey,
|
||||
withoutWithheldStalkerItems,
|
||||
} from './stalker-parental-lock.utils';
|
||||
import { ALL_CATEGORIES_WITHHELD } from '@iptvnator/shared/interfaces';
|
||||
@@ -64,3 +65,15 @@ describe('isStalkerItemWithheld in fail-closed mode', () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stalkerWithheldRowKey', () => {
|
||||
it('keys rows by whichever id field they carry, never by a shared empty string', () => {
|
||||
expect(stalkerWithheldRowKey({ id: 5 })).toBe('id:5');
|
||||
expect(stalkerWithheldRowKey({ stream_id: '6' })).toBe('id:6');
|
||||
expect(stalkerWithheldRowKey({ movie_id: 7 })).toBe('id:7');
|
||||
expect(stalkerWithheldRowKey({ series_id: 8 })).toBe('id:8');
|
||||
expect(stalkerWithheldRowKey({ cmd: 'ffmpeg http://a' })).not.toBe(
|
||||
stalkerWithheldRowKey({ cmd: 'ffmpeg http://b' })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,27 @@ import type { StalkerContentType } from '../stalker-store.contracts';
|
||||
export interface StalkerLockableItem {
|
||||
tv_genre_id?: string | number;
|
||||
category_id?: string | number;
|
||||
id?: string | number;
|
||||
stream_id?: string | number;
|
||||
movie_id?: string | number;
|
||||
series_id?: string | number;
|
||||
cmd?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The key the paging bookkeeping records a withheld row under. Stalker rows
|
||||
* carry their id in one of several fields; keying every id-less row under
|
||||
* `''` would count only the first of them as new, and a page of new locked
|
||||
* rows would then look like a stalled portal and end paging short of later
|
||||
* visible matches.
|
||||
*/
|
||||
export function stalkerWithheldRowKey(item: StalkerLockableItem): string {
|
||||
const id = item.id ?? item.stream_id ?? item.movie_id ?? item.series_id;
|
||||
if (id !== undefined && id !== null && String(id) !== '') {
|
||||
return `id:${String(id)}`;
|
||||
}
|
||||
return `row:${String(item.cmd ?? item.name ?? '')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
executeStalkerRequest,
|
||||
StalkerPortalRepairService,
|
||||
StalkerSessionService,
|
||||
stalkerWithheldRowKey,
|
||||
withoutWithheldStalkerItems,
|
||||
} from '@iptvnator/portal/stalker/data-access';
|
||||
import {
|
||||
@@ -389,7 +390,7 @@ export class StalkerSearchComponent {
|
||||
if (items.length < rawItems.length) {
|
||||
const kept = new Set(items);
|
||||
for (const item of rawItems) {
|
||||
const id = String(item.id ?? '');
|
||||
const id = stalkerWithheldRowKey(item);
|
||||
if (
|
||||
!kept.has(item) &&
|
||||
!this.searchWithheldIds.has(id)
|
||||
|
||||
+10
-1
@@ -427,6 +427,8 @@ export class LiveStreamLayoutComponent
|
||||
* not resolved it yet (or cannot) — a relock then stops the channel.
|
||||
*/
|
||||
private activeLiveProviderCategoryId: number | null | 'unknown' = null;
|
||||
/** Bumped per resolution so a slow hidden-category lookup cannot land on a later playback. */
|
||||
private liveCategoryLookupGeneration = 0;
|
||||
readonly playbackSessionKey = computed(() => {
|
||||
const sourceId = this.xtreamStore.currentPlaylist()?.id;
|
||||
const contentId = this.activeLiveItemId();
|
||||
@@ -863,6 +865,9 @@ export class LiveStreamLayoutComponent
|
||||
private resolveLiveProviderCategoryId(
|
||||
item: XtreamLiveChannelItem
|
||||
): number | null | 'unknown' {
|
||||
// Every resolution retires the lookups before it, including one for
|
||||
// a same-id channel of another playlist that resolved synchronously.
|
||||
const generation = ++this.liveCategoryLookupGeneration;
|
||||
const categoryId = Number(item.category_id);
|
||||
if (!Number.isFinite(categoryId)) {
|
||||
return null;
|
||||
@@ -886,7 +891,11 @@ export class LiveStreamLayoutComponent
|
||||
void this.databaseService
|
||||
.getAllXtreamCategories(playlistId, 'live')
|
||||
.then((rows) => {
|
||||
if (this.activeLiveItemId() !== item.xtream_id) {
|
||||
if (
|
||||
generation !== this.liveCategoryLookupGeneration ||
|
||||
this.xtreamStore.currentPlaylist()?.id !== playlistId ||
|
||||
this.activeLiveItemId() !== item.xtream_id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const row = rows.find(
|
||||
|
||||
+27
@@ -255,6 +255,33 @@ describe('LiveStreamLayoutComponent parental lock', () => {
|
||||
expect(component.activePlayback()).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores a hidden-category lookup that lands after a later playback', async () => {
|
||||
runtime.supportsXtreamSqliteDataSource = true;
|
||||
let resolveLookup: (rows: unknown[]) => void = () => undefined;
|
||||
databaseService.getAllXtreamCategories.mockReturnValueOnce(
|
||||
new Promise<unknown[]>((resolve) => (resolveLookup = resolve))
|
||||
);
|
||||
// Playlist 1: hidden category 99, lookup in flight.
|
||||
component.playLive({ ...sampleChannel, category_id: 99 });
|
||||
|
||||
// Playlist 2: a channel with the same provider id whose category is
|
||||
// visible (row 41 → provider 7) resolves synchronously.
|
||||
xtreamStore.currentPlaylist.set({ ...playlist, id: 'playlist-2' });
|
||||
fixture.detectChanges();
|
||||
component.playLive({ ...sampleChannel, category_id: 41 });
|
||||
expect(component.activePlayback()).not.toBeNull();
|
||||
|
||||
// The stale lookup answers for playlist 1 with provider 9.
|
||||
resolveLookup([{ id: 99, xtream_id: 9 }]);
|
||||
await fixture.whenStable();
|
||||
|
||||
lock(9);
|
||||
expect(component.activePlayback()).not.toBeNull();
|
||||
lock(7);
|
||||
expect(component.activePlayback()).toBeNull();
|
||||
xtreamStore.currentPlaylist.set(playlist);
|
||||
});
|
||||
|
||||
it('stops a channel whose category is still unresolved when the lock activates', () => {
|
||||
runtime.supportsXtreamSqliteDataSource = true;
|
||||
databaseService.getAllXtreamCategories.mockReturnValue(
|
||||
|
||||
Reference in new issue
Block a user