Skip to content

Feature/custom vod portal#838

Open
Markko1982 wants to merge 5 commits into4gray:masterfrom
Markko1982:feature/custom-vod-portal
Open

Feature/custom vod portal#838
Markko1982 wants to merge 5 commits into4gray:masterfrom
Markko1982:feature/custom-vod-portal

Conversation

@Markko1982
Copy link
Copy Markdown

No description provided.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 27, 2026

Greptile Summary

This PR introduces support for a new custom VOD portal type, allowing users to connect to custom JSON-based VOD APIs through the existing Stalker portal infrastructure. A specially-formatted URL string identifies these portals, which are stored with isCustomPortal, customPortalKey, and customPortalOriginalUrl fields on the Playlist model.

Key changes:

  • New portal type: New fields are stored on Playlist, propagated through the NgRx store and PlaylistMeta pick type.
  • Import flow: StalkerPortalImportComponent can now detect and parse the custom portal URL format; MAC address is made optional for custom portals with a runtime guard.
  • Content fetching: withStalkerContent feature dispatches Stalker IPC requests for custom portal categories and items; custom portals are limited to the VoD content type only.
  • IPC layer: stalker.events.ts is heavily refactored to handle flexible parameter shapes and adds a GET→POST 405 fallback retry; ElectronService mirrors this with a normalizeParamsToPlainObject helper — resulting in redundant normalization on both sides of the IPC boundary.
  • Electron startup: New dev-server readiness polling prevents Electron from racing a slow webpack dev server.
  • Routing: playlist-switcher correctly limits forced-VoD navigation to custom portals; however recent-playlists incorrectly forces VoD for all stalker portals including regular MAC-address ones, which is a regression.

Confidence Score: 4/5

Safe to merge after fixing the getPlaylist regression that forces all stalker portals to the VoD section.

One P1 regression exists: recent-playlists.component.ts getPlaylist() hardcodes the 'vod' section for every stalker portal click, breaking section memory for regular MAC-address stalker portals. The P2 findings (i18n strings, isRecord duplication, redundant IPC normalization, Portuguese comment) are quality improvements but do not block the feature from working.

libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts — the getPlaylist() regression affecting regular stalker portal navigation.

Important Files Changed

Filename Overview
libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts Filter logic updated to treat isCustomPortal playlists as Stalker; getPlaylist() regression forces 'vod' section for ALL stalker portals (custom and regular), breaking last-visited section restoration for regular MAC-address portals.
apps/electron-backend/src/app/events/stalker.events.ts Significantly refactored to support flexible parameter shapes and adds a GET→POST 405 fallback retry; isRecord utility is duplicated from the renderer side and the multi-case normalization logic is largely redundant given IPC serialisation.
apps/web/src/app/services/electron.service.ts Adds normalizeParamsToPlainObject for flexible params input; introduces StalkerPayload/StalkerPayloadInput interfaces and isSilentXtreamAction helper. The normalization logic is effectively duplicated on both sides of the IPC boundary.
libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-content.feature.ts New custom-portal branch dispatches bare STALKER_REQUEST with empty params for categories and encoded JSON category-request for content; correctly guards custom portals to VoD-only content type.
libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-player.feature.ts Substantial refactor of playback resolution with direct-stream detection and fallback logic; contains two Portuguese-language inline comments inconsistent with the codebase.
libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts Adds parsing for custom portal URL syntax, makes MAC address optional with runtime validation, and persists isCustomPortal/customPortalKey/customPortalOriginalUrl; two new snackbar messages are hardcoded English strings that bypass the i18n pipeline.
libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.ts Correctly routes custom portals as Stalker provider, introduces forceVodForCustomPortal flag only for isCustomPortal === true, and adds custom-portal-aware icon/type-label helpers.
libs/shared/interfaces/src/lib/playlist.interface.ts Adds isCustomPortal, customPortalKey, and customPortalOriginalUrl fields to the Playlist interface; well-documented with JSDoc.
libs/m3u-state/src/lib/reducers/playlist.reducers.ts Adds reducer handling for the three new custom-portal fields following the existing conditional-spread pattern.
libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts Adds extractPlaylistChannels() helper to safely handle playlists that have no playlist.items property, returning an empty array with a warning for custom portals.

