mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(website): add the M3U playlist and EPG setup guide
Publish "How to Load an M3U Playlist and Add an EPG in IPTVnator": the three import methods plus drag-and-drop and OS file opening, the playlist views, refresh and startup auto-update, attaching an XMLTV guide through Settings or a url-tvg header, the tvg-id / tvg-name / name matching order with manual mapping, catch-up attributes, troubleshooting and a seven-question FAQ. The three guides now link to each other, the download pages point at all three, and llms.txt lists the new one. Three guide shots join the manifest: the M3U URL dialog, the Groups view (reusing open-m3u-groups under the guides group) and the EPG settings section with a staged source row. A settings shot leaves the form dirty, which arms the app's close guard and blocked app.close() indefinitely; the capture now discards unsaved settings before every action and before teardown, and bounds every locator wait. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
fe3c86394c
commit
50b980af7a
21 files changed
+335
-13
No files matched your search
@@ -66,8 +66,8 @@ without depending on a specific version.
|
||||
## Guides
|
||||
|
||||
Evergreen how-to posts live in the blog collection next to release notes
|
||||
(`xtream-codes-setup-guide.mdx` and `stalker-portal-setup-guide.mdx` in
|
||||
`apps/website/src/content/blog/`).
|
||||
(`xtream-codes-setup-guide.mdx`, `stalker-portal-setup-guide.mdx` and
|
||||
`m3u-playlist-epg-setup-guide.mdx` in `apps/website/src/content/blog/`).
|
||||
Two conventions set them apart:
|
||||
|
||||
- **`faq` frontmatter.** An optional list of `{ q, a }` entries. `BlogPost.astro`
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 431 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -13,6 +13,7 @@
|
||||
- Download for Linux: https://4gray.github.io/iptvnator/download/linux/
|
||||
- Guide, Xtream Codes setup: https://4gray.github.io/iptvnator/blog/xtream-codes-setup-guide/
|
||||
- Guide, Stalker/Ministra portal setup: https://4gray.github.io/iptvnator/blog/stalker-portal-setup-guide/
|
||||
- Guide, M3U playlist and EPG setup: https://4gray.github.io/iptvnator/blog/m3u-playlist-epg-setup-guide/
|
||||
- Release Notes (v0.18): https://4gray.github.io/iptvnator/blog/v0-18-release-notes/
|
||||
- Troubleshooting (macOS app damaged fix): https://4gray.github.io/iptvnator/blog/macos-arm-app-damaged-fix/
|
||||
- Source Code: https://github.com/4gray/iptvnator
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: How to Load an M3U Playlist and Add an EPG in IPTVnator
|
||||
description: Import an M3U or M3U8 playlist into IPTVnator from a URL, a file or pasted text, keep it updated, and attach an XMLTV program guide. Covers channel groups, catch-up attributes, EPG matching by tvg-id, and the usual reasons a playlist or guide does not load.
|
||||
pubDate: 2026-09-04
|
||||
author: 4gray
|
||||
heroImage: /iptvnator/blog/guides/screenshots/guide-m3u-live-groups-dark.png
|
||||
tags:
|
||||
- guide
|
||||
- m3u
|
||||
- epg
|
||||
- setup
|
||||
- tutorial
|
||||
draft: false
|
||||
faq:
|
||||
- q: What is the difference between an M3U and an M3U8 playlist?
|
||||
a: Nothing that matters to IPTVnator. Both are the same text format; M3U8 only signals that the file is UTF-8 encoded. Both extensions are accepted from a URL, a file or pasted text, and both can carry the same attributes for logos, groups, program guide ids and catch-up.
|
||||
- q: Does IPTVnator refresh a playlist that was added from a URL?
|
||||
a: On request always, and automatically on every start when you enable Auto-update in the playlist details. The desktop app downloads the file again, keeps your favorites and settings, and reports what changed. A playlist imported from a local file is refreshed from that file.
|
||||
- q: Why do my channels show no program information?
|
||||
a: The channel is not matched to a guide entry. IPTVnator matches by tvg-id first, then by tvg-name, then by the channel name. Check that the playlist carries tvg-id values that exist in the XMLTV source, or map the channel by hand with a right-click and "Map EPG channel". The program guide is a desktop feature; the browser version does not load XMLTV.
|
||||
- q: Do I have to add the EPG URL myself?
|
||||
a: Not when the playlist declares it. Many providers put url-tvg or x-tvg-url in the first line of the file, and IPTVnator imports those sources automatically and lists them in the playlist details. Otherwise add the URL under Settings › EPG or in the playlist details.
|
||||
- q: Does catch-up work with M3U playlists?
|
||||
a: Yes, when the provider marks channels with catch-up attributes such as catchup, catchup-source and catchup-days, or with tvg-rec and timeshift. Archived programs can then be started from the timeline under the player. Without those attributes the timeline shows the schedule only.
|
||||
- q: Can I open a playlist file by double-clicking it?
|
||||
a: Yes. The desktop app registers the .m3u and .m3u8 extensions, so opening a file from the file manager, dropping it anywhere onto the IPTVnator window, or passing it on the command line imports it like the dialog does.
|
||||
- q: Why does the browser version warn about a proxy when I paste a playlist URL?
|
||||
a: Browsers block cross-origin downloads, so the browser version fetches remote playlists through a proxy. The self-hosted Docker image ships its own backend for that. If the playlist URL contains credentials you would rather not route through a proxy, import the file instead.
|
||||
---
|
||||
|
||||
import Alert from '../../components/blog/Alert.astro';
|
||||
import PostButton from '../../components/blog/PostButton.astro';
|
||||
import StepRail from '../../components/blog/StepRail.astro';
|
||||
import LinkCards from '../../components/blog/LinkCards.astro';
|
||||
|
||||
An M3U playlist is the plainest way to get channels into IPTVnator: a text file that lists
|
||||
streams, one per line, with optional attributes for logos, groups and program guide ids.
|
||||
Providers hand it out as a link or as a file, and IPTVnator accepts both, plus the file's
|
||||
raw text. This guide walks through the import, the maintenance options, and how to attach
|
||||
an XMLTV program guide so channels show what is on.
|
||||
|
||||
It applies to the desktop app on Windows, macOS and Linux and, with the limits noted below,
|
||||
to the self-hosted browser version.
|
||||
|
||||
<Alert type="info" title="IPTVnator is a player, not a service">
|
||||
It does not include playlists or channels. The playlist link, file and guide URL in this
|
||||
guide come from the provider you already have a contract with.
|
||||
</Alert>
|
||||
|
||||
## Three ways to import
|
||||
|
||||
<StepRail
|
||||
title="In IPTVnator"
|
||||
steps={[
|
||||
'Open the Add playlist dialog from the dashboard, the header playlist switcher, or the command palette entry "Add M3U playlist".',
|
||||
'Choose "M3U URL" and paste the link your provider gave you, or choose "M3U file" and pick the file, or choose "Raw m3u text" and paste the playlist itself.',
|
||||
'Give the playlist a title. For a URL the title is optional; IPTVnator falls back to the file name.',
|
||||
'Click "Add playlist". The channels are parsed, grouped by their group-title attribute, and the playlist opens.',
|
||||
]}
|
||||
/>
|
||||
|
||||

