mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(settings): close the startup, re-stamp, relock-refresh and switch-persistence gaps
- The window before the initial lock store read settles now withholds everything, like an unreadable store: settings can report the feature as on before the locks are known. - The store commits before the SQLite index re-stamp; a failed re-stamp now rolls the store back, a failed rollback re-stamps on the next access, and every launch re-derives the index from the store. - Xtream category/content reloads fail closed: a rejected reload empties the affected lists (content types drop back to idle) instead of keeping rows read under the previous lock state. - Enabling/disabling the feature persists through one guarded path that undoes the in-memory switch and skips the Electron mirror on a failed settings write. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
f5c3543c28
commit
4b14c0929c
7 files changed
+355
-31
No files matched your search
@@ -115,6 +115,13 @@ content and search, the M3U channel list) receive
|
||||
is entered or the store reads again (`requestUnlock` and every lock write
|
||||
retry the read first, and a write is refused while it still fails, since it
|
||||
would be built on an empty in-memory store and wipe the persisted locks).
|
||||
The window before the initial read settles is treated the same way
|
||||
(`ParentalLockLockStore.readable` is false until then): settings can report
|
||||
the feature as on before the locks are known. The feature switch itself is
|
||||
persisted through one guarded path (`persistEnabled`): `updateSettings`
|
||||
patches memory before it writes, so a failed write is undone in memory, the
|
||||
Electron mirror is left untouched and `setupPin`/`disable` report false —
|
||||
the toggle never shows a state the next launch will not have.
|
||||
|
||||
### In-memory catalogs
|
||||
|
||||
@@ -137,7 +144,11 @@ would be built on an empty in-memory store and wipe the persisted locks).
|
||||
in-portal search (`XtreamStore.refreshSearchResults`, the last
|
||||
`searchContent` call as issued) — `searchResults` is a separate array the
|
||||
search page renders directly and would otherwise keep locked titles until
|
||||
the query changes. Live playback is stopped by the layout itself:
|
||||
the query changes. Both reloads fail closed: a category reload that
|
||||
rejects empties the three category lists, and a per-type content reload
|
||||
that rejects empties that type and sets it back to `idle` so the next
|
||||
visit loads it again (filtered) — rows read under the previous lock state
|
||||
are never kept. Live playback is stopped by the layout itself:
|
||||
`LiveStreamLayoutComponent` keeps the playing channel's provider category
|
||||
id (mapped from the SQLite row id on Electron) and drops `activePlayback`
|
||||
on a `version` change that locks it, because the player is gated on that
|
||||
@@ -233,7 +244,13 @@ would be built on an empty in-memory store and wipe the persisted locks).
|
||||
reports a failed save in a snackbar (the dialog has closed by then). On
|
||||
Electron the `categories.locked` re-stamp (`setCategoryLocks`) clears and
|
||||
re-locks one playlist/type inside ONE transaction, so a failed restamp
|
||||
keeps the previous index instead of leaving every category unlocked.
|
||||
keeps the previous index instead of leaving every category unlocked. The
|
||||
store commits BEFORE that re-stamp, so a failed re-stamp rolls the store
|
||||
back to what the index reflects (a category must not be recorded and
|
||||
shown as locked while Electron reads, which filter by the index alone,
|
||||
still serve it); if the rollback write fails too, the playlist is
|
||||
re-stamped on the next store access, and every launch re-derives the
|
||||
index from the store for each playlist that has locks.
|
||||
- Header lock/unlock button and the `parental-lock-now` /
|
||||
`parental-unlock` palette commands.
|
||||
|
||||
|
||||
@@ -462,6 +462,46 @@ describe('withContent import state', () => {
|
||||
expect(store.isContentInitialized()).toBe(true);
|
||||
});
|
||||
|
||||
it('empties the category lists when their reload fails', async () => {
|
||||
dataSource.getCategories.mockResolvedValue([{ category_id: 'x' }]);
|
||||
await store.reloadCategories();
|
||||
expect(store.liveCategories()).toEqual([{ category_id: 'x' }]);
|
||||
|
||||
dataSource.getCategories.mockRejectedValue(new Error('db'));
|
||||
await store.reloadCategories();
|
||||
|
||||
expect(store.liveCategories()).toEqual([]);
|
||||
expect(store.vodCategories()).toEqual([]);
|
||||
expect(store.serialCategories()).toEqual([]);
|
||||
});
|
||||
|
||||
it('empties a type whose cached reload fails and lets the others reload', async () => {
|
||||
dataSource.getContent.mockResolvedValue([{ xtream_id: 1 }]);
|
||||
await store.initializeContent();
|
||||
expect(store.contentLoadStateByType()).toEqual({
|
||||
live: 'ready',
|
||||
vod: 'ready',
|
||||
series: 'ready',
|
||||
});
|
||||
|
||||
dataSource.getContent.mockImplementation(
|
||||
(_playlistId: string, _credentials: unknown, type: ContentType) =>
|
||||
type === 'movie'
|
||||
? Promise.reject(new Error('db'))
|
||||
: Promise.resolve([{ xtream_id: 2 }])
|
||||
);
|
||||
await store.reloadCachedContent();
|
||||
|
||||
expect(store.vodStreams()).toEqual([]);
|
||||
expect(store.liveStreams()).toEqual([{ xtream_id: 2 }]);
|
||||
expect(store.serialStreams()).toEqual([{ xtream_id: 2 }]);
|
||||
expect(store.contentLoadStateByType()).toEqual({
|
||||
live: 'ready',
|
||||
vod: 'idle',
|
||||
series: 'ready',
|
||||
});
|
||||
});
|
||||
|
||||
it('records the VOD category owner when categories are reloaded', async () => {
|
||||
dataSource.getCategories.mockImplementation(
|
||||
(
|
||||
|
||||
@@ -1491,7 +1491,16 @@ export function withContent() {
|
||||
serialCategories: series,
|
||||
});
|
||||
} catch (error) {
|
||||
// Fail closed: the lists on screen were read under
|
||||
// the previous lock state, so keeping them would
|
||||
// keep locked category names visible. They are
|
||||
// rebuilt by the next category load.
|
||||
logger.error('Error reloading categories', error);
|
||||
patchState(store, {
|
||||
liveCategories: [],
|
||||
vodCategories: [],
|
||||
serialCategories: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1508,16 +1517,28 @@ export function withContent() {
|
||||
return;
|
||||
}
|
||||
const loadStates = store.contentLoadStateByType();
|
||||
try {
|
||||
if (loadStates.live === 'ready') {
|
||||
// Each type on its own: one failing read must neither
|
||||
// skip the remaining types nor keep its own rows, which
|
||||
// were read under the previous lock state. A failed type
|
||||
// is emptied and set back to `idle`, so the next visit
|
||||
// loads it again (filtered) instead of showing a gap.
|
||||
const failed: ContentType[] = [];
|
||||
if (loadStates.live === 'ready') {
|
||||
try {
|
||||
const live = (await dataSource.getContent(
|
||||
ctx.playlistId,
|
||||
ctx.credentials,
|
||||
'live'
|
||||
)) as XtreamLiveStream[];
|
||||
patchState(store, { liveStreams: live });
|
||||
} catch (error) {
|
||||
logger.error('Error reloading live streams', error);
|
||||
patchState(store, { liveStreams: [] });
|
||||
failed.push('live');
|
||||
}
|
||||
if (loadStates.vod === 'ready') {
|
||||
}
|
||||
if (loadStates.vod === 'ready') {
|
||||
try {
|
||||
const vod = (await dataSource.getContent(
|
||||
ctx.playlistId,
|
||||
ctx.credentials,
|
||||
@@ -1527,17 +1548,32 @@ export function withContent() {
|
||||
vodStreams: vod,
|
||||
vodStreamsPlaylistId: ctx.playlistId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error reloading VOD streams', error);
|
||||
patchState(store, { vodStreams: [] });
|
||||
failed.push('vod');
|
||||
}
|
||||
if (loadStates.series === 'ready') {
|
||||
}
|
||||
if (loadStates.series === 'ready') {
|
||||
try {
|
||||
const series = (await dataSource.getContent(
|
||||
ctx.playlistId,
|
||||
ctx.credentials,
|
||||
'series'
|
||||
)) as XtreamSerieItem[];
|
||||
patchState(store, { serialStreams: series });
|
||||
} catch (error) {
|
||||
logger.error('Error reloading series', error);
|
||||
patchState(store, { serialStreams: [] });
|
||||
failed.push('series');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error reloading cached content', error);
|
||||
}
|
||||
if (failed.length > 0) {
|
||||
const next = { ...store.contentLoadStateByType() };
|
||||
for (const type of failed) {
|
||||
next[type] = 'idle';
|
||||
}
|
||||
patchState(store, { contentLoadStateByType: next });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ParentalLockStore } from '@iptvnator/shared/interfaces';
|
||||
import { DatabaseService } from '../database-electron.service';
|
||||
import { RuntimeCapabilitiesService } from '../runtime-capabilities.service';
|
||||
import { ParentalLockLockStore } from './parental-lock-lock-store.service';
|
||||
import { ParentalLockStorageService } from './parental-lock-storage';
|
||||
|
||||
describe('ParentalLockLockStore', () => {
|
||||
const stored: ParentalLockStore = {
|
||||
'pl-1': {
|
||||
xtream: [{ categoryType: 'live', xtreamId: 7 }],
|
||||
stalker: [],
|
||||
m3u: [],
|
||||
},
|
||||
};
|
||||
let storage: { readLocks: jest.Mock; writeLocks: jest.Mock };
|
||||
let setCategoryLocks: jest.Mock;
|
||||
let store: ParentalLockLockStore;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = {
|
||||
readLocks: jest.fn(async () => JSON.parse(JSON.stringify(stored))),
|
||||
writeLocks: jest.fn(async () => true),
|
||||
};
|
||||
setCategoryLocks = jest.fn(async () => true);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ParentalLockLockStore,
|
||||
{ provide: ParentalLockStorageService, useValue: storage },
|
||||
{
|
||||
provide: RuntimeCapabilitiesService,
|
||||
useValue: { supportsXtreamSqliteDataSource: true },
|
||||
},
|
||||
{ provide: DatabaseService, useValue: { setCategoryLocks } },
|
||||
],
|
||||
});
|
||||
store = TestBed.inject(ParentalLockLockStore);
|
||||
});
|
||||
|
||||
it('is not readable until the initial read has settled', async () => {
|
||||
let resolveRead: (locks: ParentalLockStore) => void = () => undefined;
|
||||
storage.readLocks.mockReturnValue(
|
||||
new Promise<ParentalLockStore>((resolve) => (resolveRead = resolve))
|
||||
);
|
||||
const load = store.load();
|
||||
|
||||
expect(store.readable()).toBe(false);
|
||||
|
||||
resolveRead({});
|
||||
await load;
|
||||
expect(store.readable()).toBe(true);
|
||||
});
|
||||
|
||||
it('re-derives the SQLite index from the store on load', async () => {
|
||||
await store.load();
|
||||
// ensureReadable awaits the reconcile that load() started.
|
||||
await store.ensureReadable();
|
||||
|
||||
expect(setCategoryLocks).toHaveBeenCalledWith('pl-1', 'live', [7]);
|
||||
expect(setCategoryLocks).toHaveBeenCalledWith('pl-1', 'movies', []);
|
||||
expect(setCategoryLocks).toHaveBeenCalledWith('pl-1', 'series', []);
|
||||
});
|
||||
|
||||
it('rolls the store back when the index re-stamp fails', async () => {
|
||||
await store.load();
|
||||
setCategoryLocks.mockResolvedValue(false);
|
||||
|
||||
await expect(
|
||||
store.setXtreamLocks('pl-1', 'live', [7, 9])
|
||||
).resolves.toBe(false);
|
||||
|
||||
expect(store.lockedXtreamIds('pl-1', 'live')).toEqual([7]);
|
||||
expect(storage.writeLocks).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
'pl-1': expect.objectContaining({
|
||||
xtream: [{ categoryType: 'live', xtreamId: 7 }],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('re-stamps on the next access when even the rollback write failed', async () => {
|
||||
await store.load();
|
||||
await store.ensureReadable();
|
||||
setCategoryLocks.mockClear();
|
||||
setCategoryLocks.mockResolvedValueOnce(false);
|
||||
storage.writeLocks
|
||||
.mockResolvedValueOnce(true) // the new locks
|
||||
.mockResolvedValueOnce(false); // the rollback
|
||||
|
||||
await expect(
|
||||
store.setXtreamLocks('pl-1', 'live', [7, 9])
|
||||
).resolves.toBe(false);
|
||||
expect(store.lockedXtreamIds('pl-1', 'live')).toEqual([7, 9]);
|
||||
|
||||
await store.ensureReadable();
|
||||
|
||||
expect(setCategoryLocks).toHaveBeenCalledWith('pl-1', 'live', [7, 9]);
|
||||
});
|
||||
});
|
||||
@@ -42,24 +42,51 @@ export class ParentalLockLockStore {
|
||||
|
||||
private readonly locks = signal<ParentalLockStore>({});
|
||||
private readonly revisionState = signal(0);
|
||||
private readonly loadedState = signal(false);
|
||||
private loading: Promise<void> | null = null;
|
||||
/**
|
||||
* Playlists whose SQLite index could not be brought in line with the
|
||||
* store (a failed re-stamp whose rollback failed too); re-stamped on
|
||||
* the next store access.
|
||||
*/
|
||||
private readonly staleIndexPlaylists = new Set<string>();
|
||||
|
||||
/** The persisted store could not be read; see `ensureReadable()`. */
|
||||
readonly unreadable = signal(false);
|
||||
/**
|
||||
* The store has been read and is trustworthy. False while the initial
|
||||
* read is still in flight — the settings can report the feature as on
|
||||
* before the locks are known, and an empty in-memory store must not
|
||||
* read as "nothing is locked" in that window.
|
||||
*/
|
||||
readonly readable = computed(
|
||||
() => this.loadedState() && !this.unreadable()
|
||||
);
|
||||
/** Bumps whenever the lock set changes; consumers re-query. */
|
||||
readonly revision = this.revisionState.asReadonly();
|
||||
readonly isEmpty = computed(() => Object.keys(this.locks()).length === 0);
|
||||
|
||||
/** Reads the persisted store once. */
|
||||
/**
|
||||
* Reads the persisted store once. On Electron the `categories.locked`
|
||||
* index is then re-derived from it for every playlist that has locks:
|
||||
* the store is authoritative, and a re-stamp that failed in an earlier
|
||||
* session (or a database restored beside a newer store) must not leave
|
||||
* the index behind indefinitely.
|
||||
*/
|
||||
load(): Promise<void> {
|
||||
if (!this.loading) {
|
||||
this.loading = this.storage.readLocks().then((locks) => {
|
||||
if (locks === null) {
|
||||
console.error('The parental lock store could not be read.');
|
||||
this.unreadable.set(true);
|
||||
return;
|
||||
} else {
|
||||
this.locks.set(locks);
|
||||
for (const playlistId of Object.keys(locks)) {
|
||||
this.staleIndexPlaylists.add(playlistId);
|
||||
}
|
||||
}
|
||||
this.locks.set(locks);
|
||||
this.loadedState.set(true);
|
||||
void this.reconcileXtreamIndex();
|
||||
});
|
||||
}
|
||||
return this.loading;
|
||||
@@ -72,6 +99,7 @@ export class ParentalLockLockStore {
|
||||
async ensureReadable(): Promise<boolean> {
|
||||
await this.load();
|
||||
if (!this.unreadable()) {
|
||||
await this.reconcileXtreamIndex();
|
||||
return true;
|
||||
}
|
||||
const locks = await this.storage.readLocks();
|
||||
@@ -84,6 +112,26 @@ export class ParentalLockLockStore {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Re-stamps every playlist whose index may lag behind the store. */
|
||||
private async reconcileXtreamIndex(): Promise<void> {
|
||||
if (!this.runtime.supportsXtreamSqliteDataSource) {
|
||||
this.staleIndexPlaylists.clear();
|
||||
return;
|
||||
}
|
||||
for (const playlistId of [...this.staleIndexPlaylists]) {
|
||||
try {
|
||||
if (await this.stampXtreamLocks(playlistId)) {
|
||||
this.staleIndexPlaylists.delete(playlistId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to reconcile the parental lock index.',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locksFor(playlistId: string): ParentalLockPlaylistLocks {
|
||||
return (
|
||||
this.locks()[playlistId] ?? createEmptyParentalLockPlaylistLocks()
|
||||
@@ -113,15 +161,16 @@ export class ParentalLockLockStore {
|
||||
categoryType: ParentalLockXtreamCategoryType,
|
||||
xtreamIds: number[]
|
||||
): Promise<boolean> {
|
||||
const next = withXtreamLocks(
|
||||
this.locksFor(playlistId),
|
||||
categoryType,
|
||||
xtreamIds
|
||||
);
|
||||
const previous = this.locksFor(playlistId);
|
||||
const next = withXtreamLocks(previous, categoryType, xtreamIds);
|
||||
if (!(await this.persistPlaylistLocks(playlistId, next))) {
|
||||
return false;
|
||||
}
|
||||
return this.stampXtreamLocks(playlistId, [categoryType]);
|
||||
if (await this.stampXtreamLocks(playlistId, [categoryType])) {
|
||||
return true;
|
||||
}
|
||||
await this.rollBack(playlistId, previous);
|
||||
return false;
|
||||
}
|
||||
|
||||
async setStalkerLocks(
|
||||
@@ -154,10 +203,32 @@ export class ParentalLockLockStore {
|
||||
playlistId: string,
|
||||
locks: ParentalLockPlaylistLocks
|
||||
): Promise<boolean> {
|
||||
const previous = this.locksFor(playlistId);
|
||||
if (!(await this.persistPlaylistLocks(playlistId, locks))) {
|
||||
return false;
|
||||
}
|
||||
return this.stampXtreamLocks(playlistId, XTREAM_CATEGORY_TYPES);
|
||||
if (await this.stampXtreamLocks(playlistId, XTREAM_CATEGORY_TYPES)) {
|
||||
return true;
|
||||
}
|
||||
await this.rollBack(playlistId, previous);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The store commits before the SQLite index is re-stamped; a failed
|
||||
* re-stamp would otherwise leave a category recorded (and shown) as
|
||||
* locked while Electron reads, which filter by the index alone, still
|
||||
* serve it. The store goes back to what the index reflects; if even
|
||||
* that write fails, the playlist is re-stamped on the next access.
|
||||
*/
|
||||
private async rollBack(
|
||||
playlistId: string,
|
||||
previous: ParentalLockPlaylistLocks
|
||||
): Promise<void> {
|
||||
if (!(await this.persistPlaylistLocks(playlistId, previous))) {
|
||||
console.error('Failed to roll back the parental lock store.');
|
||||
this.staleIndexPlaylists.add(playlistId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -183,6 +183,46 @@ describe('ParentalLockService', () => {
|
||||
expect(service.isM3uGroupLocked('p', 'XXX')).toBe(true);
|
||||
});
|
||||
|
||||
it('withholds everything while the initial lock store read is in flight', async () => {
|
||||
storage.pinHash = await hashParentalLockPin('1234');
|
||||
parentalLockEnabled.set(true);
|
||||
let resolveLocks: (locks: Record<string, unknown>) => void = () =>
|
||||
undefined;
|
||||
storage.readLocks.mockReturnValue(
|
||||
new Promise((resolve) => (resolveLocks = resolve))
|
||||
);
|
||||
|
||||
const service = TestBed.inject(ParentalLockService);
|
||||
const initialization = service.initialize();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(service.active()).toBe(true);
|
||||
expect(service.withholdsEverything()).toBe(true);
|
||||
expect(service.isM3uGroupLocked('p', 'News')).toBe(true);
|
||||
|
||||
resolveLocks({});
|
||||
await initialization;
|
||||
expect(service.withholdsEverything()).toBe(false);
|
||||
expect(service.isM3uGroupLocked('p', 'News')).toBe(false);
|
||||
});
|
||||
|
||||
it('rolls the switch back when enabling cannot be persisted', async () => {
|
||||
prompt.requestPin.mockResolvedValue('1234');
|
||||
updateSettings.mockImplementationOnce(async () => {
|
||||
// updateSettings patches memory before the write fails.
|
||||
parentalLockEnabled.set(true);
|
||||
throw new Error('QuotaExceededError');
|
||||
});
|
||||
const service = await createService();
|
||||
|
||||
await expect(service.setupPin()).resolves.toBe(false);
|
||||
|
||||
expect(service.hasPin()).toBe(true);
|
||||
expect(service.enabled()).toBe(false);
|
||||
expect(service.unlocked()).toBe(false);
|
||||
expect(updateBridgeSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('starts locked with the feature on and unlocks through the prompt', async () => {
|
||||
storage.pinHash = await hashParentalLockPin('1234');
|
||||
parentalLockEnabled.set(true);
|
||||
|
||||
@@ -97,7 +97,7 @@ export class ParentalLockService {
|
||||
* protected content precisely during a storage failure.
|
||||
*/
|
||||
readonly withholdsEverything = computed(
|
||||
() => this.active() && this.locks.unreadable()
|
||||
() => this.active() && !this.locks.readable()
|
||||
);
|
||||
/** Bumps whenever `active` or the lock store changes; consumers re-query. */
|
||||
readonly version = computed(
|
||||
@@ -241,15 +241,36 @@ export class ParentalLockService {
|
||||
return false;
|
||||
}
|
||||
this.unlockedState.set(true);
|
||||
if (!this.enabled()) {
|
||||
await this.settingsStore.updateSettings({
|
||||
parentalLockEnabled: true,
|
||||
});
|
||||
mirrorParentalLockEnabledSetting(true);
|
||||
if (!this.enabled() && !(await this.persistEnabled(true))) {
|
||||
this.unlockedState.set(false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the feature switch. `updateSettings` patches the in-memory
|
||||
* value before the write; on a failed write that patch is undone (the
|
||||
* second write fails the same way and is ignored), so the toggle
|
||||
* cannot show a state the next launch will not have, and the Electron
|
||||
* mirror is only updated for a persisted switch.
|
||||
*/
|
||||
private async persistEnabled(enabled: boolean): Promise<boolean> {
|
||||
try {
|
||||
await this.settingsStore.updateSettings({
|
||||
parentalLockEnabled: enabled,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to persist the parental lock switch.', error);
|
||||
await this.settingsStore
|
||||
.updateSettings({ parentalLockEnabled: !enabled })
|
||||
.catch(() => undefined);
|
||||
return false;
|
||||
}
|
||||
mirrorParentalLockEnabledSetting(enabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Verifies the current PIN, then replaces it. */
|
||||
async changePin(): Promise<boolean> {
|
||||
await this.initialize();
|
||||
@@ -274,10 +295,9 @@ export class ParentalLockService {
|
||||
if (!(await this.verifyCurrentPin())) {
|
||||
return false;
|
||||
}
|
||||
await this.settingsStore.updateSettings({
|
||||
parentalLockEnabled: false,
|
||||
});
|
||||
mirrorParentalLockEnabledSetting(false);
|
||||
if (!(await this.persistEnabled(false))) {
|
||||
return false;
|
||||
}
|
||||
this.unlockedState.set(false);
|
||||
return true;
|
||||
}
|
||||
@@ -342,7 +362,7 @@ export class ParentalLockService {
|
||||
): boolean {
|
||||
return (
|
||||
this.active() &&
|
||||
(this.locks.unreadable() ||
|
||||
(!this.locks.readable() ||
|
||||
this.lockedXtreamIds(playlistId, categoryType).includes(
|
||||
xtreamId
|
||||
))
|
||||
@@ -359,7 +379,7 @@ export class ParentalLockService {
|
||||
}
|
||||
return (
|
||||
this.active() &&
|
||||
(this.locks.unreadable() ||
|
||||
(!this.locks.readable() ||
|
||||
this.lockedStalkerIds(playlistId, categoryType).includes(
|
||||
String(categoryId)
|
||||
))
|
||||
@@ -369,7 +389,7 @@ export class ParentalLockService {
|
||||
isM3uGroupLocked(playlistId: string, groupTitle: string): boolean {
|
||||
return (
|
||||
this.active() &&
|
||||
(this.locks.unreadable() ||
|
||||
(!this.locks.readable() ||
|
||||
this.lockedGroupTitles(playlistId).includes(groupTitle))
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user