Comments Outside Diff (2)

  1. apps/electron-backend/src/app/events/stalker.events.ts, line 181-183 (link)

    P2 isRecord utility function duplicated

    An identical isRecord type-guard is also defined as a private method inside ElectronService in apps/web/src/app/services/electron.service.ts (line ~789). Extracting this to a shared utility (e.g., shared-interfaces or a small utils module) would eliminate the duplication and keep the two implementations in sync.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/electron-backend/src/app/events/stalker.events.ts
    Line: 181-183
    
    Comment:
    **`isRecord` utility function duplicated**
    
    An identical `isRecord` type-guard is also defined as a private method inside `ElectronService` in `apps/web/src/app/services/electron.service.ts` (line ~789). Extracting this to a shared utility (e.g., `shared-interfaces` or a small `utils` module) would eliminate the duplication and keep the two implementations in sync.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/web/src/app/services/electron.service.ts, line 793-880 (link)

    P2 Complex parameter normalization duplicated on both sides of IPC boundary

    normalizeParamsToPlainObject() in the renderer and buildMergedRequestParams() / appendParamsFromUnknown() in the main process both perform deep normalization of the params field into a flat Record<string, string>. Since the IPC layer serialises values through JSON.stringify / JSON.parse before they reach the main process, any type information (e.g., URLSearchParams instances) is already lost by the time the backend sees the payload.

    Normalising to a plain object once — on the renderer side before the IPC call — and then sending a Record<string, string> is simpler and removes the need for the elaborate multi-case handling in appendParamsFromUnknown. The current approach adds ~200 lines of essentially redundant defensive code on the backend that will never receive URLSearchParams or custom toString objects.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/web/src/app/services/electron.service.ts
    Line: 793-880
    
    Comment:
    **Complex parameter normalization duplicated on both sides of IPC boundary**
    
    `normalizeParamsToPlainObject()` in the renderer and `buildMergedRequestParams()` / `appendParamsFromUnknown()` in the main process both perform deep normalization of the `params` field into a flat `Record<string, string>`. Since the IPC layer serialises values through `JSON.stringify` / `JSON.parse` before they reach the main process, any type information (e.g., `URLSearchParams` instances) is already lost by the time the backend sees the payload.
    
    Normalising to a plain object once — on the renderer side before the IPC call — and then sending a `Record<string, string>` is simpler and removes the need for the elaborate multi-case handling in `appendParamsFromUnknown`. The current approach adds ~200 lines of essentially redundant defensive code on the backend that will never receive `URLSearchParams` or custom `toString` objects.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts
Line: 158-169

Comment:
**Regular Stalker portals forced to VoD section**

`getPlaylist()` now navigates ALL stalker portals — including regular MAC-address portals — to the hardcoded `'vod'` section. The old code navigated regular stalker portals to `/workspace/stalker/:id` without a section, allowing the router (or section memory) to pick the last-visited or default section. This is inconsistent with `playlist-switcher.component.ts`, where `forceVodForCustomPortal` is only applied when `playlist.isCustomPortal === true`, leaving regular stalker portals free to restore their previous section.

The combined effect is that users of regular (non-custom) stalker portals will always land on the **VoD** tab when clicking a playlist from the dashboard list, even if they were previously browsing **ITV** or **Series**.