|
||||
|
||||
Two shortcuts exist on the desktop app. Drop an `.m3u` or `.m3u8` file anywhere onto the
|
||||
IPTVnator window and it is imported on the spot. And because the app registers both file
|
||||
extensions, opening a playlist from the file manager or passing it on the command line
|
||||
imports it as well, even while IPTVnator is already running.
|
||||
|
||||
<Alert type="success" title="Playlist links with credentials">
|
||||
Many provider links end in `get.php` with a username and password in the query string.
|
||||
Such a link works as an M3U URL, but it is really an Xtream Codes account, and adding it
|
||||
as one gives you movies, series, provider metadata and catch-up on top of the channel
|
||||
list. The Auto-detect method recognizes the link and offers both.
|
||||
</Alert>
|
||||
|
||||
## Inside the playlist
|
||||
|
||||

|
||||
|
||||
The playlist page keeps the player on the right and a sidebar with four views:
|
||||
|
||||
- **All channels**, a virtualized list that stays fast with tens of thousands of entries,
|
||||
with search that filters as you type.
|
||||
- **Groups**, one expandable section per `group-title`. Groups you do not need can be
|
||||
hidden with **Manage groups** in the header of this view; the hidden set survives
|
||||
refreshes.
|
||||
- **Favorites**, reorderable by drag and drop.
|
||||
- **Recent**, the channels you watched last.
|
||||
|
||||
Under the player, the program guide runs as a horizontal timeline once a guide is attached,
|
||||
with a vertical single-day list as the alternative view in the settings. Channels marked
|
||||
with `radio="true"` open in a dedicated audio layout, and entries that look like movie
|
||||
files open in a movie detail view when TMDB metadata is enabled.
|
||||
|
||||
## Keep the playlist current
|
||||
|
||||
Open the playlist details from the playlist switcher in the header or from the source card
|
||||
on the dashboard. Besides the title you will find:
|
||||
|
||||
- **Refresh playlist**, which downloads the file again from its URL or re-reads it from
|
||||
disk while keeping your favorites, hidden groups and settings.
|
||||
- **Auto-update**, which does the same automatically on every start of the desktop app,
|
||||
with the result reported once the update is done.
|
||||
- **User agent**, for providers that only answer known player signatures.
|
||||
- **Export playlist as m3u**, the current channel list as a file.
|
||||
- **EPG sources**, the guide URLs used for this playlist, described next.
|
||||
|
||||
## Attach a program guide
|
||||
|
||||
<Alert type="warning" title="Desktop only">
|
||||
The program guide is downloaded, parsed and stored on your device by the desktop app. The
|
||||
browser version plays the channels but does not load XMLTV.
|
||||
</Alert>
|
||||
|
||||
IPTVnator reads guides in the **XMLTV** format, as a plain `.xml` file or compressed
|
||||
`.xml.gz`. Providers publish the URL next to the playlist, and many put it straight into
|
||||
the playlist's first line as `url-tvg` or `x-tvg-url`. In that case there is nothing to do:
|
||||
the sources are imported when the playlist is added, and the playlist details list them
|
||||
with a note on how many were picked. Very long provider lists are trimmed to the guides that
|
||||
match the playlist's country and language hints.
|
||||
|
||||
For everything else, add the URL yourself:
|
||||
|
||||
<StepRail
|
||||
title="Settings › EPG"
|
||||
steps={[
|
||||
'Open Settings from the bottom of the sidebar and choose the EPG section.',
|
||||
'Click "Add EPG source" and paste the URL of the .xml or .xml.gz file.',
|
||||
'Save. IPTVnator downloads and parses the guide in the background and shows a status chip next to the source: up to date, needs refresh, or failed.',
|
||||
'Open a channel. The current program appears in the channel list and the timeline under the player fills in.',
|
||||
]}
|
||||
/>
|
||||
|
||||

