Files
iptvnator/libs/shared/interfaces/src/lib/stalker-item.normalizer.ts
T
4gray 2ac0de752f fix(skills): align repository guidance with implementation (#1315)
* docs(skills): design implementation synchronization

* docs(skills): plan implementation synchronization

* fix(release): filter internal notes from public body

* docs(release): synchronize release workflow guidance

* fix(stalker): normalize catalog series flags

* fix(stalker): preserve progress with scoped episode IDs

* fix(playback): expose strict position persistence

* docs(stalker): record series position compatibility

* test(skills): validate repository skill contracts

* fix(database): keep SQL trace values private

* docs(skills): refresh Nx and SQLite ownership

* docs(skills): align provider and UI guidance

* docs(skills): tighten validated guidance

* docs(release): require exact release pushes

* style(electron): remove trailing blank line

* fix(ci): classify repository skills coverage
2026-07-31 08:00:59 +02:00

187 lines
5.3 KiB
TypeScript

import {
PortalActivityType,
StalkerPortalItem,
} from './stalker-portal-item.interface';
const SQLITE_UTC_TIMESTAMP_PATTERN =
/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/;
/**
* Extract a stable ID from an untyped Stalker portal item.
*
* Stalker portals use different id fields depending on the content type
* (`id`, `stream_id`, `series_id`, `movie_id`). This function returns
* the first non-empty one, or a generated fallback based on the playlist
* id and index.
*/
export function extractStalkerItemId(
item: StalkerPortalItem | Record<string, unknown>,
fallbackPrefix = '',
fallbackIndex = 0
): string {
const raw = item as Record<string, unknown>;
const id =
raw['id'] ?? raw['stream_id'] ?? raw['series_id'] ?? raw['movie_id'];
const idStr = String(id ?? '').trim();
return idStr || `${fallbackPrefix}-${fallbackIndex}`;
}
/**
* Extract a display title from a Stalker portal item.
*/
export function extractStalkerItemTitle(
item: StalkerPortalItem | Record<string, unknown>
): string {
const raw = item as Record<string, unknown>;
return String(raw['title'] ?? raw['o_name'] ?? raw['name'] ?? 'Unknown');
}
/**
* Extract a poster/cover URL from a Stalker portal item.
*/
export function extractStalkerItemPoster(
item: StalkerPortalItem | Record<string, unknown>
): string {
const raw = item as Record<string, unknown>;
return String(raw['cover'] ?? raw['logo'] ?? raw['poster_url'] ?? '');
}
/**
* Determine the normalised activity type of a Stalker item.
*
* - `itv` / `live` / radio → `'live'`
* - `series` or `is_series` equal to `true`, `1`, or `'1'` → `'series'`
* - everything else → `'movie'`
*/
export function extractStalkerItemType(
item: StalkerPortalItem | Record<string, unknown>
): PortalActivityType {
const raw = item as Record<string, unknown>;
const categoryId = String(raw['category_id'] ?? '').toLowerCase();
const streamType = String(raw['stream_type'] ?? '').toLowerCase();
if (
categoryId === 'itv' ||
streamType === 'live' ||
isStalkerRadioItem(raw)
) {
return 'live';
}
if (
categoryId === 'series' ||
raw['is_series'] === true ||
raw['is_series'] === 1 ||
raw['is_series'] === '1'
) {
return 'series';
}
return 'movie';
}
/**
* Detect radio station records from Stalker favorites/recently-viewed payloads.
*/
export function isStalkerRadioItem(
item: StalkerPortalItem | Record<string, unknown>
): boolean {
const raw = item as Record<string, unknown>;
const radio = raw['radio'];
const radioFlag =
radio === true ||
radio === 1 ||
String(radio ?? '')
.trim()
.toLowerCase() === 'true' ||
String(radio ?? '').trim() === '1';
const categoryId = String(raw['category_id'] ?? '')
.trim()
.toLowerCase();
const streamType = String(raw['stream_type'] ?? '')
.trim()
.toLowerCase();
const cmd = String(raw['cmd'] ?? '')
.trim()
.toLowerCase();
return (
radioFlag ||
categoryId === 'radio' ||
streamType === 'radio' ||
cmd.includes('://radio/')
);
}
/**
* Normalise a raw date value (epoch number, numeric string, or ISO string)
* into a consistent ISO 8601 string.
*/
export function normalizeStalkerDate(value: unknown): string {
if (typeof value === 'number') {
if (!Number.isFinite(value)) return '';
const ms = value > 10_000_000_000 ? value : value * 1000;
return new Date(ms).toISOString();
}
if (typeof value === 'string') {
const trimmed = value.trim();
if (/^\d+$/.test(trimmed)) {
const numeric = Number(trimmed);
if (Number.isFinite(numeric)) {
const ms = numeric > 10_000_000_000 ? numeric : numeric * 1000;
return new Date(ms).toISOString();
}
}
const sqliteUtcIso = normalizeSqliteUtcTimestamp(trimmed);
if (sqliteUtcIso) {
return sqliteUtcIso;
}
const parsed = Date.parse(trimmed);
if (!Number.isNaN(parsed)) {
return new Date(parsed).toISOString();
}
}
return '';
}
/**
* Check whether a raw item from `Playlist.favorites` matches the given itemId.
*
* Used to filter stalker favorites/recently-viewed arrays when removing items.
*/
export function stalkerItemMatchesId(
raw: unknown,
targetId: string,
fallbackPrefix = '',
fallbackIndex = 0
): boolean {
const item = (raw ?? {}) as Record<string, unknown>;
const rawId = extractStalkerItemId(item, fallbackPrefix, fallbackIndex);
return rawId === targetId;
}
function normalizeSqliteUtcTimestamp(value: string): string | null {
const match = SQLITE_UTC_TIMESTAMP_PATTERN.exec(value);
if (!match) {
return null;
}
const [, year, month, day, hours, minutes, seconds, milliseconds = '0'] =
match;
const normalizedMs = milliseconds.padEnd(3, '0').slice(0, 3);
return new Date(
Date.UTC(
Number(year),
Number(month) - 1,
Number(day),
Number(hours),
Number(minutes),
Number(seconds),
Number(normalizedMs)
)
).toISOString();
}