Files
iptvnator/libs/shared/interfaces/src/lib/playlist-recently-viewed.utils.spec.ts
T
4grayandClaude Opus 5 4e575a810e feat(dashboard): say where you left off instead of which provider it came from (#1646)
Every dashboard title carried a "Xtream · Series" / "Stalker · Movie" subtitle. Provider kind and content kind are the app's own taxonomy, not a property of the title, and they are identical on every card in a rail — so the one line that could tell two cards apart said nothing.

The hero now shows the source name alone, through `playlistDisplayLabel` (a stored playlist name is routinely the pasted URL with credentials, or a MAC). Continue Watching cards show what actually varies: the "S1·E5" chip plus "12 min left". Favorites keep the title alone, Recently Added keeps the source name, and a meta row with nothing in it is no longer rendered.

Stalker shows filed under Movies had no badge, no progress and no resume. An embedded-VOD row announces its episodes through a `series[]` array and carries no `is_series` flag, so `extractStalkerItemType` reports `movie` on purpose — the item must keep routing to the VOD catalog — while its progress is a set of episode positions keyed by the parent id, which the dashboard was looking up as a single `vod` row and never finding.

Split the two questions: `PortalActivityItem.watch_kind` records the progress model when it differs from the routing type, and every reader that has to choose goes through `resolvePortalActivityWatchKind` instead of `type`. That makes the resume handoff reachable for Stalker, so wire it through `STALKER_SERIES_RESUME_TARGET`: consumed once after the series positions are read, hydrating a lazy Ministra season first with a bounded two-attempt retry, and playing nothing at all when the position read failed rather than restarting the episode from zero.

Also fixes the global-recent route template, which bound `[seriesResume]` only on its Xtream branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 22:19:47 +02:00

93 lines
3.1 KiB
TypeScript

import { buildPlaylistRecentItems } from './playlist-recently-viewed.utils';
import { resolvePortalActivityWatchKind } from './portal-activity-item.interface';
import type { PlaylistMeta } from './playlist-meta.type';
const labels = { stalker: 'Stalker', m3u: 'M3U' };
function stalkerPlaylist(recentlyViewed: unknown[]): PlaylistMeta {
return {
_id: 'stalker-1',
title: 'Portal',
count: 0,
importDate: '2026-01-01T00:00:00.000Z',
autoRefresh: false,
macAddress: '00:1A:79:00:00:01',
recentlyViewed,
} as unknown as PlaylistMeta;
}
describe('buildPlaylistRecentItems (Stalker watch kind)', () => {
it('keeps embedded-VOD shows routing as movies while tracking them as series', () => {
// Real stored shape: no `is_series`, numeric VOD category, and the
// `series[]` episode array the portal answered with.
const [item] = buildPlaylistRecentItems(
[
stalkerPlaylist([
{
id: '17572',
title: 'Fake (10 episodes)',
category_id: '7',
series: [1, 2, 3],
added_at: '2026-09-19T16:13:50.000Z',
},
]),
],
labels
);
expect(item.type).toBe('movie');
expect(item.watch_kind).toBe('series');
expect(resolvePortalActivityWatchKind(item)).toBe('series');
});
it('marks lazy Ministra is_series rows as series-tracked too', () => {
const [item] = buildPlaylistRecentItems(
[
stalkerPlaylist([
{
id: '50001',
title: 'Flagged',
category_id: 'vod',
is_series: '1',
added_at: '2026-09-19T16:13:50.000Z',
},
]),
],
labels
);
expect(item.type).toBe('series');
expect(item.watch_kind).toBe('series');
});
it('leaves plain movies and live channels without a watch kind override', () => {
const items = buildPlaylistRecentItems(
[
stalkerPlaylist([
{
id: '900',
title: 'A Movie',
category_id: '7',
series: [],
added_at: '2026-09-19T16:13:50.000Z',
},
{
id: '712',
title: 'Channel',
category_id: 'itv',
added_at: '2026-09-19T14:49:22.000Z',
},
]),
],
labels
);
expect(items.map((item) => item.watch_kind)).toEqual([
undefined,
undefined,
]);
expect(resolvePortalActivityWatchKind(items[0])).toBe('movie');
expect(resolvePortalActivityWatchKind(items[1])).toBeNull();
});
});