mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
fix(epg): remove cached XMLTV data after source deletion
This commit is contained in:
1 parent
b55a3cc322
commit
ffcbee8140
32 files changed
+1051
-82
No files matched your search
@@ -65,7 +65,7 @@ function formatXmltvDate(date: Date): string {
|
||||
}
|
||||
|
||||
test.describe('Electron EPG', () => {
|
||||
test('@epg @electron adds an EPG source, fetches guide data, removes the source row, and clears stored EPG data', async ({
|
||||
test('@epg @electron adds an EPG source, fetches guide data, removes its stored EPG data on save', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const epgServer = await createMutableTextServer(epgFixtureXml, {
|
||||
@@ -103,6 +103,8 @@ test.describe('Electron EPG', () => {
|
||||
.first()
|
||||
).toBeVisible();
|
||||
|
||||
await saveSettings(app.mainWindow);
|
||||
|
||||
await app.mainWindow
|
||||
.locator('.epg-source-row button')
|
||||
.nth(1)
|
||||
@@ -116,18 +118,6 @@ test.describe('Electron EPG', () => {
|
||||
// would block the close and leak the Electron process.
|
||||
await saveSettings(app.mainWindow);
|
||||
|
||||
await app.mainWindow
|
||||
.getByRole('button', { name: 'Clear EPG data' })
|
||||
.click();
|
||||
const dialog = app.mainWindow.locator('mat-dialog-container');
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog
|
||||
.getByRole('button', { name: 'Yes', exact: true })
|
||||
.click();
|
||||
await app.mainWindow.waitForSelector('mat-dialog-container', {
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(() => getEpgChannelCount(app.mainWindow), {
|
||||
timeout: 20000,
|
||||
@@ -139,9 +129,99 @@ test.describe('Electron EPG', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('@epg @electron removes only the saved source, preserves shared IDs and mappings, and repairs old orphan data on restart', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
test.setTimeout(120000);
|
||||
const first = await createMutableTextServer(
|
||||
createCurrentXmltvFixture(
|
||||
'shared-news',
|
||||
'Shared News',
|
||||
'Removed Bulletin'
|
||||
),
|
||||
{ contentType: 'application/xml', resourcePath: '/first.xml' }
|
||||
);
|
||||
const second = await createMutableTextServer(
|
||||
createCurrentXmltvFixture(
|
||||
'shared-news',
|
||||
'Shared News',
|
||||
'Retained Bulletin'
|
||||
),
|
||||
{ contentType: 'application/xml', resourcePath: '/second.xml' }
|
||||
);
|
||||
let app = await launchElectronApp(dataDir);
|
||||
try {
|
||||
await openSettings(app.mainWindow);
|
||||
await openSettingsSection(app.mainWindow, 'epg');
|
||||
for (const url of [first.resourceUrl, second.resourceUrl]) {
|
||||
await app.mainWindow
|
||||
.getByRole('button', { name: 'Add EPG source' })
|
||||
.click();
|
||||
await app.mainWindow
|
||||
.locator('.epg-source-row input')
|
||||
.last()
|
||||
.fill(url);
|
||||
}
|
||||
await saveSettings(app.mainWindow);
|
||||
const programs = () =>
|
||||
app.mainWindow.evaluate(async () =>
|
||||
(
|
||||
await window.electron.getChannelPrograms('mapped-news')
|
||||
).map((p) => p.title)
|
||||
);
|
||||
await app.mainWindow.evaluate(() =>
|
||||
window.electron.setEpgMapping('mapped-news', 'shared-news')
|
||||
);
|
||||
await expect
|
||||
.poll(programs, { timeout: 30000 })
|
||||
.toEqual(['Removed Bulletin', 'Retained Bulletin']);
|
||||
await app.mainWindow
|
||||
.locator('.epg-source-row')
|
||||
.first()
|
||||
.locator('button')
|
||||
.nth(1)
|
||||
.click();
|
||||
// A staged removal must not delete data before Save.
|
||||
expect(await programs()).toContain('Removed Bulletin');
|
||||
await saveSettings(app.mainWindow);
|
||||
await expect.poll(programs).toEqual(['Retained Bulletin']);
|
||||
await closeElectronApp(app);
|
||||
app = await launchElectronApp(dataDir);
|
||||
await expect.poll(programs).toEqual(['Retained Bulletin']);
|
||||
// Simulate a cache left by 0.23: import a source absent from settings.
|
||||
await app.mainWindow.evaluate(
|
||||
(url) => window.electron.forceFetchEpg(url),
|
||||
first.resourceUrl
|
||||
);
|
||||
await expect.poll(programs).toContain('Removed Bulletin');
|
||||
await closeElectronApp(app);
|
||||
app = await launchElectronApp(dataDir);
|
||||
await expect.poll(programs).toEqual(['Retained Bulletin']);
|
||||
await openSettings(app.mainWindow);
|
||||
await openSettingsSection(app.mainWindow, 'epg');
|
||||
await app.mainWindow
|
||||
.locator('.epg-source-row button')
|
||||
.nth(1)
|
||||
.click();
|
||||
await saveSettings(app.mainWindow);
|
||||
await expect.poll(programs).toEqual([]);
|
||||
await expect.poll(() => getEpgChannelCount(app.mainWindow)).toBe(0);
|
||||
expect(
|
||||
await app.mainWindow.evaluate(() =>
|
||||
window.electron.getEpgMapping('mapped-news')
|
||||
)
|
||||
).toMatchObject({ epgChannelId: 'shared-news' });
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
await first.close();
|
||||
await second.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('@epg @electron imports and renders an EPG source declared by an M3U playlist header', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
test.setTimeout(90000);
|
||||
const epgServer = await createMutableTextServer(
|
||||
createCurrentXmltvFixture(
|
||||
'playlist-guide-news',
|
||||
@@ -212,6 +292,27 @@ test.describe('Electron EPG', () => {
|
||||
'Playlist Scoped Bulletin',
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await openSettings(app.mainWindow);
|
||||
await openSettingsSection(app.mainWindow, 'epg');
|
||||
await app.mainWindow
|
||||
.getByRole('button', { name: 'Add EPG source' })
|
||||
.click();
|
||||
await app.mainWindow
|
||||
.locator('.epg-source-row input')
|
||||
.fill(epgServer.resourceUrl);
|
||||
await saveSettings(app.mainWindow);
|
||||
await app.mainWindow
|
||||
.locator('.epg-source-row button')
|
||||
.nth(1)
|
||||
.click();
|
||||
await saveSettings(app.mainWindow);
|
||||
// The same URL still belongs to the saved M3U playlist.
|
||||
const retained = await app.mainWindow.evaluate(async () =>
|
||||
window.electron.getChannelPrograms('playlist-guide-news')
|
||||
);
|
||||
expect(retained.map((program) => program.title)).toContain(
|
||||
'Playlist Scoped Bulletin'
|
||||
);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
await playlistServer.close();
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
channelItemByTitle,
|
||||
clickCategoryByNameExact,
|
||||
closeElectronApp,
|
||||
createMutableTextServer,
|
||||
expect,
|
||||
goToDashboard,
|
||||
launchElectronApp,
|
||||
@@ -28,6 +29,93 @@ const epgCredentials = {
|
||||
password: 'epg',
|
||||
};
|
||||
|
||||
test('@epg @xtream @electron removes uploaded guide data and restores provider EPG without restarting', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
test.setTimeout(120000);
|
||||
await resetMockServers(request, ['xtream']);
|
||||
const fixture = await fetchXtreamEpgFixture(request, epgCredentials);
|
||||
const id = fixture.stream.epg_channel_id;
|
||||
expect(id).toBeTruthy();
|
||||
const stamp = (date: Date) =>
|
||||
date.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
||||
const xml = `<tv><channel id="${id}"><display-name>Uploaded Guide</display-name></channel>
|
||||
<programme channel="${id}" start="${stamp(new Date(Date.now() - 600000))} +0000" stop="${stamp(new Date(Date.now() + 3600000))} +0000"><title>Temporary XMLTV Bulletin</title></programme></tv>`;
|
||||
const source = await createMutableTextServer(xml, {
|
||||
contentType: 'application/xml',
|
||||
resourcePath: '/temporary.xml',
|
||||
});
|
||||
const app = await launchElectronApp(dataDir);
|
||||
try {
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
name: epgPortalName,
|
||||
...epgCredentials,
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
await openSettings(app.mainWindow);
|
||||
await openSettingsSection(app.mainWindow, 'epg');
|
||||
await app.mainWindow
|
||||
.getByRole('button', { name: 'Add EPG source' })
|
||||
.click();
|
||||
await app.mainWindow
|
||||
.locator('.epg-source-row input')
|
||||
.fill(source.resourceUrl);
|
||||
await app.mainWindow
|
||||
.getByTestId('toggle-prefer-uploaded-epg')
|
||||
.locator('input')
|
||||
.check();
|
||||
await saveSettings(app.mainWindow);
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
app.mainWindow.evaluate(
|
||||
async (channelId) =>
|
||||
(
|
||||
await window.electron.getChannelPrograms(
|
||||
channelId!
|
||||
)
|
||||
).map((p) => p.title),
|
||||
id
|
||||
),
|
||||
{ timeout: 30000 }
|
||||
)
|
||||
.toContain('Temporary XMLTV Bulletin');
|
||||
await openWorkspaceSection(app.mainWindow, 'Live TV');
|
||||
await clickCategoryByNameExact(app.mainWindow, fixture.categoryName);
|
||||
const row = channelItemByTitle(
|
||||
app.mainWindow,
|
||||
fixture.stream.name ?? ''
|
||||
).first();
|
||||
await expect(row.locator('.epg-title')).toHaveText(
|
||||
'Temporary XMLTV Bulletin'
|
||||
);
|
||||
await row.click();
|
||||
await expect
|
||||
.poll(() => timelineBlockTitles(app.mainWindow))
|
||||
.toContain('Temporary XMLTV Bulletin');
|
||||
await openSettings(app.mainWindow);
|
||||
await openSettingsSection(app.mainWindow, 'epg');
|
||||
await app.mainWindow.locator('.epg-source-row button').nth(1).click();
|
||||
await saveSettings(app.mainWindow);
|
||||
await openWorkspaceSection(app.mainWindow, 'Live TV');
|
||||
await clickCategoryByNameExact(app.mainWindow, fixture.categoryName);
|
||||
await expect(row.locator('.epg-title')).toHaveText(
|
||||
fixture.shortEpg[0].title
|
||||
);
|
||||
await row.click();
|
||||
await expect
|
||||
.poll(() => timelineBlockTitles(app.mainWindow))
|
||||
.not.toContain('Temporary XMLTV Bulletin');
|
||||
await expect
|
||||
.poll(() => timelineBlockTitles(app.mainWindow))
|
||||
.toContain(fixture.fullEpg[0].title);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
await source.close();
|
||||
}
|
||||
});
|
||||
|
||||
for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
|
||||
test(`@epg @xtream @electron renders Xtream EPG previews and the timeline schedule in ${timeZone}`, async ({
|
||||
dataDir,
|
||||
@@ -66,9 +154,7 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
|
||||
|
||||
// Provider-declared catch-up (tv_archive=1 in the fixture) is
|
||||
// surfaced as a badge on the sidebar row (#1128).
|
||||
await expect(
|
||||
channelRow.getByTestId('catchup-badge')
|
||||
).toBeVisible();
|
||||
await expect(channelRow.getByTestId('catchup-badge')).toBeVisible();
|
||||
|
||||
// Sidebar channel list shows the per-channel "now" programme line.
|
||||
await expect
|
||||
@@ -263,10 +349,7 @@ test('@radio @stalker @electron keeps radio rows compact at narrow widths', asyn
|
||||
await expect(categoryButton).toBeVisible();
|
||||
await categoryButton.click();
|
||||
|
||||
const radioRow = channelItemByTitle(
|
||||
app.mainWindow,
|
||||
firstTitle
|
||||
).first();
|
||||
const radioRow = channelItemByTitle(app.mainWindow, firstTitle).first();
|
||||
await expect(radioRow).toBeVisible({ timeout: 20000 });
|
||||
await expect(radioRow).toHaveClass(/compact/);
|
||||
await expect(radioRow.locator('.epg-placeholder')).toHaveCount(0);
|
||||
@@ -301,7 +384,8 @@ test('@epg @xtream @electron shifts the sidebar preview and the timeline "now" b
|
||||
// The guide runs an hour ahead of the real schedule, so the programme
|
||||
// actually on air is the one the provider files under "an hour ago" —
|
||||
// and every surface must agree on that same programme.
|
||||
const providerNowSeconds = Math.floor(Date.now() / 1000) - offsetMinutes * 60;
|
||||
const providerNowSeconds =
|
||||
Math.floor(Date.now() / 1000) - offsetMinutes * 60;
|
||||
const expectedProgram = fixture.fullEpg.find(
|
||||
(listing) =>
|
||||
listing.startTimestamp <= providerNowSeconds &&
|
||||
@@ -341,8 +425,7 @@ test('@epg @xtream @electron shifts the sidebar preview and the timeline "now" b
|
||||
await expect
|
||||
.poll(async () =>
|
||||
(
|
||||
(await channelRow.locator('.epg-title').textContent()) ??
|
||||
''
|
||||
(await channelRow.locator('.epg-title').textContent()) ?? ''
|
||||
).trim()
|
||||
)
|
||||
.toBe(expectedProgram.title);
|
||||
|
||||
Reference in new issue
Block a user