mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { isHostConnectivityFastFailMessage } from './host-connectivity.util';
|
|
import { isStalkerAuthFailureMessage } from './stalker-auth-failure.util';
|
|
import type { PlaylistMeta } from './playlist-meta.type';
|
|
|
|
export type SourceHealthState =
|
|
'unknown' | 'checking' | 'active' | 'inactive' | 'expired' | 'unavailable';
|
|
export type SourceHealthReason =
|
|
| 'available'
|
|
| 'disabled'
|
|
| 'expired'
|
|
| 'auth'
|
|
| 'http'
|
|
| 'timeout'
|
|
| 'network'
|
|
| 'paused'
|
|
| 'invalid'
|
|
| 'unknown'
|
|
| 'cancelled';
|
|
export interface SourceHealthResult {
|
|
state: SourceHealthState;
|
|
reason: SourceHealthReason;
|
|
expiresAtSeconds?: number;
|
|
httpStatus?: number;
|
|
confirmedInactive: boolean;
|
|
}
|
|
export interface SourceHealthSnapshot extends SourceHealthResult {
|
|
checkedAt: number;
|
|
lastSuccessAt?: number;
|
|
}
|
|
export interface SourceProbeContext {
|
|
requestId: string;
|
|
deadlineAt: number;
|
|
}
|
|
export const SOURCE_HEALTH_CANCEL = 'SOURCE_HEALTH_CANCEL';
|
|
export const M3U_SOURCE_PROBE = 'M3U_SOURCE_PROBE';
|
|
export function sourceHealthUnknown(
|
|
reason: SourceHealthReason = 'unknown'
|
|
): SourceHealthResult {
|
|
return { state: 'unavailable', reason, confirmedInactive: false };
|
|
}
|
|
export function sourceHealthType(
|
|
p: Partial<PlaylistMeta>
|
|
): 'stalker' | 'xtream' | 'm3u' | null {
|
|
if (p.macAddress && p.portalUrl) return 'stalker';
|
|
if (p.serverUrl && p.username && p.password) return 'xtream';
|
|
return p.url && /^https?:\/\//i.test(p.url) ? 'm3u' : null;
|
|
}
|
|
export function sourceHealthKey(p: Partial<PlaylistMeta>): string {
|
|
return JSON.stringify([
|
|
sourceHealthType(p),
|
|
p.url,
|
|
p.serverUrl,
|
|
p.portalUrl,
|
|
p.username,
|
|
p.password,
|
|
p.macAddress,
|
|
p.userAgent,
|
|
p.isFullStalkerPortal,
|
|
p.stalkerSerialNumber,
|
|
p.stalkerDeviceId1,
|
|
p.stalkerDeviceId2,
|
|
p.stalkerSignature1,
|
|
p.stalkerSignature2,
|
|
]);
|
|
}
|
|
export function accountHealth(
|
|
status: unknown,
|
|
expiration?: number,
|
|
authenticated = false
|
|
): SourceHealthResult {
|
|
const value = String(status ?? '')
|
|
.trim()
|
|
.toLowerCase();
|
|
if (value === 'expired' || (expiration && expiration * 1000 < Date.now())) {
|
|
return {
|
|
state: 'expired',
|
|
reason: 'expired',
|
|
confirmedInactive: true,
|
|
expiresAtSeconds: expiration,
|
|
};
|
|
}
|
|
if (['disabled', 'banned', 'inactive'].includes(value)) {
|
|
return {
|
|
state: 'inactive',
|
|
reason: 'disabled',
|
|
confirmedInactive: true,
|
|
expiresAtSeconds: expiration,
|
|
};
|
|
}
|
|
if (value === 'active' || authenticated) {
|
|
return {
|
|
state: 'active',
|
|
reason: 'available',
|
|
confirmedInactive: false,
|
|
expiresAtSeconds: expiration,
|
|
};
|
|
}
|
|
return sourceHealthUnknown();
|
|
}
|
|
export function sourceHealthError(error: unknown): SourceHealthResult {
|
|
const text = error instanceof Error ? error.message : String(error);
|
|
if (/abort|cancel/i.test(text)) return sourceHealthUnknown('cancelled');
|
|
if (
|
|
isHostConnectivityFastFailMessage(text) ||
|
|
/guard|cooldown|paused|circuit/i.test(text)
|
|
)
|
|
return sourceHealthUnknown('paused');
|
|
if (/timeout|timed out|deadline/i.test(text))
|
|
return sourceHealthUnknown('timeout');
|
|
if (
|
|
isStalkerAuthFailureMessage(text) ||
|
|
(text.toLowerCase().includes('http error') &&
|
|
(text.includes('401') || text.includes('403')))
|
|
)
|
|
return { ...sourceHealthUnknown('auth'), state: 'inactive' };
|
|
if (/HTTP Error/i.test(text)) return sourceHealthUnknown('http');
|
|
return sourceHealthUnknown('network');
|
|
}
|