mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
Merge origin/master into claude/server-storage-pwa
Resolves app-startup-status.component.ts: master replaced the progress spinner with static markup (its import goes), the branch's router import for the sign-in route stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
614b0d3500
239 files changed
+17820
-7244
No files matched your search
@@ -113,4 +113,5 @@ export * from './lib/xtream-connection-test';
|
||||
export * from './lib/portal-request-failure.util';
|
||||
|
||||
export * from './lib/source-health';
|
||||
export * from './lib/source-reachability';
|
||||
export * from './lib/server-storage.contract';
|
||||
@@ -16,7 +16,9 @@ describe('playlistDisplayLabel', () => {
|
||||
// The default :80 is dropped by URL normalization, which reads better
|
||||
// anyway; a non-default port is kept (see the case above).
|
||||
expect(
|
||||
playlistDisplayLabel('http://marveltv.info:80 usr== Tl24uy0xGi pas=== pNt9PQMmL2')
|
||||
playlistDisplayLabel(
|
||||
'http://marveltv.info:80 usr== Tl24uy0xGi pas=== pNt9PQMmL2'
|
||||
)
|
||||
).toBe('marveltv.info');
|
||||
});
|
||||
|
||||
@@ -67,7 +69,9 @@ describe('playlistDisplayLabel', () => {
|
||||
describe('playlistNameHasCredentials', () => {
|
||||
it('detects query-string credentials', () => {
|
||||
expect(
|
||||
playlistNameHasCredentials('http://x.tv/get.php?username=a&password=b')
|
||||
playlistNameHasCredentials(
|
||||
'http://x.tv/get.php?username=a&password=b'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -81,4 +85,13 @@ describe('playlistNameHasCredentials', () => {
|
||||
expect(playlistNameHasCredentials('cord-cutter')).toBe(false);
|
||||
expect(playlistNameHasCredentials('http://plain.tv:8080')).toBe(false);
|
||||
});
|
||||
|
||||
it('answers the same for the same name on every call', () => {
|
||||
const name = 'Portal usr== abc pas== def';
|
||||
expect([1, 2, 3].map(() => playlistNameHasCredentials(name))).toEqual([
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,8 @@
|
||||
*/
|
||||
|
||||
/** Query keys whose presence means the string is a credential-bearing URL. */
|
||||
const CREDENTIAL_KEYS = /(?:^|[?&;])(?:username|password|user|pass|token|auth)=/i;
|
||||
const CREDENTIAL_KEYS =
|
||||
/(?:^|[?&;])(?:username|password|user|pass|token|auth)=/i;
|
||||
|
||||
/** Hand-typed credential fragments, e.g. "usr== Tl24uy0xGi pas== abc". */
|
||||
const INLINE_CREDENTIALS =
|
||||
@@ -56,6 +57,9 @@ export function playlistNameHasCredentials(
|
||||
rawName: string | null | undefined
|
||||
): boolean {
|
||||
const name = rawName ?? '';
|
||||
// INLINE_CREDENTIALS is global for `replace`; `test` on a global pattern
|
||||
// resumes at its lastIndex, so a second call would skip the match.
|
||||
INLINE_CREDENTIALS.lastIndex = 0;
|
||||
return CREDENTIAL_KEYS.test(name) || INLINE_CREDENTIALS.test(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { SourceHealthReason } from './source-health';
|
||||
|
||||
/**
|
||||
* What a source list shows about one network source, folded from the six
|
||||
* health states: `ok` answers, `bad` does not, `expired` answers with an
|
||||
* expired account, `checking` is mid-check with nothing known yet, and
|
||||
* `unknown` was never checked or the check was inconclusive.
|
||||
*/
|
||||
export type SourceReachability =
|
||||
'ok' | 'bad' | 'expired' | 'checking' | 'unknown';
|
||||
|
||||
/** A reachability a check actually established. */
|
||||
export type ConclusiveSourceReachability = Extract<
|
||||
SourceReachability,
|
||||
'ok' | 'bad' | 'expired'
|
||||
>;
|
||||
|
||||
/** The two choices of a "responds / does not respond" status filter. */
|
||||
export type SourceStatusBucket = 'ok' | 'bad';
|
||||
|
||||
/** A network source's status as a list shows it. */
|
||||
export interface SourceStatus {
|
||||
reachability: SourceReachability;
|
||||
/** When the current run of failed checks began. */
|
||||
failingSince?: number;
|
||||
checkedAt?: number;
|
||||
/** A check is running; `reachability` is the last known result. */
|
||||
pending?: boolean;
|
||||
/**
|
||||
* Why the check concluded what it did (`auth`, `disabled`, `timeout`…),
|
||||
* when known; a list otherwise describes the reachability alone.
|
||||
*/
|
||||
reason?: SourceHealthReason;
|
||||
}
|
||||
|
||||
/** The last conclusive result kept for a source across restarts. */
|
||||
export interface SourceHealthHistoryEntry {
|
||||
state: ConclusiveSourceReachability;
|
||||
checkedAt: number;
|
||||
/** When the current run of failed checks began; absent while `ok`. */
|
||||
failingSince?: number;
|
||||
lastSuccessAt?: number;
|
||||
/** The cause of the last failed check; absent while `ok`. */
|
||||
reason?: SourceHealthReason;
|
||||
/**
|
||||
* Fingerprint of the connection that was checked
|
||||
* (`sourceHealthFingerprint`): a result for an edited address or account
|
||||
* is not this source's any more.
|
||||
*/
|
||||
connection?: string;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
getPlaylistSourceIcon,
|
||||
resolvePlaylistSourceIconKey,
|
||||
resolvePlaylistSourceKind,
|
||||
SOURCE_TYPE_ICONS,
|
||||
} from './source-type-icons.util';
|
||||
|
||||
@@ -40,4 +41,14 @@ describe('source type icons', () => {
|
||||
SOURCE_TYPE_ICONS[expected]
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ macAddress: '00:1A:79:00:00:01', serverUrl: 'http://x' }, 'stalker'],
|
||||
// An Xtream row missing a credential is still Xtream, not M3U: the
|
||||
// old list filter dropped it from every type.
|
||||
[{ serverUrl: 'http://xtream' }, 'xtream'],
|
||||
[{}, 'm3u'],
|
||||
] as const)('puts %o in the %s family', (playlist, expected) => {
|
||||
expect(resolvePlaylistSourceKind(playlist)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -48,3 +48,20 @@ export function getPlaylistSourceIcon(
|
||||
): string {
|
||||
return SOURCE_TYPE_ICONS[resolvePlaylistSourceIconKey(playlist)];
|
||||
}
|
||||
|
||||
/** The provider family a stored playlist belongs to. */
|
||||
export type PlaylistSourceKind = 'm3u' | 'xtream' | 'stalker';
|
||||
|
||||
/**
|
||||
* The provider family by the same precedence as the icon, so every type
|
||||
* filter, count and label agrees with the icon on the row: a playlist
|
||||
* counted as Xtream is also listed and drawn as Xtream.
|
||||
*/
|
||||
export function resolvePlaylistSourceKind(
|
||||
playlist: Pick<PlaylistMeta, 'macAddress' | 'serverUrl'>
|
||||
): PlaylistSourceKind {
|
||||
if (playlist.macAddress) {
|
||||
return 'stalker';
|
||||
}
|
||||
return playlist.serverUrl ? 'xtream' : 'm3u';
|
||||
}
|
||||
@@ -64,4 +64,6 @@ export interface StalkerVodInfo {
|
||||
* this to send Discover clicks to the right media type.
|
||||
*/
|
||||
tmdb_media_type?: TmdbMediaType;
|
||||
/** See `XtreamVodInfo.tmdb_supplied_release_date` — same contract */
|
||||
tmdb_supplied_release_date?: boolean;
|
||||
}
|
||||
Reference in new issue
Block a user