mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
- Moving browser playlists matches them to the server's by id or source and carries the legacy Xtream maps under the server id; it no longer goes through the backup format, whose PWA export has no Xtream user data. A partial move stays on offer. - User-data batches go out in order, and a re-read keeps entries edited while it was in flight. - A keepPayload write keeps its cached copy only when the server replaced the version its payload came from (new X-Replaced-ETag header). - 502/503/504 from the proxy count as offline. - The start-up screen lets the sign-in page through while a token is required. - Settings → Server offers a reload after a move. - The probe detects Electron through RuntimeCapabilitiesService. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
import { RuntimeCapabilitiesService } from '@iptvnator/services';
|
|
import type { ServerInstanceInfo } from '@iptvnator/shared/interfaces';
|
|
import type { IptvnatorRuntimeConfig } from '../services/runtime-config';
|
|
|
|
const PROBE_TIMEOUT_MS = 2_500;
|
|
|
|
/**
|
|
* - `skipped`: no runtime backend URL, or Electron.
|
|
* - `server` / `none`: the backend answered.
|
|
* - `unreachable`: no answer, a timeout or a 5xx (nginx answers 502 while
|
|
* the backend restarts), so the result says nothing about storage.
|
|
*/
|
|
export type ServerInstanceProbe =
|
|
| { readonly kind: 'skipped' | 'none' | 'unreachable' }
|
|
| { readonly kind: 'server'; readonly instance: ServerInstanceInfo };
|
|
|
|
export interface ServerInstanceProbeOptions {
|
|
readonly runtimeConfig?: IptvnatorRuntimeConfig;
|
|
readonly isElectron?: boolean;
|
|
readonly fetchFn?: typeof fetch;
|
|
}
|
|
|
|
/**
|
|
* Asks the backend whether it offers server storage. Runs only when the
|
|
* deployment wrote a runtime `BACKEND_URL` (the Docker entrypoint does), so
|
|
* hosted builds never call an external parser API for it.
|
|
*/
|
|
export async function probeServerInstance(
|
|
options: ServerInstanceProbeOptions = {}
|
|
): Promise<ServerInstanceProbe> {
|
|
const runtimeConfig =
|
|
options.runtimeConfig ?? globalThis.window?.__IPTVNATOR_CONFIG__;
|
|
// The capability service has no dependencies, so it works before
|
|
// bootstrap and keeps one definition of the desktop runtime.
|
|
const isElectron =
|
|
options.isElectron ?? new RuntimeCapabilitiesService().isElectron;
|
|
const baseUrl = runtimeConfig?.BACKEND_URL?.trim();
|
|
if (isElectron || !baseUrl) {
|
|
return { kind: 'skipped' };
|
|
}
|
|
|
|
let response: Response;
|
|
try {
|
|
const fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
response = await fetchFn(`${baseUrl.replace(/\/+$/, '')}/v1/instance`, {
|
|
cache: 'no-store',
|
|
signal:
|
|
typeof AbortSignal.timeout === 'function'
|
|
? AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
: undefined,
|
|
});
|
|
} catch {
|
|
return { kind: 'unreachable' };
|
|
}
|
|
if (response.status >= 500) {
|
|
return { kind: 'unreachable' };
|
|
}
|
|
if (!response.ok) {
|
|
// A backend from before server storage has no /v1 routes.
|
|
return { kind: 'none' };
|
|
}
|
|
try {
|
|
const body: unknown = await response.json();
|
|
return isServerInstance(body) && body.storage === 'server'
|
|
? { kind: 'server', instance: body }
|
|
: { kind: 'none' };
|
|
} catch {
|
|
return { kind: 'none' };
|
|
}
|
|
}
|
|
|
|
function isServerInstance(value: unknown): value is ServerInstanceInfo {
|
|
const record = value as Partial<ServerInstanceInfo> | null;
|
|
return (
|
|
!!record &&
|
|
record.service === 'iptvnator' &&
|
|
typeof record.apiVersion === 'number' &&
|
|
typeof record.instanceId === 'string' &&
|
|
(record.storage === 'server' || record.storage === 'none') &&
|
|
(record.auth === 'none' || record.auth === 'token')
|
|
);
|
|
}
|