mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
feat(portals): find the same movie in your other playlists
A movie that exists in several imported Xtream playlists now shows a "Sources N" chip on its detail page and in the player. Switching playlist mid-film keeps the timecode, a preferred source can be pinned per movie, and a failed stream offers the alternatives instead of a dead end. The governing rule is that a guess is never presented as a fact. Every metadata value carries where it came from — `api` (the provider said so), `parsed` (inferred from the title) or `probe` (we contacted the stream). Facts render as plain tags, guesses are prefixed `~` in a warning colour, and an unknown value renders no tag at all plus a "check" affordance. Ranking and failover read through `factualOnly()`, so a filename claiming 4K is structurally unable to outrank a source that was actually reached. A probe that could not complete reports "unknown", never "unavailable". Scope is deliberately narrow: Xtream to Xtream, movies only, Electron only. Stalker never reaches the `content` table and M3U is a JSON blob whose search forces live content; both are additive later, since the candidate type already carries all three portal kinds. In the PWA every entry point is gated off and the chip renders nothing. Auto-failover is opt-in and off by default. Each source is tried at most once per session, so it terminates structurally, and the switch is never silent — the toast names the new playlist, offers an undo, and warns that the dub may differ only when both sides state an audio track as fact. Notable details: - Playlist names are routinely the pasted URL, credentials included. They are never rendered raw; a short host-only label is derived instead. - Quality is derived from pixel width, not height: a 2.39:1 1080p master is 1920x800, and bucketing that by height would publish "720p" as a fact. - Switching is a single `inlinePlayback.set()` so the player and engine survive and re-seek; the carried position is read before the 15s persistence throttle so it does not rewind. - Sources from one playlist collapse into a group, since the same film often appears there several times under different stream ids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2300bea11
commit
004cb65fe6
101 files changed
+7183
-192
No files matched your search
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
addXtreamPortal,
|
||||
clickCategoryByNameExact,
|
||||
clickGridListCardByTitle,
|
||||
closeElectronApp,
|
||||
expect,
|
||||
launchElectronApp,
|
||||
resetMockServers,
|
||||
test,
|
||||
waitForXtreamWorkspaceReady,
|
||||
} from './electron-test-fixtures';
|
||||
import {
|
||||
fetchXtreamVodFixture,
|
||||
getXtreamTitle,
|
||||
pickDistinctTitles,
|
||||
} from './portal-mock-fixtures';
|
||||
|
||||
/**
|
||||
* `multisrc1` and `multisrc2` are two distinct credential pairs that share one
|
||||
* faker seed, so both portals generate an IDENTICAL catalog. That overlap is
|
||||
* the whole premise of this feature: the same movie in two playlists.
|
||||
*/
|
||||
const portalA = { username: 'multisrc1', password: 'multisrc1' };
|
||||
const portalB = { username: 'multisrc2', password: 'multisrc2' };
|
||||
|
||||
test.describe('VOD multi-source', () => {
|
||||
test('offers the same movie from another playlist and switches to it', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
await resetMockServers(request, ['xtream']);
|
||||
const vodFixture = await fetchXtreamVodFixture(request, portalA);
|
||||
const [movieTitle] = pickDistinctTitles(
|
||||
vodFixture.items,
|
||||
getXtreamTitle
|
||||
);
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
...portalA,
|
||||
name: 'Multi Source A',
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
|
||||
// The second portal is what makes an alternative exist at all.
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
...portalB,
|
||||
name: 'Multi Source B',
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
|
||||
await app.mainWindow
|
||||
.getByRole('link', { name: 'Movies', exact: true })
|
||||
.click();
|
||||
await clickCategoryByNameExact(
|
||||
app.mainWindow,
|
||||
vodFixture.categoryName
|
||||
);
|
||||
await clickGridListCardByTitle(app.mainWindow, movieTitle);
|
||||
await app.mainWindow.waitForURL(
|
||||
/\/workspace\/xtreams\/[^/]+\/vod\/[^/]+\/[^/]+$/
|
||||
);
|
||||
|
||||
// Discovery is lazy, so the chip appears after the detail view.
|
||||
const chip = app.mainWindow.locator('app-vod-sources-chip button');
|
||||
await expect(chip).toBeVisible({ timeout: 20000 });
|
||||
// Exactly one alternative: the same film in the other playlist.
|
||||
await expect(chip).toContainText('1');
|
||||
|
||||
await chip.click();
|
||||
|
||||
// Both the playing source and the alternative are listed.
|
||||
const rows = app.mainWindow.locator('app-vod-source-row');
|
||||
await expect(rows).toHaveCount(2);
|
||||
// Portal B was added last and is active, so A is the alternative.
|
||||
await expect(
|
||||
rows.filter({ hasText: 'Multi Source A' })
|
||||
).toHaveCount(1);
|
||||
// The playing row must name its playlist, never a raw UUID.
|
||||
await expect(
|
||||
rows.filter({ hasText: 'Multi Source B' })
|
||||
).toHaveCount(1);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('shows no sources chip when only one playlist has the movie', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
await resetMockServers(request, ['xtream']);
|
||||
const vodFixture = await fetchXtreamVodFixture(request, portalA);
|
||||
const [movieTitle] = pickDistinctTitles(
|
||||
vodFixture.items,
|
||||
getXtreamTitle
|
||||
);
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
...portalA,
|
||||
name: 'Multi Source A',
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
|
||||
await app.mainWindow
|
||||
.getByRole('link', { name: 'Movies', exact: true })
|
||||
.click();
|
||||
await clickCategoryByNameExact(
|
||||
app.mainWindow,
|
||||
vodFixture.categoryName
|
||||
);
|
||||
await clickGridListCardByTitle(app.mainWindow, movieTitle);
|
||||
await app.mainWindow.waitForURL(
|
||||
/\/workspace\/xtreams\/[^/]+\/vod\/[^/]+\/[^/]+$/
|
||||
);
|
||||
|
||||
// The action row must look exactly as it did before this feature.
|
||||
await expect(
|
||||
app.mainWindow.locator('.play-btn').first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
await expect(
|
||||
app.mainWindow.locator('app-vod-sources-chip')
|
||||
).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user