mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(portals): make connection cooldown configurable in desktop settings (#1536)
This commit is contained in:
1 parent
de81e3b238
commit
0245d73d78
48 files changed
+787
-27
No files matched your search
@@ -28,6 +28,7 @@ const STORE_KEYS = {
|
||||
MPV_PLAYER_ARGUMENTS: 'MPV_PLAYER_ARGUMENTS',
|
||||
MPV_REUSE_INSTANCE: 'MPV_REUSE_INSTANCE',
|
||||
STARTUP_WINDOW_MODE: 'STARTUP_WINDOW_MODE',
|
||||
PORTAL_CONNECTIVITY_GUARD: 'PORTAL_CONNECTIVITY_GUARD',
|
||||
VLC_PLAYER_ARGUMENTS: 'VLC_PLAYER_ARGUMENTS',
|
||||
VLC_REUSE_INSTANCE: 'VLC_REUSE_INSTANCE',
|
||||
} as const;
|
||||
@@ -55,6 +56,7 @@ jest.mock('../services/store.service', () => ({
|
||||
MPV_PLAYER_ARGUMENTS: STORE_KEYS.MPV_PLAYER_ARGUMENTS,
|
||||
MPV_REUSE_INSTANCE: STORE_KEYS.MPV_REUSE_INSTANCE,
|
||||
STARTUP_WINDOW_MODE: STORE_KEYS.STARTUP_WINDOW_MODE,
|
||||
PORTAL_CONNECTIVITY_GUARD: STORE_KEYS.PORTAL_CONNECTIVITY_GUARD,
|
||||
VLC_PLAYER_ARGUMENTS: STORE_KEYS.VLC_PLAYER_ARGUMENTS,
|
||||
VLC_REUSE_INSTANCE: STORE_KEYS.VLC_REUSE_INSTANCE,
|
||||
store: {
|
||||
@@ -101,6 +103,37 @@ describe('SETTINGS_UPDATE', () => {
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('restores the persisted preference before the renderer starts', async () => {
|
||||
mockStoreGet.mockReturnValue(false);
|
||||
const settingsEvents = (await import('./settings.events')).default;
|
||||
settingsEvents.bootstrapSettingsEvents();
|
||||
const guard = await import('../util/host-connectivity-guard');
|
||||
expect(
|
||||
guard.beginGuardedHostRequest('https://portal.example')
|
||||
).toBeNull();
|
||||
settingsUpdateHandler({}, { portalConnectivityGuard: true });
|
||||
expect(
|
||||
guard.beginGuardedHostRequest('https://portal.example')
|
||||
).not.toBeNull();
|
||||
expect(mockStoreSet).toHaveBeenCalledWith(
|
||||
STORE_KEYS.PORTAL_CONNECTIVITY_GUARD,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('applies an opt-out immediately and preserves it through unrelated saves', async () => {
|
||||
const guard = await import('../util/host-connectivity-guard');
|
||||
settingsUpdateHandler({}, { portalConnectivityGuard: false });
|
||||
settingsUpdateHandler({}, { showCaptions: true });
|
||||
expect(
|
||||
guard.beginGuardedHostRequest('https://portal.example')
|
||||
).toBeNull();
|
||||
expect(mockStoreSet).toHaveBeenCalledWith(
|
||||
STORE_KEYS.PORTAL_CONNECTIVITY_GUARD,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes external-player arguments and preserves explicit false reuse settings', () => {
|
||||
settingsUpdateHandler(
|
||||
{},
|
||||
|
||||
@@ -12,14 +12,19 @@ import {
|
||||
MPV_PLAYER_ARGUMENTS,
|
||||
MPV_REUSE_INSTANCE,
|
||||
STARTUP_WINDOW_MODE,
|
||||
PORTAL_CONNECTIVITY_GUARD,
|
||||
store,
|
||||
VLC_PLAYER_ARGUMENTS,
|
||||
VLC_REUSE_INSTANCE,
|
||||
} from '../services/store.service';
|
||||
import { httpServer } from '../server/http-server';
|
||||
import { setHostConnectivityGuardEnabled } from '../util/host-connectivity-guard';
|
||||
|
||||
export default class SettingsEvents {
|
||||
static bootstrapSettingsEvents(): Electron.IpcMain {
|
||||
setHostConnectivityGuardEnabled(
|
||||
store.get(PORTAL_CONNECTIVITY_GUARD, true) !== false
|
||||
);
|
||||
return ipcMain;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +35,12 @@ ipcMain.handle('SETTINGS_UPDATE', (_event, arg) => {
|
||||
redactSensitiveData(arg)
|
||||
);
|
||||
|
||||
if (arg.portalConnectivityGuard !== undefined) {
|
||||
const enabled = arg.portalConnectivityGuard !== false;
|
||||
store.set(PORTAL_CONNECTIVITY_GUARD, enabled);
|
||||
setHostConnectivityGuardEnabled(enabled);
|
||||
}
|
||||
|
||||
if (arg.mpvPlayerArguments !== undefined) {
|
||||
store.set(
|
||||
MPV_PLAYER_ARGUMENTS,
|
||||
|
||||
@@ -25,6 +25,9 @@ export const EMBEDDED_MPV_FRAME_COPY = 'EMBEDDED_MPV_FRAME_COPY';
|
||||
*/
|
||||
export const STARTUP_WINDOW_MODE = 'STARTUP_WINDOW_MODE';
|
||||
|
||||
/** Desktop portal request cooldown; absent means enabled. */
|
||||
export const PORTAL_CONNECTIVITY_GUARD = 'PORTAL_CONNECTIVITY_GUARD';
|
||||
|
||||
/**
|
||||
* Extra libmpv options for embedded sessions, one "key=value" per line, as
|
||||
* typed in Settings > Playback. Mirrored here by the SETTINGS_UPDATE handler
|
||||
@@ -52,6 +55,7 @@ export type StoreType = {
|
||||
[EMBEDDED_MPV_EXTRA_OPTIONS]: string;
|
||||
[EMBEDDED_MPV_AUTO_RECONNECT]: boolean;
|
||||
[STARTUP_WINDOW_MODE]: StartupWindowMode;
|
||||
[PORTAL_CONNECTIVITY_GUARD]: boolean;
|
||||
};
|
||||
|
||||
// Export singleton store instance
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
beginGuardedHostRequest,
|
||||
reportGuardedHostFailure,
|
||||
resetHostConnectivityGuardForTests,
|
||||
setHostConnectivityGuardEnabled,
|
||||
reportGuardedHostSuccess,
|
||||
} from './host-connectivity-guard';
|
||||
|
||||
describe('reportGuardedHostFailure', () => {
|
||||
@@ -49,6 +51,62 @@ describe('reportGuardedHostFailure', () => {
|
||||
resetHostConnectivityGuardForTests();
|
||||
});
|
||||
|
||||
it('bypasses an open guard when disabled and starts fresh when re-enabled', () => {
|
||||
attempt(ownFailure());
|
||||
attempt(ownFailure());
|
||||
expect(() => beginGuardedHostRequest(URL_ON_ENDPOINT)).toThrow(
|
||||
HostConnectivityGuardError
|
||||
);
|
||||
setHostConnectivityGuardEnabled(false);
|
||||
expect(beginGuardedHostRequest(URL_ON_ENDPOINT)).toBeNull();
|
||||
setHostConnectivityGuardEnabled(true);
|
||||
expect(() => beginGuardedHostRequest(URL_ON_ENDPOINT)).not.toThrow();
|
||||
attempt(ownFailure());
|
||||
expect(() => beginGuardedHostRequest(URL_ON_ENDPOINT)).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not reset the guard when saving an unchanged preference', () => {
|
||||
attempt(ownFailure());
|
||||
attempt(ownFailure());
|
||||
setHostConnectivityGuardEnabled(true);
|
||||
expect(() => beginGuardedHostRequest(URL_ON_ENDPOINT)).toThrow(
|
||||
HostConnectivityGuardError
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores old failures and successes after a preference transition', () => {
|
||||
const oldFailure = beginGuardedHostRequest(URL_ON_ENDPOINT);
|
||||
const oldSuccess = beginGuardedHostRequest(URL_ON_ENDPOINT);
|
||||
setHostConnectivityGuardEnabled(false);
|
||||
setHostConnectivityGuardEnabled(true);
|
||||
reportGuardedHostFailure(oldFailure, ownFailure());
|
||||
reportGuardedHostFailure(oldFailure, ownFailure());
|
||||
expect(() => beginGuardedHostRequest(URL_ON_ENDPOINT)).not.toThrow();
|
||||
attempt(ownFailure());
|
||||
attempt(ownFailure());
|
||||
reportGuardedHostSuccess(oldSuccess);
|
||||
expect(() => beginGuardedHostRequest(URL_ON_ENDPOINT)).toThrow(
|
||||
HostConnectivityGuardError
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the environment disable switch authoritative', () => {
|
||||
const original = process.env.IPTVNATOR_DISABLE_CONNECTIVITY_GUARD;
|
||||
process.env.IPTVNATOR_DISABLE_CONNECTIVITY_GUARD = '1';
|
||||
try {
|
||||
setHostConnectivityGuardEnabled(true);
|
||||
attempt(ownFailure());
|
||||
attempt(ownFailure());
|
||||
expect(() =>
|
||||
beginGuardedHostRequest(URL_ON_ENDPOINT)
|
||||
).not.toThrow();
|
||||
} finally {
|
||||
if (original === undefined)
|
||||
delete process.env.IPTVNATOR_DISABLE_CONNECTIVITY_GUARD;
|
||||
else process.env.IPTVNATOR_DISABLE_CONNECTIVITY_GUARD = original;
|
||||
}
|
||||
});
|
||||
|
||||
it('does not charge a same-origin redirect hop to the guarded endpoint', () => {
|
||||
// `/player_api.php` -> 302 -> `/slow/player_api.php` on the same origin:
|
||||
// the origin answered, so its record must clear even though the failing
|
||||
|
||||
@@ -22,6 +22,16 @@ export { HostConnectivityGuardError };
|
||||
export type { HostRequestToken };
|
||||
|
||||
let sharedGuard: HostConnectivityGuard | null = null;
|
||||
let enabled = true;
|
||||
let currentTokens = new WeakSet<HostRequestToken>();
|
||||
|
||||
/** Apply a saved desktop preference without letting old requests affect a new guard. */
|
||||
export function setHostConnectivityGuardEnabled(value: boolean): void {
|
||||
if (enabled === value) return;
|
||||
enabled = value;
|
||||
sharedGuard = null;
|
||||
currentTokens = new WeakSet<HostRequestToken>();
|
||||
}
|
||||
|
||||
/** The guard both portal IPC handlers share. */
|
||||
export function getHostConnectivityGuard(): HostConnectivityGuard {
|
||||
@@ -40,6 +50,8 @@ export function getHostConnectivityGuard(): HostConnectivityGuard {
|
||||
|
||||
/** Test seam: forgets the shared guard so each spec starts clean. */
|
||||
export function resetHostConnectivityGuardForTests(): void {
|
||||
enabled = true;
|
||||
currentTokens = new WeakSet<HostRequestToken>();
|
||||
sharedGuard = null;
|
||||
}
|
||||
|
||||
@@ -53,7 +65,7 @@ export function resetHostConnectivityGuardForTests(): void {
|
||||
*/
|
||||
export function beginGuardedHostRequest(url: string): HostRequestToken | null {
|
||||
const endpoint = portalEndpointKeyOf(url);
|
||||
if (!endpoint) {
|
||||
if (!endpoint || !enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -62,6 +74,7 @@ export function beginGuardedHostRequest(url: string): HostRequestToken | null {
|
||||
throw new HostConnectivityGuardError(endpoint);
|
||||
}
|
||||
|
||||
currentTokens.add(check.token);
|
||||
return check.token;
|
||||
}
|
||||
|
||||
@@ -70,11 +83,14 @@ export function observeGuardedHostRequest(
|
||||
url: string
|
||||
): HostRequestToken | null {
|
||||
const endpoint = portalEndpointKeyOf(url);
|
||||
return endpoint ? getHostConnectivityGuard().observe(endpoint) : null;
|
||||
if (!endpoint || !enabled) return null;
|
||||
const token = getHostConnectivityGuard().observe(endpoint);
|
||||
currentTokens.add(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
export function reportGuardedHostSuccess(token: HostRequestToken | null): void {
|
||||
if (token) {
|
||||
if (token && currentTokens.has(token)) {
|
||||
getHostConnectivityGuard().reportSuccess(token);
|
||||
}
|
||||
}
|
||||
@@ -91,7 +107,7 @@ export function reportGuardedHostFailure(
|
||||
error: unknown,
|
||||
options: { countFailures?: boolean; requestUrl?: string } = {}
|
||||
): void {
|
||||
if (!token) {
|
||||
if (!token || !currentTokens.has(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user