mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
The dashboard hero showed no backdrop for items whose detail page had one. Two independent causes, both about identity rather than the matching gate. Stalker items never reach the `content` table, so the xtream back-fill of `content.backdrop_url` has no equivalent for them — but the enriched backdrop is already sitting in the stored playlist entry (`info.tmdb_backdrop`). The activity mappers now surface it as `backdrop_url`, where the hero already looks first. The hero's TMDB lookup ran on the display title alone, while the detail view searched with the original title and the release year. Without a year `pickConfidentMatch` requires a single exact title match, which common titles never satisfy, and the miss lands in the negative cache under a lookup key the detail view's hit can never be found at. The query is now built from the same fields (`extractStalkerItemTmdbHints`), and the resolved `tmdb_id` short-circuits the search entirely. A 'movie' verdict retries as 'tv' without the id — 'movie' is the answer every row falls back to, and an id is valid only for its own media type. A 'tv' verdict, reached only on positive series evidence, gets no retry back to 'movie'. Also removes `buildStalkerRecentItems`, a dead duplicate of the mapper the dashboard actually uses.
135 lines
4.2 KiB
TypeScript
135 lines
4.2 KiB
TypeScript
import { PlaylistMeta } from './playlist-meta.type';
|
|
import { PortalRecentItem } from './portal-activity-item.interface';
|
|
import { extractStalkerItemTmdbHints } from './stalker-item-tmdb-hints';
|
|
import {
|
|
extractStalkerItemId,
|
|
extractStalkerItemPoster,
|
|
extractStalkerItemTitle,
|
|
extractStalkerItemType,
|
|
normalizeStalkerDate,
|
|
} from './stalker-item.normalizer';
|
|
import {
|
|
PlaylistRecentlyViewedItem,
|
|
isM3uRecentlyViewedItem,
|
|
} from './playlist-recently-viewed.interface';
|
|
|
|
interface PlaylistRecentLabels {
|
|
stalker: string;
|
|
m3u: string;
|
|
}
|
|
|
|
function firstNonEmpty(...values: Array<string | undefined>): string | undefined {
|
|
return values.map((value) => value?.trim()).find(Boolean);
|
|
}
|
|
|
|
function getPlaylistRecentItems(
|
|
playlist: PlaylistMeta
|
|
): PlaylistRecentlyViewedItem[] {
|
|
return Array.isArray(playlist.recentlyViewed)
|
|
? (playlist.recentlyViewed as PlaylistRecentlyViewedItem[])
|
|
: [];
|
|
}
|
|
|
|
function mapStalkerPlaylistRecentItems(
|
|
playlist: PlaylistMeta,
|
|
defaultPlaylistName: string
|
|
): PortalRecentItem[] {
|
|
return getPlaylistRecentItems(playlist).reduce<PortalRecentItem[]>(
|
|
(acc, rawItem, index) => {
|
|
if (isM3uRecentlyViewedItem(rawItem)) {
|
|
return acc;
|
|
}
|
|
|
|
const item = (rawItem ?? {}) as Record<string, unknown>;
|
|
const id = extractStalkerItemId(item, playlist._id, index);
|
|
|
|
acc.push({
|
|
id,
|
|
title: extractStalkerItemTitle(item),
|
|
type: extractStalkerItemType(item),
|
|
playlist_id: playlist._id,
|
|
playlist_name: playlist.title || defaultPlaylistName,
|
|
viewed_at: normalizeStalkerDate(item['added_at']),
|
|
category_id: String(item['category_id'] ?? ''),
|
|
xtream_id: id,
|
|
poster_url: extractStalkerItemPoster(item),
|
|
// Stalker has no `content` row to back-fill, so the backdrop
|
|
// travels inside the stored entry itself — present once the
|
|
// detail view has enriched the item.
|
|
backdrop_url: extractStalkerItemTmdbHints(item).backdropUrl,
|
|
source: 'stalker',
|
|
stalker_item: rawItem,
|
|
});
|
|
|
|
return acc;
|
|
},
|
|
[]
|
|
);
|
|
}
|
|
|
|
function mapM3uPlaylistRecentItems(
|
|
playlist: PlaylistMeta,
|
|
defaultPlaylistName: string
|
|
): PortalRecentItem[] {
|
|
return getPlaylistRecentItems(playlist).reduce<PortalRecentItem[]>(
|
|
(acc, rawItem) => {
|
|
if (!isM3uRecentlyViewedItem(rawItem)) {
|
|
return acc;
|
|
}
|
|
|
|
const channelUrl = rawItem.url.trim();
|
|
if (!channelUrl) {
|
|
return acc;
|
|
}
|
|
|
|
acc.push({
|
|
id: rawItem.id || channelUrl,
|
|
title:
|
|
firstNonEmpty(
|
|
rawItem.title,
|
|
rawItem.tvg_name,
|
|
rawItem.channel_id
|
|
) ||
|
|
channelUrl,
|
|
type: 'live',
|
|
playlist_id: playlist._id,
|
|
playlist_name: playlist.title || defaultPlaylistName,
|
|
viewed_at: normalizeStalkerDate(rawItem.added_at),
|
|
category_id: rawItem.category_id || 'live',
|
|
xtream_id: channelUrl,
|
|
poster_url: rawItem.poster_url || undefined,
|
|
epg_lookup_key: firstNonEmpty(
|
|
rawItem.tvg_id,
|
|
rawItem.tvg_name,
|
|
rawItem.title,
|
|
rawItem.channel_id
|
|
),
|
|
source: 'm3u',
|
|
});
|
|
|
|
return acc;
|
|
},
|
|
[]
|
|
);
|
|
}
|
|
|
|
export function buildPlaylistRecentItems(
|
|
playlists: PlaylistMeta[],
|
|
labels: PlaylistRecentLabels
|
|
): PortalRecentItem[] {
|
|
return playlists.reduce<PortalRecentItem[]>((acc, playlist) => {
|
|
if (playlist.macAddress) {
|
|
acc.push(
|
|
...mapStalkerPlaylistRecentItems(playlist, labels.stalker)
|
|
);
|
|
return acc;
|
|
}
|
|
|
|
if (!playlist.serverUrl) {
|
|
acc.push(...mapM3uPlaylistRecentItems(playlist, labels.m3u));
|
|
}
|
|
|
|
return acc;
|
|
}, []);
|
|
}
|