|
||||
|
||||
Sources added in the settings apply to every playlist. A source added in the playlist
|
||||
details applies to that playlist only and is kept across refreshes, as is a source you
|
||||
removed there, so a provider cannot silently re-enable it.
|
||||
|
||||
### How channels are matched
|
||||
|
||||
A guide entry is attached to a channel by comparing ids, in this order:
|
||||
|
||||
1. the playlist's `tvg-id` against the XMLTV channel id,
|
||||
2. the playlist's `tvg-name` against the XMLTV display name,
|
||||
3. the channel name itself.
|
||||
|
||||
If none match, the channel simply shows no program. The fix is either a playlist with proper
|
||||
`tvg-id` values, or a manual mapping: right-click the channel, choose **Map EPG channel**,
|
||||
and search the guide for the right entry. Mappings are stored on your device and survive
|
||||
playlist refreshes.
|
||||
|
||||
### Catch-up from the timeline
|
||||
|
||||
Providers that keep an archive mark channels with `catchup`, `catchup-source` and
|
||||
`catchup-days`, or with the older `tvg-rec` and `timeshift` attributes. IPTVnator reads
|
||||
them from the playlist, shows how many days of archive a channel has, and lets you start a
|
||||
past program from the timeline. Without those attributes the timeline is a schedule only;
|
||||
the guide alone cannot make a stream replayable.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Failed to fetch the playlist**: the URL is wrong or the host is down. The desktop app
|
||||
shows the HTTP status it received; 401 and 403 mean the link needs credentials or is
|
||||
restricted, 404 means it does not point to a playlist.
|
||||
- **Certificate for this playlist host is invalid**: the provider's TLS certificate is
|
||||
broken. IPTVnator offers to trust that one host; other hosts still need valid
|
||||
certificates.
|
||||
- **The playlist imported but has no groups**: the file carries no `group-title`
|
||||
attributes. Everything is in All channels, and Groups stays empty.
|
||||
- **A guide is attached but a channel shows nothing**: see the matching order above, then
|
||||
map the channel by hand. A guide that was fetched hours ago can also be refreshed from
|
||||
the settings.
|
||||
- **The guide fails to download**: check the URL in a browser first. Sources on a private
|
||||
or local network address ask for confirmation before IPTVnator connects to them.
|
||||
- **Some channels do not play**: the built-in web players cannot decode every codec. Open
|
||||
the stream in MPV or VLC via Settings › Playback, or enable the experimental embedded
|
||||
MPV engine. DASH streams with ClearKey keys declared through `#KODIPROP` lines play in
|
||||
the built-in player.
|
||||
|
||||
## Related
|
||||
|
||||
<LinkCards
|
||||
links={[
|
||||
{ label: 'Download IPTVnator', href: '/iptvnator/download/', hint: 'Windows, macOS and Linux builds, package managers and the browser version.', icon: 'download' },
|
||||
{ label: 'Understanding the program guide', href: '/iptvnator/blog/epg-guide/', hint: 'A closer look at how XMLTV data reaches your channels.', icon: 'docs' },
|
||||
{ label: 'Add an Xtream Codes account', href: '/iptvnator/blog/xtream-codes-setup-guide/', hint: 'When your provider link is really an Xtream login.', icon: 'docs' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<PostButton href="/iptvnator/download/" label="Get IPTVnator for your platform" external={false} />
|
||||
@@ -184,7 +184,7 @@ exactly those values.
|
||||
links={[
|
||||
{ label: 'Download IPTVnator', href: '/iptvnator/download/', hint: 'Windows, macOS and Linux builds, package managers and the browser version.', icon: 'download' },
|
||||
{ label: 'Add an Xtream Codes account', href: '/iptvnator/blog/xtream-codes-setup-guide/', hint: 'The username-and-password way of connecting a provider.', icon: 'docs' },
|
||||
{ label: 'Why some streams need an external player', href: '/iptvnator/blog/why-external-players-help/', hint: 'Codecs, containers and when MPV or VLC is the right tool.', icon: 'docs' },
|
||||
{ label: 'Load an M3U playlist and add an EPG', href: '/iptvnator/blog/m3u-playlist-epg-setup-guide/', hint: 'Plain playlists, refresh options and XMLTV guides.', icon: 'docs' },
|
||||
]}
|
||||
/>
|
||||
|
||||
|
||||
@@ -179,8 +179,8 @@ them there; the next request uses the new connection.
|
||||
<LinkCards
|
||||
links={[
|
||||
{ label: 'Download IPTVnator', href: '/iptvnator/download/', hint: 'Windows, macOS and Linux builds, package managers and the browser version.', icon: 'download' },
|
||||
{ label: 'Why some streams need an external player', href: '/iptvnator/blog/why-external-players-help/', hint: 'Codecs, containers and when MPV or VLC is the right tool.', icon: 'docs' },
|
||||
{ label: 'Understanding the program guide', href: '/iptvnator/blog/epg-guide/', hint: 'How XMLTV data reaches your channels.', icon: 'docs' },
|
||||
{ label: 'Connect a Stalker or Ministra portal', href: '/iptvnator/blog/stalker-portal-setup-guide/', hint: 'The MAC-address way of connecting a provider.', icon: 'docs' },
|
||||
{ label: 'Load an M3U playlist and add an EPG', href: '/iptvnator/blog/m3u-playlist-epg-setup-guide/', hint: 'Plain playlists, refresh options and XMLTV guides.', icon: 'docs' },
|
||||
]}
|
||||
/>
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ sudo emerge iptvnator-bin</TerminalBlock>
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="guides" eyebrow="Keep reading" title="Guides and" accent="background">
|
||||
<RelatedPosts slugs={['xtream-codes-setup-guide', 'stalker-portal-setup-guide', 'beware-unofficial-iptvnator-websites']} />
|
||||
<RelatedPosts slugs={['m3u-playlist-epg-setup-guide', 'xtream-codes-setup-guide', 'stalker-portal-setup-guide']} />
|
||||
<div class="mt-10">
|
||||
<OfficialSourcesNote />
|
||||
</div>
|
||||
|
||||
@@ -207,7 +207,7 @@ const jsonLd = buildDownloadPageSchema({
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="guides" eyebrow="Keep reading" title="Guides and" accent="background">
|
||||
<RelatedPosts slugs={['xtream-codes-setup-guide', 'stalker-portal-setup-guide', 'macos-arm-app-damaged-fix']} />
|
||||
<RelatedPosts slugs={['m3u-playlist-epg-setup-guide', 'xtream-codes-setup-guide', 'stalker-portal-setup-guide']} />
|
||||
<div class="mt-10">
|
||||
<OfficialSourcesNote />
|
||||
</div>
|
||||
|
||||
@@ -228,7 +228,7 @@ const jsonLd = buildDownloadPageSchema({
|
||||
</DownloadSection>
|
||||
|
||||
<DownloadSection id="guides" eyebrow="Keep reading" title="Guides and" accent="background">
|
||||
<RelatedPosts slugs={['xtream-codes-setup-guide', 'stalker-portal-setup-guide', 'beware-unofficial-iptvnator-websites']} />
|
||||
<RelatedPosts slugs={['m3u-playlist-epg-setup-guide', 'xtream-codes-setup-guide', 'stalker-portal-setup-guide']} />
|
||||
<div class="mt-10">
|
||||
<OfficialSourcesNote />
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,11 @@ export const XTREAM_FIXTURE_CREDENTIALS = {
|
||||
password: 'marketing',
|
||||
} as const;
|
||||
|
||||
/** Fictional playlist and guide addresses typed into forms for the M3U guide shots; never fetched. */
|
||||
export const M3U_FIXTURE_PLAYLIST_URL = `${XTREAM_MOCK_ORIGIN}/demo/channels.m3u8`;
|
||||
export const M3U_FIXTURE_PLAYLIST_TITLE = 'Fictional TV playlist';
|
||||
export const EPG_FIXTURE_URL = `${XTREAM_MOCK_ORIGIN}/demo/guide.xml.gz`;
|
||||
|
||||
/** A fictional "your subscription is ready" message for the Auto-detect shot. */
|
||||
export const AUTO_DETECT_FIXTURE_MESSAGE = [
|
||||
'Welcome to Fictional TV! Your account is ready.',
|
||||
|
||||
@@ -8,6 +8,9 @@ import type { Page } from '@playwright/test';
|
||||
|
||||
import {
|
||||
AUTO_DETECT_FIXTURE_MESSAGE,
|
||||
EPG_FIXTURE_URL,
|
||||
M3U_FIXTURE_PLAYLIST_TITLE,
|
||||
M3U_FIXTURE_PLAYLIST_URL,
|
||||
STALKER_FIXTURE_MAC,
|
||||
STALKER_FIXTURE_PORTAL_URL,
|
||||
STALKER_FIXTURE_TITLE,
|
||||
@@ -82,10 +85,12 @@ export async function runAction(
|
||||
action: string,
|
||||
param: string | null
|
||||
): Promise<void> {
|
||||
// Manifest steps are order-independent, and two of them end with a modal
|
||||
// dialog open. Close whatever the previous step left behind before this
|
||||
// one starts navigating, or the dialog backdrop swallows every click.
|
||||
// Manifest steps are order-independent, and some of them end with a modal
|
||||
// dialog open or a dirty settings form. Clear whatever the previous step
|
||||
// left behind before this one starts navigating: a dialog backdrop
|
||||
// swallows every click, and unsaved settings raise a leave prompt.
|
||||
await dismissDialogs(page);
|
||||
await discardUnsavedSettings(page);
|
||||
|
||||
switch (action) {
|
||||
case 'open-settings': {
|
||||
@@ -299,6 +304,51 @@ export async function runAction(
|
||||
await page.waitForTimeout(700);
|
||||
return;
|
||||
}
|
||||
case 'open-add-playlist-m3u-url': {
|
||||
await goHome(page);
|
||||
await openAddPlaylistDialog(page);
|
||||
const dialog = page.locator('mat-dialog-container').last();
|
||||
|
||||
await clickDialogOption(dialog, /m3u url/i);
|
||||
// Typed only: the dialog fetches nothing until Add is clicked,
|
||||
// and the address points at the local mock anyway.
|
||||
await dialog
|
||||
.locator('input[formcontrolname="playlistUrl"]')
|
||||
.fill(M3U_FIXTURE_PLAYLIST_URL);
|
||||
await dialog
|
||||
.locator('input[formcontrolname="playlistName"]')
|
||||
.fill(M3U_FIXTURE_PLAYLIST_TITLE);
|
||||
await page.waitForTimeout(500);
|
||||
return;
|
||||
}
|
||||
case 'open-settings-epg': {
|
||||
await runAction(page, 'open-settings', null);
|
||||
const sectionLink = page
|
||||
.locator('[data-test-id="settings-section-epg"]')
|
||||
.first();
|
||||
|
||||
await sectionLink.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
await sectionLink.click({ timeout: 10_000 });
|
||||
await page.waitForURL(/\/workspace\/settings\/epg/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
const section = page.locator('#epg');
|
||||
await section.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
// Show a filled source row instead of the empty state. The value
|
||||
// is staged in the form only; nothing is saved or fetched. The
|
||||
// dirty form is discarded by `discardUnsavedSettings` before the
|
||||
// next action or the app teardown — the settings close guard
|
||||
// would otherwise hold `app.close()` open forever.
|
||||
await section
|
||||
.getByRole('button', { name: /add epg source/i })
|
||||
.click({ timeout: 10_000 });
|
||||
const field = section.locator('input[type="url"]').last();
|
||||
await field.waitFor({ state: 'visible', timeout: 10_000 });
|
||||
await field.fill(EPG_FIXTURE_URL, { timeout: 10_000 });
|
||||
await page.waitForTimeout(500);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown setup action: ${action}`);
|
||||
}
|
||||
@@ -334,6 +384,24 @@ export async function clickDialogOption(
|
||||
throw new Error(`Dialog option matching ${label} not found`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings edits staged by a shot (the EPG source row) must never persist:
|
||||
* saving would start a fetch, and a dirty form arms the app's close guard,
|
||||
* which blocks `app.close()` until someone answers the save/discard prompt.
|
||||
*/
|
||||
export async function discardUnsavedSettings(page: Page): Promise<void> {
|
||||
const discard = page.locator('[data-test-id="discard-settings"]').first();
|
||||
|
||||
if ((await discard.count()) === 0 || !(await discard.isVisible())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await discard.click({ timeout: 10_000 });
|
||||
await discard
|
||||
.waitFor({ state: 'hidden', timeout: 10_000 })
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
async function dismissDialogs(page: Page): Promise<void> {
|
||||
const dialogs = page.locator('mat-dialog-container');
|
||||
|
||||
|
||||
@@ -52,7 +52,12 @@ import {
|
||||
validateReleaseSlug,
|
||||
} from './screenshot-guards.mjs';
|
||||
import * as driver from './capture-app-driver';
|
||||
import { applyTheme, runAction, settleUi } from './capture-navigation';
|
||||
import {
|
||||
applyTheme,
|
||||
discardUnsavedSettings,
|
||||
runAction,
|
||||
settleUi,
|
||||
} from './capture-navigation';
|
||||
import { assertTmdbDisabled } from './capture-tmdb-check';
|
||||
import {
|
||||
drainRecordedRequests,
|
||||
@@ -162,6 +167,7 @@ async function main(): Promise<void> {
|
||||
: undefined;
|
||||
const dataDir = mkdtempSync(path.join(tmpdir(), 'iptvnator-release-shots-'));
|
||||
let app: Awaited<ReturnType<typeof driver.launchApp>> | undefined;
|
||||
let page: Page | undefined;
|
||||
let recordedRequests: string[] = [];
|
||||
let captured = 0;
|
||||
let primaryError: unknown;
|
||||
@@ -184,7 +190,7 @@ async function main(): Promise<void> {
|
||||
// main process itself fetches.
|
||||
await installRequestRecorder(app, networkPolicy());
|
||||
|
||||
const page = await driver.findMainWindow(app);
|
||||
page = await driver.findMainWindow(app);
|
||||
|
||||
// G3, second layer: page-level deny-by-default, which also blocks.
|
||||
await page.route('**/*', async (route) => {
|
||||
@@ -268,6 +274,11 @@ async function main(): Promise<void> {
|
||||
// isolation override is most likely, so G1 must still be evaluated.
|
||||
primaryError = error;
|
||||
} finally {
|
||||
// A shot may leave the settings form dirty, which arms the app's
|
||||
// close guard and would block the close below indefinitely.
|
||||
if (page) {
|
||||
await discardUnsavedSettings(page).catch(() => undefined);
|
||||
}
|
||||
// Close before the G1 comparison: SQLite runs in WAL mode, so writes
|
||||
// may sit in -wal until the worker shuts down and checkpoints.
|
||||
await app?.close().catch(() => undefined);
|
||||
|
||||
@@ -50,6 +50,8 @@ export const KNOWN_ACTIONS = [
|
||||
'open-xtream-live',
|
||||
'open-add-playlist-stalker',
|
||||
'open-stalker-live',
|
||||
'open-add-playlist-m3u-url',
|
||||
'open-settings-epg',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -195,6 +195,18 @@ describe('manifest validation', () => {
|
||||
title: 'Stalker live',
|
||||
group: 'guides',
|
||||
setup: ['open-stalker-live'],
|
||||
},
|
||||
{
|
||||
slug: 'guide-m3u-add-playlist',
|
||||
title: 'M3U URL form',
|
||||
group: 'guides',
|
||||
setup: ['open-add-playlist-m3u-url'],
|
||||
},
|
||||
{
|
||||
slug: 'guide-epg-settings',
|
||||
title: 'EPG settings',
|
||||
group: 'guides',
|
||||
setup: ['open-settings-epg'],
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -83,6 +83,30 @@
|
||||
"setup": [
|
||||
"open-stalker-live"
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "guide-m3u-add-playlist",
|
||||
"title": "Add playlist: M3U URL",
|
||||
"group": "guides",
|
||||
"setup": [
|
||||
"open-add-playlist-m3u-url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "guide-m3u-live-groups",
|
||||
"title": "M3U Live TV groups",
|
||||
"group": "guides",
|
||||
"setup": [
|
||||
"open-m3u-groups"
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "guide-epg-settings",
|
||||
"title": "Settings: EPG sources",
|
||||
"group": "guides",
|
||||
"setup": [
|
||||
"open-settings-epg"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -27,6 +27,14 @@ const GUIDES = [
|
||||
'blog/guides/screenshots/guide-stalker-live-dark.png',
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'm3u-playlist-epg-setup-guide',
|
||||
screenshots: [
|
||||
'blog/guides/screenshots/guide-m3u-add-playlist-dark.png',
|
||||
'blog/guides/screenshots/guide-m3u-live-groups-dark.png',
|
||||
'blog/guides/screenshots/guide-epg-settings-dark.png',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const readDist = (relativePath) => readFile(new URL(relativePath, distRoot), 'utf8');
|
||||
|
||||
Reference in new issue
Block a user