mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(dashboard): address PR review feedback
This commit is contained in:
1 parent
7083bd2af4
commit
2ecb5c28f4
30 files changed
+340
-104
No files matched your search
@@ -242,6 +242,7 @@ describe('DashboardDataService', () => {
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
poster_url: 'https://example.com/logo-1.png',
|
||||
epg_lookup_key: 'tvg-1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'https://example.com/stream-2.m3u8',
|
||||
@@ -250,6 +251,7 @@ describe('DashboardDataService', () => {
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
poster_url: 'https://example.com/logo-2.png',
|
||||
epg_lookup_key: 'tvg-2',
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -436,6 +438,7 @@ describe('DashboardDataService', () => {
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
xtream_id: 'https://example.com/stream-1.m3u8',
|
||||
epg_lookup_key: 'tvg-1',
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -696,10 +699,27 @@ describe('DashboardDataService', () => {
|
||||
const series = service
|
||||
.globalRecentVodItems()
|
||||
.find((item) => item.title === 'Shadow Bay');
|
||||
const position = service.getPlaybackPositionForItem(series!);
|
||||
const positionsMap = service.playbackPositions$();
|
||||
const failOnLinearScan = jest.fn(() => {
|
||||
throw new Error('series lookup should not scan all positions');
|
||||
});
|
||||
(
|
||||
positionsMap as unknown as {
|
||||
values: () => IterableIterator<PlaybackPositionData>;
|
||||
}
|
||||
).values =
|
||||
failOnLinearScan as unknown as () => IterableIterator<PlaybackPositionData>;
|
||||
|
||||
expect(series).toBeDefined();
|
||||
if (!series) {
|
||||
throw new Error('expected Shadow Bay recent item');
|
||||
}
|
||||
|
||||
const position = service.getPlaybackPositionForItem(series);
|
||||
expect(position?.contentType).toBe('episode');
|
||||
expect(position?.seasonNumber).toBe(3);
|
||||
expect(position?.episodeNumber).toBe(7);
|
||||
expect(failOnLinearScan).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exposes a live-only slice of global favorites so the dashboard can promote favorited channels into the Live rail', async () => {
|
||||
|
||||
@@ -65,6 +65,28 @@ function playbackPositionMapKey(
|
||||
return `${playlistId}::${contentXtreamId}::${contentType}`;
|
||||
}
|
||||
|
||||
function seriesPlaybackPositionMapKey(
|
||||
playlistId: string,
|
||||
seriesXtreamId: number
|
||||
): string {
|
||||
return `${playlistId}::${seriesXtreamId}`;
|
||||
}
|
||||
|
||||
function newestPlaybackPosition(
|
||||
current: PlaybackPositionData | null | undefined,
|
||||
candidate: PlaybackPositionData | null | undefined
|
||||
): PlaybackPositionData | null {
|
||||
if (!current) {
|
||||
return candidate ?? null;
|
||||
}
|
||||
if (!candidate) {
|
||||
return current;
|
||||
}
|
||||
return (candidate.updatedAt ?? '') > (current.updatedAt ?? '')
|
||||
? candidate
|
||||
: current;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link PortalRecentItem} from `@iptvnator/shared/interfaces` instead. */
|
||||
export type GlobalRecentItem = PortalRecentItem;
|
||||
/** @deprecated Use {@link PortalFavoriteItem} from `@iptvnator/shared/interfaces` instead. */
|
||||
@@ -239,6 +261,9 @@ export class DashboardDataService {
|
||||
private readonly playbackPositionsMap = signal<
|
||||
Map<string, PlaybackPositionData>
|
||||
>(new Map());
|
||||
private readonly playbackPositionsBySeriesMap = signal<
|
||||
Map<string, PlaybackPositionData>
|
||||
>(new Map());
|
||||
|
||||
readonly playbackPositions$ = this.playbackPositionsMap.asReadonly();
|
||||
|
||||
@@ -267,29 +292,17 @@ export class DashboardDataService {
|
||||
|
||||
// Series recent_items rows carry either the series id (the series
|
||||
// landing page writes the series itself) or, for direct-play flows,
|
||||
// the episode id. Match both shapes and prefer the most recently
|
||||
// updated episode position so the card can show resume progress and
|
||||
// a "S2 · E5" badge regardless of how the row was originally saved.
|
||||
let best: PlaybackPositionData | null = null;
|
||||
for (const position of this.playbackPositionsMap().values()) {
|
||||
if (position.contentType !== 'episode') continue;
|
||||
if (
|
||||
position.playlistId &&
|
||||
position.playlistId !== item.playlist_id
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const matchesEpisode = position.contentXtreamId === xtreamId;
|
||||
const matchesSeries = position.seriesXtreamId === xtreamId;
|
||||
if (!matchesEpisode && !matchesSeries) continue;
|
||||
if (
|
||||
!best ||
|
||||
(position.updatedAt ?? '') > (best.updatedAt ?? '')
|
||||
) {
|
||||
best = position;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
// the episode id. Match both shapes through keyed maps so card renders
|
||||
// do not scan every saved playback position.
|
||||
const episodePosition =
|
||||
this.playbackPositionsMap().get(
|
||||
playbackPositionMapKey(item.playlist_id, xtreamId, 'episode')
|
||||
) ?? null;
|
||||
const seriesPosition =
|
||||
this.playbackPositionsBySeriesMap().get(
|
||||
seriesPlaybackPositionMapKey(item.playlist_id, xtreamId)
|
||||
) ?? null;
|
||||
return newestPlaybackPosition(episodePosition, seriesPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,10 +320,12 @@ export class DashboardDataService {
|
||||
}
|
||||
if (playlistIds.size === 0) {
|
||||
this.playbackPositionsMap.set(new Map());
|
||||
this.playbackPositionsBySeriesMap.set(new Map());
|
||||
return;
|
||||
}
|
||||
|
||||
const next = new Map<string, PlaybackPositionData>();
|
||||
const nextBySeries = new Map<string, PlaybackPositionData>();
|
||||
for (const playlistId of playlistIds) {
|
||||
try {
|
||||
const positions =
|
||||
@@ -326,6 +341,22 @@ export class DashboardDataService {
|
||||
),
|
||||
position
|
||||
);
|
||||
if (
|
||||
position.contentType === 'episode' &&
|
||||
Number.isFinite(position.seriesXtreamId)
|
||||
) {
|
||||
const seriesKey = seriesPlaybackPositionMapKey(
|
||||
playlistId,
|
||||
position.seriesXtreamId as number
|
||||
);
|
||||
nextBySeries.set(
|
||||
seriesKey,
|
||||
newestPlaybackPosition(
|
||||
nextBySeries.get(seriesKey),
|
||||
position
|
||||
) as PlaybackPositionData
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
@@ -336,7 +367,10 @@ export class DashboardDataService {
|
||||
}
|
||||
}
|
||||
|
||||
this.ngZone.run(() => this.playbackPositionsMap.set(next));
|
||||
this.ngZone.run(() => {
|
||||
this.playbackPositionsMap.set(next);
|
||||
this.playbackPositionsBySeriesMap.set(nextBySeries);
|
||||
});
|
||||
}
|
||||
|
||||
readonly stalkerGlobalFavorites = computed<DashboardFavoriteItem[]>(() => {
|
||||
@@ -1041,6 +1075,11 @@ export class DashboardDataService {
|
||||
category_id: 'live',
|
||||
xtream_id: matchedFavoriteId,
|
||||
poster_url: channel.tvg?.logo || undefined,
|
||||
epg_lookup_key:
|
||||
channel.tvg?.id?.trim() ||
|
||||
channel.tvg?.name?.trim() ||
|
||||
channel.name?.trim() ||
|
||||
undefined,
|
||||
source: 'm3u',
|
||||
});
|
||||
return acc;
|
||||
|
||||
Reference in new issue
Block a user