```suggestion
    getPlaylist(playlistMeta: PlaylistMeta): void {
        if (playlistMeta.isCustomPortal) {
            this.router.navigate([
                '/workspace',
                'stalker',
                playlistMeta._id,
                'vod',
            ]);
            return;
        }

        if (playlistMeta.macAddress) {
            this.router.navigate(['/workspace', 'stalker', playlistMeta._id]);
            return;
        }

        if (playlistMeta.serverUrl) {
            this.router.navigate(['/workspace', 'xtreams', playlistMeta._id]);
            return;
        }

        this.router.navigate(['/workspace', 'playlists', playlistMeta._id]);
        this.playlistClicked.emit(playlistMeta._id);
    }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-player.feature.ts
Line: 261-263

Comment:
**Portuguese comment in English codebase**

These comments are written in Portuguese and are inconsistent with the rest of the codebase, which uses English throughout.

```suggestion
                    // First try the most direct path when the cmd already
                    // looks like a valid stream.
                    if (canUseDirectUrl) {
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts
Line: 109-133

Comment:
**Hardcoded untranslated UI strings**

Two new user-facing validation messages bypass the i18n pipeline and are hardcoded in English:

- `'Invalid portal URL format.'` (line 110)
- `'MAC address is required for stalker portals.'` (line 128)

All other snackbar messages in this file and elsewhere in the project use `this.translate.instant(...)` or equivalent. These should either be added to the translation files and accessed via the `TranslateService`, or at minimum be surfaced as named constants so they can be updated consistently.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/electron-backend/src/app/events/stalker.events.ts
Line: 181-183

Comment:
**`isRecord` utility function duplicated**

An identical `isRecord` type-guard is also defined as a private method inside `ElectronService` in `apps/web/src/app/services/electron.service.ts` (line ~789). Extracting this to a shared utility (e.g., `shared-interfaces` or a small `utils` module) would eliminate the duplication and keep the two implementations in sync.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/web/src/app/services/electron.service.ts
Line: 793-880

Comment:
**Complex parameter normalization duplicated on both sides of IPC boundary**

`normalizeParamsToPlainObject()` in the renderer and `buildMergedRequestParams()` / `appendParamsFromUnknown()` in the main process both perform deep normalization of the `params` field into a flat `Record<string, string>`. Since the IPC layer serialises values through `JSON.stringify` / `JSON.parse` before they reach the main process, any type information (e.g., `URLSearchParams` instances) is already lost by the time the backend sees the payload.

Normalising to a plain object once — on the renderer side before the IPC call — and then sending a `Record<string, string>` is simpler and removes the need for the elaborate multi-case handling in `appendParamsFromUnknown`. The current approach adds ~200 lines of essentially redundant defensive code on the backend that will never receive `URLSearchParams` or custom `toString` objects.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "WIP: custom VOD portal current debug sta..." | Re-trigger Greptile

Comment on lines 158 to +169
}

