mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
feat(epg): accept local XMLTV files as EPG sources (#1600)
* feat(epg): accept local XMLTV files as EPG sources Settings → EPG and the playlist dialog accepted `file://` in their form pattern, but the main process rejected everything except http(s), so a local XMLTV entry saved fine and then failed on import. Both surfaces now take a remote link, a `file:` URL, an absolute POSIX path or a Windows drive/UNC path (`classifyEpgSourceReference` in shared/interfaces), and the settings section spells out the accepted formats with examples. The EPG worker opens every source through `openEpgSourceStream`: remote links keep the validated-redirect client and trust policy, local files are read from disk behind the signature-sniffing optional gunzip stage, so .xml, .xml.gz and extension-less gzip all parse. Only hand-typed sources may be local: `extractM3uEpgUrls` harvests http(s) links only from M3U headers, since the local branch bypasses `validateRemoteUrl`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): pick local XMLTV files with a native file dialog A folder button beside each EPG source row (Settings → EPG and the playlist dialog) opens the native open-file dialog and writes the chosen absolute path into the row. New `EPG_OPEN_FILE_DIALOG` IPC behind `ElectronBridgeApi.openEpgFileDialog`, gated in the renderer by `RuntimeCapabilitiesService.supportsEpgFilePicker`. The row's refresh/remove buttons carry `data-test-id`s now, and the EPG e2e suites address them by id instead of index, since the folder button became the first button in a row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): authorize local XMLTV files in the main process Review follow-up (Greptile P1, Codex P1). The renderer hands source strings to FETCH_EPG/EPG_FORCE_FETCH unchanged, so the form validator alone could not enforce the provenance rule: a compromised renderer, or a legacy `file://` entry an older version stored from an M3U header, could name any file on disk. `EpgWorkerService.startFetch` now asks a main-process `EpgLocalSourceAuthorizer` before a local path reaches the worker: a path the native picker returned is trusted at once, a hand-typed path is confirmed once in a native message box the renderer cannot fake, and a refusal is reported in the progress panel. Allowed paths persist under TRUSTED_LOCAL_EPG_SOURCES in the main-process config. The worker opens its local branch only when main set `allowLocalFile`; the service defaults to deny-all until epg.events installs the persisted authorizer. `resolvePlaylistEpgSourceState` and `filterPlaylistEpgUrlsForFetch` drop a stored non-remote entry unless it is also in `manualEpgUrls`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): fail a refused local EPG fetch instead of resolving it Review follow-up (Codex P2). A denied native confirmation now rejects the fetch after reporting the error row, so handleFetchEpg and the renderer's fetch result cannot claim the file was read. Also restores the unrelated CLAUDE.md paragraph an earlier formatter pass had reflowed into a list. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): cancel a local source retired during its authorization prompt Review follow-up (Codex P2). startFetch keeps the request generation captured before awaiting the native confirmation and rechecks it afterwards: a source retired meanwhile ends as cancelled instead of starting an import that a pending clear would then have to await. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(epg): leave the local XMLTV e2e with a pristine settings form The local-file test ended with the EPG source field still dirty, which arms the main-process close guard: the app then waited for the unsaved changes dialog instead of closing, the close timeout killed it, and on Windows the killed process kept iptvnator.db busy (EBUSY on the data-dir cleanup) and hung the Playwright worker teardown. Discarding the form before the app closes takes the test from 15 s to 4 s locally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
65 files changed
+1621
-149
No files matched your search
@@ -1081,6 +1081,63 @@ retried as plain XML. Source errors and consumer cancellation terminate the
|
||||
whole decoding chain. The decoder does not recursively unpack file layers or
|
||||
change XML parsing, source reconciliation, or database persistence contracts.
|
||||
|
||||
### Local XMLTV files
|
||||
|
||||
An EPG source is not necessarily a URL. Settings → EPG and the playlist
|
||||
dialog accept, next to `http(s)` links, a file on the user's computer in any
|
||||
of these shapes (Electron only — the PWA has no EPG import at all):
|
||||
|
||||
- a `file:` URL (`file:///home/you/epg/guide.xml.gz`, `file:///C:/epg/guide.xml`)
|
||||
- an absolute POSIX path (`/home/you/epg/guide.xml`)
|
||||
- a Windows drive or UNC path (`C:\epg\guide.xml.gz`, `\\nas\share\guide.xml`)
|
||||
|
||||
Both surfaces also offer a folder button that opens the native file picker
|
||||
(`EPG_OPEN_FILE_DIALOG` → `ElectronBridgeApi.openEpgFileDialog`, gated on
|
||||
`RuntimeCapabilitiesService.supportsEpgFilePicker`) and writes the chosen
|
||||
absolute path into the row. Relative paths are refused: the main process
|
||||
has no meaningful working directory to resolve them against. The value is stored exactly as typed
|
||||
(trimmed) and is the source key everywhere — freshness, reconciliation, the
|
||||
progress panel and `epg_channel_sources` all treat it like a URL string.
|
||||
|
||||
The shape rules live in one place, `classifyEpgSourceReference()` in
|
||||
`libs/shared/interfaces/src/lib/epg-source-reference.util.ts`, and both
|
||||
forms validate through its structural `validateEpgSourceReferenceControl`.
|
||||
In the worker, `openEpgSourceStream()` (`epg-source-stream.ts`) is the single
|
||||
entry that yields decoded XMLTV bytes: remote sources still go through
|
||||
`requestWithValidatedRedirects` with the private-network/TLS trust policy,
|
||||
while local sources are read with `fs.createReadStream` behind the same
|
||||
signature-sniffing optional gunzip stage the HTTP path uses, so `.xml`,
|
||||
`.xml.gz` and an extension-less gzip file all parse. A missing file, a
|
||||
directory, or a truncated gzip fail the import with a plain message; nothing
|
||||
is retried as XML.
|
||||
|
||||
**Provenance rule.** Only values the user chose or typed by hand may be
|
||||
local, and the decision is enforced in the main process, not in the form.
|
||||
The local branch bypasses `validateRemoteUrl` (which only knows http/https),
|
||||
so two layers guard it:
|
||||
|
||||
- Header-declared M3U sources (`x-tvg-url`/`url-tvg`/`tvg-url`) are filtered
|
||||
to remote links — `extractM3uEpgUrls` no longer matches `file:`, and
|
||||
`resolvePlaylistEpgSourceState` / `filterPlaylistEpgUrlsForFetch` drop a
|
||||
legacy non-remote entry that older versions stored from a header unless it
|
||||
also appears in `manualEpgUrls`. A downloaded playlist cannot point the
|
||||
importer at a file on disk; a user whose own local M3U references a local
|
||||
XMLTV adds that file in the playlist dialog instead.
|
||||
- `FETCH_EPG`/`EPG_FORCE_FETCH` carry renderer-supplied strings, so
|
||||
`EpgWorkerService.startFetch` asks the main-process
|
||||
`EpgLocalSourceAuthorizer` (`epg-local-source-authorizer.ts`) before a
|
||||
local path reaches the worker. A path the native picker returned is trusted
|
||||
at once; a hand-typed path is confirmed once in a native message box the
|
||||
renderer cannot fake, and a refusal fails that fetch — the progress panel
|
||||
shows it with Retry ("Retry to be asked again") and the IPC result does
|
||||
not report success. Allowed paths persist under
|
||||
`TRUSTED_LOCAL_EPG_SOURCES` in the main-process config (`store.service.ts`)
|
||||
so startup refreshes need no prompt. The worker's local branch opens only
|
||||
when main set `allowLocalFile` on the fetch options — the flag is never
|
||||
taken from the renderer — and the service defaults to a deny-all authorizer
|
||||
until `epg.events.ts` installs the persisted one, so a missing wiring fails
|
||||
closed. This mirrors `save-file-dialog` → `write-file` for playlist exports.
|
||||
|
||||
### EpgService (`@iptvnator/epg/data-access`)
|
||||
|
||||
```typescript
|
||||
|
||||
Reference in new issue
Block a user