chore: resolve master conflicts for embedded mpv branch

Entire-Checkpoint: 5b514fe72836
This commit is contained in:
4gray committed 2026-05-02 02:19:54 +02:00
commit ec2f6a702a
290 files changed
+12183 -4142

No files matched your search

@@ -1,6 +1,7 @@
import { Page } from '@playwright/test';
import {
addXtreamPortal,
channelItemByTitle,
clickCategoryByNameExact,
clickFirstGridListCard,
closeElectronApp,
@@ -94,8 +95,15 @@ test.describe('Dashboard Activation', () => {
/\/workspace\/xtreams\/[^/]+\/favorites$/
);
await expect(
app.mainWindow.locator('.player-toolbar .close-btn').first()
app.mainWindow
.locator(
'app-unified-live-tab .content-container .video-player'
)
.first()
).toBeVisible({ timeout: 20000 });
await expect(
channelItemByTitle(app.mainWindow, liveTitle).first()
).toHaveClass(/(^|\s)active(\s|$)/);
await app.mainWindow.goBack();
await app.mainWindow.waitForURL(/\/workspace\/dashboard$/);
+37 -23
View File
@@ -73,21 +73,23 @@ test.describe('Electron Recently Viewed', () => {
app.mainWindow,
'Stable Recent Channel'
).first();
const closeButton = app.mainWindow
.locator('.player-toolbar .close-btn')
const livePlayer = app.mainWindow
.locator('app-unified-live-tab .content-container .video-player')
.first();
await item.click();
await expect(closeButton).toBeVisible({ timeout: 20000 });
await expect(livePlayer).toBeVisible({ timeout: 20000 });
await expect(item).toHaveClass(/(^|\s)active(\s|$)/);
await item.click();
await expect(closeButton).toBeVisible({ timeout: 20000 });
await expect(livePlayer).toBeVisible({ timeout: 20000 });
await expect(item).toHaveClass(/(^|\s)active(\s|$)/);
} finally {
await closeElectronApp(app);
}
});
test('tracks M3U recent channels in newest-first order, supports all-playlists scope, and persists removals after restart', async ({
test('tracks M3U recent channels in newest-first order, supports all-playlists scope, and persists favorites after restart', async ({
dataDir,
}) => {
const playlistTitle = 'm3u-recent-source.m3u';
@@ -127,10 +129,15 @@ test.describe('Electron Recently Viewed', () => {
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Recent Channel Two', 'Recent Channel One']);
await removeLiveRecentItem(app.mainWindow, 'Recent Channel Two');
await toggleFavoriteForChannel(app.mainWindow, 'Recent Channel Two');
await expect
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Recent Channel One']);
.toEqual(['Recent Channel Two', 'Recent Channel One']);
await openPlaylistFavorites(app.mainWindow);
await expect(
channelItemByTitle(app.mainWindow, 'Recent Channel Two').first()
).toBeVisible({ timeout: 20000 });
const restarted = await restartElectronApp(app, dataDir);
app.electronApp = restarted.electronApp;
@@ -145,7 +152,12 @@ test.describe('Electron Recently Viewed', () => {
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
await expect
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Recent Channel One']);
.toEqual(['Recent Channel Two', 'Recent Channel One']);
await openPlaylistFavorites(app.mainWindow);
await expect(
channelItemByTitle(app.mainWindow, 'Recent Channel Two').first()
).toBeVisible({ timeout: 20000 });
} finally {
await closeElectronApp(app);
}
@@ -189,7 +201,7 @@ test.describe('Electron Recently Viewed', () => {
await toggleFavoriteForChannel(app.mainWindow, liveTitle);
await openPlaylistFavorites(app.mainWindow);
await channelItemByTitle(app.mainWindow, liveTitle).first().click();
await closeUnifiedLiveDetail(app.mainWindow);
await expectUnifiedLiveDetailOpen(app.mainWindow, liveTitle);
await app.mainWindow
.getByRole('link', { name: 'Movies', exact: true })
@@ -396,7 +408,7 @@ test.describe('Electron Recently Viewed', () => {
await toggleFavoriteForChannel(app.mainWindow, liveTitle);
await openPlaylistFavorites(app.mainWindow);
await channelItemByTitle(app.mainWindow, liveTitle).first().click();
await closeUnifiedLiveDetail(app.mainWindow);
await expectUnifiedLiveDetailOpen(app.mainWindow, liveTitle);
await app.mainWindow
.getByRole('link', { name: 'Movies', exact: true })
@@ -561,11 +573,18 @@ async function clearRecentItems(page: Page, typeLabel: string): Promise<void> {
await page.getByRole('button', { name: 'Yes' }).click();
}
async function closeUnifiedLiveDetail(page: Page): Promise<void> {
const closeButton = page.locator('.player-toolbar .close-btn').first();
await expect(closeButton).toBeVisible({ timeout: 20000 });
await closeButton.click();
async function expectUnifiedLiveDetailOpen(
page: Page,
title: string
): Promise<void> {
await expect(
page
.locator('app-unified-live-tab .content-container .video-player')
.first()
).toBeVisible({ timeout: 20000 });
await expect(channelItemByTitle(page, title).first()).toHaveClass(
/(^|\s)active(\s|$)/
);
}
async function goBackFromDetail(page: Page): Promise<void> {
@@ -634,14 +653,6 @@ async function playFirstSeriesEpisode(page: Page): Promise<void> {
await episodeCard.click();
}
async function removeLiveRecentItem(page: Page, title: string): Promise<void> {
const item = channelItemByTitle(page, title).first();
await expect(item).toBeVisible({ timeout: 20000 });
await item.hover();
await item.locator('.favorite-button').first().click();
}
async function toggleFavoriteForChannel(
page: Page,
title: string
@@ -651,6 +662,9 @@ async function toggleFavoriteForChannel(
await expect(item).toBeVisible({ timeout: 20000 });
await item.hover();
await item.locator('.favorite-button').first().click();
await expect(item.locator('.favorite-button mat-icon').first()).toHaveText(
'star'
);
}
async function visibleLiveTitles(page: Page): Promise<string[]> {
+74 -33
View File
@@ -31,7 +31,9 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
const fixture = await fetchXtreamEpgFixture(request, epgCredentials);
const currentProgram = fixture.shortEpg[0];
if (!currentProgram) {
throw new Error('Expected the Xtream EPG fixture to include a current program.');
throw new Error(
'Expected the Xtream EPG fixture to include a current program.'
);
}
const app = await launchElectronApp(dataDir, {
env: { TZ: timeZone },
@@ -45,7 +47,10 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
});
await waitForXtreamWorkspaceReady(app.mainWindow);
await openWorkspaceSection(app.mainWindow, 'Live TV');
await clickCategoryByNameExact(app.mainWindow, fixture.categoryName);
await clickCategoryByNameExact(
app.mainWindow,
fixture.categoryName
);
const channelRow = channelItemByTitle(
app.mainWindow,
@@ -55,14 +60,18 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
await expect
.poll(async () =>
((await channelRow.locator('.epg-title').textContent()) ?? '').trim()
(
(await channelRow
.locator('.epg-title')
.textContent()) ?? ''
).trim()
)
.toBe(currentProgram.title);
await expect
.poll(async () => {
return (await channelRow.locator('.epg-time').allInnerTexts()).map(
(value) => value.trim()
);
return (
await channelRow.locator('.epg-time').allInnerTexts()
).map((value) => value.trim());
})
.toEqual([
formatTimeInZone(currentProgram.startTimestamp, timeZone),
@@ -84,22 +93,14 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
timeZone
);
await expect.poll(() => visibleProgramTitles(app.mainWindow)).toEqual(
currentDayTitles
);
await expect
.poll(() => visibleProgramTitles(app.mainWindow))
.toEqual(currentDayTitles);
const currentProgramRow = app.mainWindow
.locator('app-epg-list .program-item.current-program')
.first();
await expect(currentProgramRow).toBeVisible();
await expect(currentProgramRow.locator('.program-title')).toHaveText(
currentProgram.title
);
await expect(currentProgramRow.locator('.time')).toHaveText(
`${formatTimeInZone(
currentProgram.startTimestamp,
timeZone
)} - ${formatTimeInZone(currentProgram.stopTimestamp, timeZone)}`
await expectCurrentProgram(
app.mainWindow,
currentProgram,
timeZone
);
const firstFutureDateKey = getFirstFutureDateKey(
@@ -114,25 +115,29 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
firstFutureDateKey!,
timeZone
);
const nextDayClicks = dayDifference(currentDateKey, firstFutureDateKey!);
const nextDayClicks = dayDifference(
currentDateKey,
firstFutureDateKey!
);
for (let index = 0; index < nextDayClicks; index += 1) {
await app.mainWindow.locator('app-epg-list .next-day').click();
await app.mainWindow
.locator(
'app-live-epg-panel .next-day, app-epg-list .next-day'
)
.click();
}
await expect.poll(() => visibleProgramTitles(app.mainWindow)).toEqual(
futureDayTitles
);
await expect
.poll(() => visibleProgramTitles(app.mainWindow))
.toEqual(futureDayTitles);
} finally {
await closeElectronApp(app);
}
});
}
function formatTimeInZone(
timestampSeconds: number,
timeZone: string
): string {
function formatTimeInZone(timestampSeconds: number, timeZone: string): string {
return new Intl.DateTimeFormat('en-US', {
timeZone,
hour: '2-digit',
@@ -157,7 +162,9 @@ function formatDateKeyInZone(
const day = parts.find((part) => part.type === 'day')?.value;
if (!year || !month || !day) {
throw new Error(`Failed to format a date key for timezone ${timeZone}.`);
throw new Error(
`Failed to format a date key for timezone ${timeZone}.`
);
}
return `${year}-${month}-${day}`;
@@ -215,14 +222,48 @@ function dayDifference(fromDateKey: string, toDateKey: string): number {
return Math.round((to - from) / (24 * 60 * 60 * 1000));
}
async function visibleProgramTitles(page: Parameters<typeof channelItemByTitle>[0]) {
async function visibleProgramTitles(
page: Parameters<typeof channelItemByTitle>[0]
) {
return page
.locator('app-epg-list .program-title')
.allInnerTexts()
.then((titles) => titles.map((title) => title.trim()).filter(Boolean));
}
async function getProgressWidthPercent(row: ReturnType<typeof channelItemByTitle>) {
async function expectCurrentProgram(
page: Parameters<typeof channelItemByTitle>[0],
currentProgram: XtreamNormalizedEpgListing,
timeZone: string
): Promise<void> {
const startTime = formatTimeInZone(currentProgram.startTimestamp, timeZone);
const stopTime = formatTimeInZone(currentProgram.stopTimestamp, timeZone);
const currentProgramRow = page
.locator('app-epg-list .program-item.current-program')
.first();
if (await currentProgramRow.isVisible().catch(() => false)) {
await expect(currentProgramRow.locator('.program-title')).toHaveText(
currentProgram.title
);
await expect(currentProgramRow.locator('.time')).toHaveText(
`${startTime}–${stopTime}`
);
return;
}
const summary = page.locator('app-live-epg-panel').first();
await expect(summary.locator('.live-epg-panel__title')).toHaveText(
currentProgram.title
);
await expect(summary.locator('.live-epg-panel__time')).toHaveText(
`${startTime} - ${stopTime}`
);
}
async function getProgressWidthPercent(
row: ReturnType<typeof channelItemByTitle>
) {
const style = await row.locator('.epg-progress-fill').getAttribute('style');
const width = style?.match(/width:\s*([\d.]+)%/)?.[1] ?? '0';
return Number.parseFloat(width);