mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
fix(xtream): confirm browser catalog identity before updating favorites
This commit is contained in:
1 parent
2d975e0b29
commit
8148f334e6
6 files changed
+302
-105
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user