mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
fix(settings): count radio playback as lock activity and read the lock store before a restore's stale-id check
- The idle relock no longer interrupts a playing radio station: playing <audio> counts as activity, like video. - A restore retries a failed lock-store read before checking a reused id for stale locks, and aborts while the store stays unreadable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2161c6a859
commit
34b3d37df0
7 files changed
+76
-4
No files matched your search
@@ -369,3 +369,45 @@ describe('ParentalLockEnforcementService apply serialization', () => {
|
||||
expect(xtreamStore.setSelectedCategory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ParentalLockEnforcementService busy probe', () => {
|
||||
it('counts playing audio (the radio player) as activity', () => {
|
||||
let probe: (() => boolean) | undefined;
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: ParentalLockService,
|
||||
useValue: {
|
||||
version: signal(0),
|
||||
registerBusyProbe: (fn: () => boolean) => (probe = fn),
|
||||
},
|
||||
},
|
||||
{ provide: XtreamStore, useValue: {} },
|
||||
{ provide: StalkerStore, useValue: {} },
|
||||
{ provide: Router, useValue: { url: '/' } },
|
||||
{
|
||||
provide: Store,
|
||||
useValue: { selectSignal: () => signal(null) },
|
||||
},
|
||||
{
|
||||
provide: PlaybackKeepAwakeService,
|
||||
useValue: { hasPlayingVideo: () => false },
|
||||
},
|
||||
],
|
||||
});
|
||||
const service = TestBed.inject(ParentalLockEnforcementService);
|
||||
TestBed.runInInjectionContext(() => service.start());
|
||||
expect(probe?.()).toBe(false);
|
||||
|
||||
const audio = document.createElement('audio');
|
||||
Object.defineProperty(audio, 'paused', { value: false });
|
||||
Object.defineProperty(audio, 'ended', { value: false });
|
||||
document.body.appendChild(audio);
|
||||
try {
|
||||
expect(probe?.()).toBe(true);
|
||||
} finally {
|
||||
audio.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -41,8 +41,11 @@ export class ParentalLockEnforcementService {
|
||||
return;
|
||||
}
|
||||
this.started = true;
|
||||
this.parentalLock.registerBusyProbe(() =>
|
||||
this.keepAwake.hasPlayingVideo()
|
||||
// Playback counts as activity: video through the keep-awake
|
||||
// tracker, and audio (the radio player) read directly, since the
|
||||
// keep-awake service deliberately ignores <audio>.
|
||||
this.parentalLock.registerBusyProbe(
|
||||
() => this.keepAwake.hasPlayingVideo() || hasPlayingAudio()
|
||||
);
|
||||
effect(() => {
|
||||
const version = this.parentalLock.version();
|
||||
@@ -294,3 +297,9 @@ export class ParentalLockEnforcementService {
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
function hasPlayingAudio(): boolean {
|
||||
return Array.from(document.querySelectorAll('audio')).some(
|
||||
(audio) => !audio.paused && !audio.ended
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,10 @@ follow in a second PR.
|
||||
(default 15; `0` = only on restart). The idle timer follows the UNLOCKED
|
||||
transition: armed the moment a session is unlocked — including the session
|
||||
that just enabled the feature — and disarmed on lock. Active playback of a built-in web
|
||||
player counts as interaction, so a film never locks half way.
|
||||
player counts as interaction, so a film never locks half way — video
|
||||
through the keep-awake tracker, and audio (the radio player) read from
|
||||
the playing `<audio>` elements, since the keep-awake service ignores
|
||||
them.
|
||||
- This is a child lock, not a security boundary: the PIN hash and the lock
|
||||
store sit in user-readable app data. The UI says so. There is no PIN
|
||||
recovery; resetting the app data is the way out.
|
||||
@@ -357,7 +360,9 @@ deletion has succeeded — the in-memory store empties at once, and a failed
|
||||
persisted clear is retried on the next store access and overwritten by the
|
||||
next write. A backup restore that CREATES a playlist (not a merge) starts
|
||||
that playlist from empty locks, so a reused id can never inherit entries a
|
||||
failed cleanup left behind.
|
||||
failed cleanup left behind; the restore retries a failed lock-store read
|
||||
first (`ensureLocksReadable`) and aborts while it still fails, so that check
|
||||
never runs against the empty fail-closed snapshot.
|
||||
|
||||
## Backup
|
||||
|
||||
|
||||
@@ -437,6 +437,12 @@ export class ParentalLockService {
|
||||
return this.locks.setM3uLocks(playlistId, groupTitles);
|
||||
}
|
||||
|
||||
/** Retries a failed lock-store read; false while it still fails. */
|
||||
async ensureLocksReadable(): Promise<boolean> {
|
||||
await this.initialize();
|
||||
return this.locks.ensureReadable();
|
||||
}
|
||||
|
||||
/** Playlist deletion: its locks leave the store. */
|
||||
async removePlaylistLocks(playlistId: string): Promise<boolean> {
|
||||
await this.initialize();
|
||||
|
||||
@@ -123,6 +123,7 @@ export function createPlaylistBackupService(
|
||||
parentalLock: {
|
||||
initialize: jest.fn().mockResolvedValue(undefined),
|
||||
locksReadable: jest.fn(() => true),
|
||||
ensureLocksReadable: jest.fn().mockResolvedValue(true),
|
||||
locksFor: jest.fn(() => ({ xtream: [], stalker: [], m3u: [] })),
|
||||
replacePlaylistLocks: jest.fn().mockResolvedValue(true),
|
||||
},
|
||||
|
||||
@@ -1117,6 +1117,14 @@ export class PlaylistBackupService {
|
||||
entry: PlaylistBackupEntry,
|
||||
isMerge: boolean
|
||||
): Promise<void> {
|
||||
// The stale-id check below and the merge base both read the store; an
|
||||
// empty fail-closed snapshot would let a reused id keep a deleted
|
||||
// playlist's locks once a later read recovers them.
|
||||
if (!(await this.parentalLock.ensureLocksReadable())) {
|
||||
throw new PlaylistBackupError(
|
||||
`Restoring the parental locks for "${playlistId}" failed: the lock store could not be read.`
|
||||
);
|
||||
}
|
||||
// A playlist created by this restore owns no locks yet: whatever the
|
||||
// store holds under a reused id belongs to a deleted playlist (a
|
||||
// cleanup that failed), and must not be inherited.
|
||||
|
||||
@@ -114,6 +114,7 @@ describe('PlaylistBackupService Xtream hidden categories (issue #1017)', () => {
|
||||
parentalLock: {
|
||||
initialize: jest.fn().mockResolvedValue(undefined),
|
||||
locksReadable: jest.fn(() => true),
|
||||
ensureLocksReadable: jest.fn().mockResolvedValue(true),
|
||||
// A failed cleanup left locks under the id the restore reuses.
|
||||
locksFor: jest.fn(() => ({
|
||||
xtream: [{ categoryType: 'live', xtreamId: 1 }],
|
||||
|
||||
Reference in new issue
Block a user