getPlaylist(playlistMeta: PlaylistMeta): void {
if (playlistMeta.isCustomPortal || playlistMeta.macAddress) {
this.router.navigate([
'/workspace',
'stalker',
playlistMeta._id,
'vod',
]);
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Regular Stalker portals forced to VoD section

getPlaylist() now navigates ALL stalker portals — including regular MAC-address portals — to the hardcoded 'vod' section. The old code navigated regular stalker portals to /workspace/stalker/:id without a section, allowing the router (or section memory) to pick the last-visited or default section. This is inconsistent with playlist-switcher.component.ts, where forceVodForCustomPortal is only applied when playlist.isCustomPortal === true, leaving regular stalker portals free to restore their previous section.

The combined effect is that users of regular (non-custom) stalker portals will always land on the VoD tab when clicking a playlist from the dashboard list, even if they were previously browsing ITV or Series.

Suggested change
}
getPlaylist(playlistMeta: PlaylistMeta): void {
if (playlistMeta.isCustomPortal || playlistMeta.macAddress) {
this.router.navigate([
'/workspace',
'stalker',
playlistMeta._id,
'vod',
]);
return;
}
getPlaylist(playlistMeta: PlaylistMeta): void {
if (playlistMeta.isCustomPortal) {
this.router.navigate([
'/workspace',
'stalker',
playlistMeta._id,
'vod',
]);
return;
}
if (playlistMeta.macAddress) {
this.router.navigate(['/workspace', 'stalker', playlistMeta._id]);
return;
}
if (playlistMeta.serverUrl) {
this.router.navigate(['/workspace', 'xtreams', playlistMeta._id]);
return;
}
this.router.navigate(['/workspace', 'playlists', playlistMeta._id]);
this.playlistClicked.emit(playlistMeta._id);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts
Line: 158-169

Comment:
**Regular Stalker portals forced to VoD section**

`getPlaylist()` now navigates ALL stalker portals — including regular MAC-address portals — to the hardcoded `'vod'` section. The old code navigated regular stalker portals to `/workspace/stalker/:id` without a section, allowing the router (or section memory) to pick the last-visited or default section. This is inconsistent with `playlist-switcher.component.ts`, where `forceVodForCustomPortal` is only applied when `playlist.isCustomPortal === true`, leaving regular stalker portals free to restore their previous section.

The combined effect is that users of regular (non-custom) stalker portals will always land on the **VoD** tab when clicking a playlist from the dashboard list, even if they were previously browsing **ITV** or **Series**.

```suggestion
    getPlaylist(playlistMeta: PlaylistMeta): void {
        if (playlistMeta.isCustomPortal) {
            this.router.navigate([
                '/workspace',
                'stalker',
                playlistMeta._id,
                'vod',
            ]);
            return;
        }

        if (playlistMeta.macAddress) {
            this.router.navigate(['/workspace', 'stalker', playlistMeta._id]);
            return;
        }

        if (playlistMeta.serverUrl) {
            this.router.navigate(['/workspace', 'xtreams', playlistMeta._id]);
            return;
        }

        this.router.navigate(['/workspace', 'playlists', playlistMeta._id]);
        this.playlistClicked.emit(playlistMeta._id);
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +261 to +263
// Primeiro tenta o caminho mais direto quando o cmd já
// parece um stream válido.
if (canUseDirectUrl) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Portuguese comment in English codebase

These comments are written in Portuguese and are inconsistent with the rest of the codebase, which uses English throughout.

Suggested change
// Primeiro tenta o caminho mais direto quando o cmd
// parece um stream válido.
if (canUseDirectUrl) {
// First try the most direct path when the cmd already
// looks like a valid stream.
if (canUseDirectUrl) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-player.feature.ts
Line: 261-263

Comment:
**Portuguese comment in English codebase**

These comments are written in Portuguese and are inconsistent with the rest of the codebase, which uses English throughout.

```suggestion
                    // First try the most direct path when the cmd already
                    // looks like a valid stream.
                    if (canUseDirectUrl) {
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +109 to +133
if (!parsedPortal) {
this.snackBar.open('Invalid portal URL format.', undefined, {
duration: 4000,
});
return;
}

const {
originalInput,
normalizedPortalUrl,
isFullStalkerPortal,
isCustomPortal,
customPortalKey,
} = parsedPortal;

const macAddress = this.form.value.macAddress?.trim() || undefined;

if (!isCustomPortal && !macAddress) {
this.snackBar.open(
'MAC address is required for stalker portals.',
undefined,
{ duration: 5000 }
);
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded untranslated UI strings

Two new user-facing validation messages bypass the i18n pipeline and are hardcoded in English:

  • 'Invalid portal URL format.' (line 110)
  • 'MAC address is required for stalker portals.' (line 128)

All other snackbar messages in this file and elsewhere in the project use this.translate.instant(...) or equivalent. These should either be added to the translation files and accessed via the TranslateService, or at minimum be surfaced as named constants so they can be updated consistently.

Prompt To Fix With AI
This is a comment left during a code review.
Path: libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts
Line: 109-133

Comment:
**Hardcoded untranslated UI strings**

Two new user-facing validation messages bypass the i18n pipeline and are hardcoded in English:

- `'Invalid portal URL format.'` (line 110)
- `'MAC address is required for stalker portals.'` (line 128)

All other snackbar messages in this file and elsewhere in the project use `this.translate.instant(...)` or equivalent. These should either be added to the translation files and accessed via the `TranslateService`, or at minimum be surfaced as named constants so they can be updated consistently.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant