fix(xtream): confirm browser catalog identity before updating favorites

This commit is contained in:
4gray committed 2026-10-14 05:23:07 +02:00
1 parent 2d975e0b29
commit 8148f334e6
6 files changed
+302 -105

No files matched your search

+129 -82
View File
@@ -1,3 +1,4 @@
import { readFile } from 'node:fs/promises';
import { expect, test } from './fixtures';
import {
addXtreamPortal,
@@ -13,90 +14,136 @@ test.beforeEach(async ({ page, request }) => {
await interceptXtreamRequests(page);
});
test('@xtream @collections catalog browsing preserves imported favorite metadata after reload', async ({
page,
request,
}) => {
const response = await request.get(`${MOCK_SERVER}/player_api.php`, {
params: {
username: DEFAULT_USERNAME,
password: DEFAULT_PASSWORD,
action: 'get_vod_streams',
},
});
expect(response.ok()).toBeTruthy();
const movie = (await response.json())[0];
await addXtreamPortal(page, { name: 'Saved Metadata Portal' });
const playlistPath = new URL(page.url()).pathname.replace(/\/vod.*$/, '');
const rawId = playlistPath.split('/').pop();
if (!rawId) throw new Error('Missing playlist ID in the portal route');
const playlistId = decodeURIComponent(rawId);
const favorite = {
contentType: 'movie',
xtreamId: movie.stream_id,
addedAt: '2026-01-02T12:00:00.000Z',
position: 3,
snapshot: {
title: movie.name,
tmdbId: 949,
releaseYear: 1995,
originalTitle: 'Saved original title',
},
};
await page.evaluate(
({ playlistId, favorite }) =>
localStorage.setItem(
`xtream-restore-${playlistId}`,
JSON.stringify({
hiddenCategories: [],
favorites: [favorite],
recentlyViewed: [],
playbackPositions: [],
})
),
{ playlistId, favorite }
);
await page.reload();
await expect
.poll(() =>
page.evaluate(
(id) => localStorage.getItem(`xtream-restore-${id}`),
playlistId
)
)
.toBeNull();
await page
.locator(`[data-category-id="${movie.category_id}"]`)
.first()
.click();
await expect(
page.getByText(movie.name, { exact: true }).first()
).toBeVisible();
// Keep the catalog in memory while opening Favorites, exercising the merge.
await page.getByRole('link', { name: 'Global favorites', exact: true }).click();
await expect(
page.getByText(movie.name, { exact: true }).first()
).toBeVisible();
await page.reload();
await expect(
page.getByText(movie.name, { exact: true }).first()
).toBeVisible();
expect(
for (const identity of ['missing', 'conflicting'] as const) {
test(`@xtream @collections ${identity} catalog identity preserves saved favorites through reload and export`, async ({
page,
request,
}) => {
const response = await request.get(`${MOCK_SERVER}/player_api.php`, {
params: {
username: DEFAULT_USERNAME,
password: DEFAULT_PASSWORD,
action: 'get_vod_streams',
},
});
expect(response.ok()).toBeTruthy();
const movie = (await response.json())[0];
await page.route('**/localhost:3000/xtream**', async (route) => {
const original = new URL(route.request().url());
if (original.searchParams.get('action') !== 'get_vod_streams') {
await route.fallback();
return;
}
const mockUrl = new URL(`${MOCK_SERVER}/xtream`);
original.searchParams.forEach((value, key) => {
if (key !== 'targetId') mockUrl.searchParams.set(key, value);
});
const response = await route.fetch({ url: mockUrl.toString() });
const body = (await response.json()) as {
payload: Record<string, unknown>[];
};
for (const item of body.payload) {
delete item['tmdb_id'];
delete item['release_year'];
if (identity === 'conflicting') {
item['tmdb_id'] = 999;
item['release_year'] = 2026;
}
}
await route.fulfill({ response, json: body });
});
await addXtreamPortal(page, { name: 'Saved Metadata Portal' });
const playlistPath = new URL(page.url()).pathname.replace(
/\/vod.*$/,
''
);
const rawId = playlistPath.split('/').pop();
if (!rawId) throw new Error('Missing playlist ID in the portal route');
const playlistId = decodeURIComponent(rawId);
const favorite = {
contentType: 'movie',
xtreamId: movie.stream_id,
addedAt: '2026-01-02T12:00:00.000Z',
position: 3,
snapshot: {
title: 'Saved original favorite',
posterUrl: '/assets/icons/icon-192x192.png',
tmdbId: 949,
releaseYear: 1995,
originalTitle: 'Saved original title',
},
};
await page.evaluate(
({ playlistId, key }) =>
JSON.parse(
localStorage.getItem('xtream-collection-items') ?? '{}'
)[playlistId]?.[key],
{ playlistId, key: `movie:${movie.stream_id}` }
)
).toMatchObject({
tmdb_id: 949,
release_year: 1995,
original_title: 'Saved original title',
added_at: favorite.addedAt,
position: 3,
({ playlistId, favorite }) =>
localStorage.setItem(
`xtream-restore-${playlistId}`,
JSON.stringify({
hiddenCategories: [],
favorites: [favorite],
recentlyViewed: [],
playbackPositions: [],
})
),
{ playlistId, favorite }
);
await page.reload();
await expect
.poll(() =>
page.evaluate(
(id) => localStorage.getItem(`xtream-restore-${id}`),
playlistId
)
)
.toBeNull();
await page
.locator(`[data-category-id="${movie.category_id}"]`)
.first()
.click();
await expect(
page.getByText(movie.name, { exact: true }).first()
).toBeVisible();
// Keep the catalog in memory while opening Favorites, exercising the merge.
await page
.getByRole('link', { name: 'Global favorites', exact: true })
.click();
await expect(
page.getByText(favorite.snapshot.title, { exact: true }).first()
).toBeVisible();
await page.reload();
await expect(
page.getByText(favorite.snapshot.title, { exact: true }).first()
).toBeVisible();
expect(
await page.evaluate(
({ playlistId, key }) =>
JSON.parse(
localStorage.getItem('xtream-collection-items') ?? '{}'
)[playlistId]?.[key],
{ playlistId, key: `movie:${movie.stream_id}` }
)
).toMatchObject({
title: favorite.snapshot.title,
poster_url: favorite.snapshot.posterUrl,
tmdb_id: 949,
release_year: 1995,
original_title: 'Saved original title',
added_at: favorite.addedAt,
position: 3,
});
await page.goto('/workspace/settings/backup');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export', exact: true }).click();
const downloaded = await (await downloadPromise).path();
if (!downloaded) throw new Error('Backup download is missing');
const backup = JSON.parse(await readFile(downloaded, 'utf8'));
expect(
backup.playlists.find(
(entry: { exportedId: string }) =>
entry.exportedId === playlistId
).userState.favorites
).toEqual([favorite]);
});
});
}
test('@xtream @collections PWA retries retain saved favorite metadata when snapshot storage is full', async ({
page,