mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
* feat(packaging): register IPTVnator as the .m3u/.m3u8 handler Every runtime path for an OS-supplied playlist existed, but no packaging metadata claimed the file types — so the OS never offered IPTVnator as a handler and `open-file` could not fire from Finder. `fileAssociations` declares one entry per extension. Electron Builder derives all three platform registrations from it: macOS `CFBundleDocumentTypes` (the prerequisite for `open-file`), the NSIS registry entries, and, on Linux, the desktop entry's `MimeType` plus `/usr/share/mime/packages/iptvnator.xml` for deb/rpm/pacman. Neither platform needs a dedicated icon — both fall back to the app icon. Declaring `MimeType` under `linux.desktop.entry` would not have worked: Electron Builder assigns the association-derived value *after* spreading that object, so an explicit key there is silently overwritten. The per-association `mimeType` fields produce the same entry through the supported path. Registering the types also exposes a gap in the delivery side. The generated Linux `Exec` ends in `%U`, so file managers hand over a percent-encoded `file://` URI rather than a path, which `createPlaylistOpenRequest` would have resolved into a bogus relative path. It now decodes a `file://` candidate before the extension check. Suppressing the `%U` instead would mean putting an exec code in `linux.executableArgs`, which also passes it to the app as a real argument. Verified on macOS against a signed packaged bundle: Launch Services lists the app as a `public.m3u-playlist` handler, and an LS-initiated open imports the playlist both on a cold launch and against the already-running process. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(playlist): open every playlist of a multi-file selection `%U` is the plural exec code, so selecting several playlists in a Linux file manager is one launch carrying one argument per file. Both argv paths called `extractPlaylistOpenRequestFromArgv`, which returned at the first match, so everything after the first playlist was silently discarded — a gap this PR itself opened by making the desktop entry reachable in the first place. The extractor is now plural and returns every match in argument order, and the queue gained `enqueueAll` so a selection is pushed under a single flush: a delivery that fails partway leaves the untouched remainder queued in arrival order rather than interleaved. Covered by unit tests over a mixed argv (percent-encoded `file://` URI, a non-playlist argument, a second URI) and by a new Electron E2E that launches with two playlist arguments and asserts both are imported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { copyFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import {
|
|
closeElectronApp,
|
|
expect,
|
|
importM3uPlaylistFromNativeDialog,
|
|
launchCompetingElectronInstance,
|
|
launchElectronApp,
|
|
m3uFixturePath,
|
|
openSources,
|
|
test,
|
|
} from './electron-test-fixtures';
|
|
|
|
test.describe('Electron Native Playlist Import', () => {
|
|
test('@m3u @electron imports an M3U playlist via the native file picker path', async ({
|
|
dataDir,
|
|
}) => {
|
|
const app = await launchElectronApp(dataDir);
|
|
|
|
try {
|
|
await importM3uPlaylistFromNativeDialog(app, m3uFixturePath);
|
|
|
|
await app.mainWindow.waitForURL(/\/workspace\/playlists\/.+/);
|
|
await expect(
|
|
app.mainWindow.getByTestId('channel-item')
|
|
).toHaveCount(4);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
|
|
test('@m3u @electron opens a playlist passed on the command line', async ({
|
|
dataDir,
|
|
}) => {
|
|
// Mirrors a file-association double-click on Windows/Linux: the path
|
|
// is the app's own argv, resolved and queued in the main process
|
|
// before the renderer exists.
|
|
const app = await launchElectronApp(dataDir, {
|
|
appArgs: [m3uFixturePath],
|
|
});
|
|
|
|
try {
|
|
await app.mainWindow.waitForURL(/\/workspace\/playlists\/.+/, {
|
|
timeout: 30000,
|
|
});
|
|
await expect(
|
|
app.mainWindow.getByTestId('channel-item')
|
|
).toHaveCount(4);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
|
|
test('@m3u @electron opens every playlist of a multi-file selection', async ({
|
|
dataDir,
|
|
}) => {
|
|
// Selecting several playlists in a file manager is a single launch
|
|
// with one argument per file, because the generated desktop entry
|
|
// ends in `%U`. Stopping at the first would drop the rest.
|
|
const secondPlaylist = join(dataDir, 'second-selection.m3u');
|
|
copyFileSync(m3uFixturePath, secondPlaylist);
|
|
|
|
const app = await launchElectronApp(dataDir, {
|
|
appArgs: [m3uFixturePath, secondPlaylist],
|
|
});
|
|
|
|
try {
|
|
await app.mainWindow.waitForURL(/\/workspace\/playlists\/.+/, {
|
|
timeout: 30000,
|
|
});
|
|
await openSources(app.mainWindow);
|
|
await expect(
|
|
app.mainWindow.locator('app-playlist-item')
|
|
).toHaveCount(2, { timeout: 30000 });
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
|
|
test('@m3u @electron opens a playlist handed over by a second launch', async ({
|
|
dataDir,
|
|
}) => {
|
|
// The second process never gets a window: the single-instance guard
|
|
// makes it quit and forwards its argv to the running app.
|
|
const app = await launchElectronApp(dataDir);
|
|
|
|
try {
|
|
const competing = await launchCompetingElectronInstance(dataDir, {
|
|
appArgs: [m3uFixturePath],
|
|
});
|
|
|
|
expect(
|
|
competing.timedOut,
|
|
`Competing instance did not exit. stderr: ${competing.stderr}`
|
|
).toBe(false);
|
|
|
|
await app.mainWindow.waitForURL(/\/workspace\/playlists\/.+/, {
|
|
timeout: 30000,
|
|
});
|
|
await expect(
|
|
app.mainWindow.getByTestId('channel-item')
|
|
).toHaveCount(4);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
});
|