mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
fix(settings): fail closed on a rejected worker lock sync, tear down Stalker synchronously and capture the Xtream dialog context before the PIN
- A rejected lock-state sync to the SQLite worker makes the locked session withhold everything until a later sync succeeds. - The Stalker enforcement chunk is preloaded when a Stalker route opens; a relock runs it synchronously, or leaves the route at once while it is not loaded, instead of awaiting the chunk. - Xtream "Manage categories" captures playlist, provider and section before the PIN prompt and re-checks them after it and after the dialog import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4aa1b5a8e6
commit
067f920833
8 files changed
+267
-62
No files matched your search
@@ -151,6 +151,34 @@ describe('ParentalLockEnforcementService', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves the Stalker route synchronously on relock while the step is not loaded', () => {
|
||||
router.url = '/workspace/stalker/stalker-1/itv';
|
||||
(
|
||||
service as unknown as {
|
||||
loadStalkerEnforcement: () => Promise<unknown>;
|
||||
}
|
||||
).loadStalkerEnforcement = () => new Promise(() => undefined);
|
||||
|
||||
service.failClosedNow();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/workspace',
|
||||
'sources',
|
||||
]);
|
||||
});
|
||||
|
||||
it('runs a preloaded Stalker step synchronously on relock', async () => {
|
||||
router.url = '/workspace/stalker/stalker-1/vod/42';
|
||||
await service.applyStalker(); // loads the step
|
||||
lockedStalkerIds.add('9');
|
||||
stalkerStore.selectedItem.set({ category_id: '9' });
|
||||
stalkerStore.clearSelectedItem.mockClear();
|
||||
|
||||
service.failClosedNow();
|
||||
|
||||
expect(stalkerStore.clearSelectedItem).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not load the Stalker step outside a Stalker route', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/live';
|
||||
const load = jest.spyOn(
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Injectable,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ChannelActions, selectActive } from '@iptvnator/m3u-state';
|
||||
import {
|
||||
@@ -41,6 +41,8 @@ export class ParentalLockEnforcementService {
|
||||
private started = false;
|
||||
private lastVersion = -1;
|
||||
private applyChain: Promise<void> = Promise.resolve();
|
||||
private stalkerModule: StalkerEnforcementModule | null = null;
|
||||
private stalkerModuleLoad: Promise<StalkerEnforcementModule> | null = null;
|
||||
|
||||
start(): void {
|
||||
if (this.started) {
|
||||
@@ -53,6 +55,16 @@ export class ParentalLockEnforcementService {
|
||||
this.parentalLock.registerBusyProbe(
|
||||
() => this.keepAwake.hasPlayingVideo() || hasPlayingAudio()
|
||||
);
|
||||
// The Stalker step is loaded as soon as a Stalker route opens, so a
|
||||
// relock there can run it synchronously instead of awaiting a chunk.
|
||||
this.router.events?.subscribe((event) => {
|
||||
if (
|
||||
event instanceof NavigationEnd &&
|
||||
STALKER_ROUTE.test(event.urlAfterRedirects)
|
||||
) {
|
||||
void this.loadStalker().catch(() => undefined);
|
||||
}
|
||||
});
|
||||
effect(() => {
|
||||
const version = this.parentalLock.version();
|
||||
untracked(() => {
|
||||
@@ -103,7 +115,7 @@ export class ParentalLockEnforcementService {
|
||||
*/
|
||||
private failClosedNow(): void {
|
||||
this.applyM3u();
|
||||
void this.applyStalker();
|
||||
this.failClosedStalkerNow();
|
||||
const playlistId = this.xtreamStore.playlistId?.();
|
||||
if (!playlistId) {
|
||||
return;
|
||||
@@ -273,9 +285,42 @@ export class ParentalLockEnforcementService {
|
||||
}
|
||||
|
||||
/** The dynamic import; a field so specs can substitute it. */
|
||||
loadStalkerEnforcement = () =>
|
||||
loadStalkerEnforcement = (): Promise<StalkerEnforcementModule> =>
|
||||
import('./parental-lock-stalker-enforcement');
|
||||
|
||||
/** Loads the Stalker step once; a failed load may be retried. */
|
||||
private loadStalker(): Promise<StalkerEnforcementModule> {
|
||||
this.stalkerModuleLoad ??= this.loadStalkerEnforcement().then(
|
||||
(module) => (this.stalkerModule = module),
|
||||
(error: unknown) => {
|
||||
this.stalkerModuleLoad = null;
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
return this.stalkerModuleLoad;
|
||||
}
|
||||
|
||||
/**
|
||||
* The relock's Stalker step, synchronously: run it when the chunk is
|
||||
* there, otherwise leave the Stalker route at once (its route session
|
||||
* clears the selection and the live layout stops playback) rather than
|
||||
* wait for a chunk that may be slow or never arrive.
|
||||
*/
|
||||
private failClosedStalkerNow(): void {
|
||||
if (!STALKER_ROUTE.test(this.router.url)) {
|
||||
return;
|
||||
}
|
||||
if (!this.stalkerModule) {
|
||||
void this.router.navigate(['/workspace', 'sources']);
|
||||
return;
|
||||
}
|
||||
this.stalkerModule.applyParentalLockToStalker(
|
||||
this.injector,
|
||||
this.parentalLock,
|
||||
this.router
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Stalker data layer stays off the initial path: the step is loaded
|
||||
* only while a Stalker route is open. Outside one there is nothing to
|
||||
@@ -286,9 +331,9 @@ export class ParentalLockEnforcementService {
|
||||
if (!STALKER_ROUTE.test(this.router.url)) {
|
||||
return;
|
||||
}
|
||||
let module: Awaited<ReturnType<typeof this.loadStalkerEnforcement>>;
|
||||
let module: StalkerEnforcementModule;
|
||||
try {
|
||||
module = await this.loadStalkerEnforcement();
|
||||
module = await this.loadStalker();
|
||||
} catch (error) {
|
||||
// Fail closed (e.g. a stale PWA page whose chunk is gone): leave
|
||||
// the Stalker route. Its route session clears the selection and
|
||||
@@ -328,6 +373,9 @@ export class ParentalLockEnforcementService {
|
||||
}
|
||||
}
|
||||
|
||||
type StalkerEnforcementModule =
|
||||
typeof import('./parental-lock-stalker-enforcement');
|
||||
|
||||
function hasPlayingAudio(): boolean {
|
||||
return Array.from(document.querySelectorAll('audio')).some(
|
||||
(audio) => !audio.paused && !audio.ended
|
||||
|
||||
Reference in new issue
Block a user