Skip to content

Conversation

@4gray
Copy link
Owner

@4gray 4gray commented Sep 19, 2025

  • Removed unnecessary imports from header, account-info,
    loading-overlay, navigation-bar, season-container, serial-details, and
    vod-details components.
  • Updated template syntax to use structural directives more efficiently in mpv-player-bar, account-info, navigation-bar, season-container, serial-details, and vod-details components.
  • Enhanced readability and maintainability of the code by organizing imports and using Angular's async pipe and structural directives appropriately.
  • Adjusted TypeScript configuration to use "bundler" for module resolution for better compatibility with modern build tools.

Summary by CodeRabbit

  • New Features

    • Settings: Manage multiple EPG URLs with per-entry refresh/remove/add actions; expanded VOD/serial metadata shown (director, duration, ratings).
  • UI Changes

    • Header menus reorganized with consolidated sort/filter and add-playlist actions.
    • Improved loading placeholders, responsive tweaks across players/lists, and refined player dialog sizing.
    • Video player selection and player-specific UI streamlined.
  • Breaking Changes

    • PWA service worker/offline caching removed.
  • Style

    • Darker active-sort highlight; input width and small-screen player/logo sizing adjusted.
  • Chores

    • Upgraded Angular to 20.x and TypeScript to 5.9.x.

@vercel
Copy link

vercel bot commented Sep 19, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
iptvnator Ready Ready Preview Comment Sep 21, 2025 10:46am

@coderabbitai
Copy link

coderabbitai bot commented Sep 19, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Expected object, received null at "reviews"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Upgrade to Angular 20 and TypeScript 5.9; schematics metadata extended. Templates migrated from Angular structural directives to new @if/@for/@defer-style syntax and many components removed CommonModule/NgIf/N gFor imports. rxResource usages refactored from request/loader to params/stream. Bootstrap simplified to platformBrowser().

Changes

Cohort / File(s) Summary
Tooling & Config
package.json, tsconfig.json, angular.json
Bump Angular packages to 20.x and TypeScript to 5.9; change tsconfig.moduleResolution to "bundler"; add schematic type/typeSeparator metadata in angular.json.
Bootstrap
src/main.ts
Replace platformBrowserDynamic() with platformBrowser(); remove Tauri detection and service worker registration.
Schematics
angular.json
Add explicit schematic metadata: types for component/directive/service and typeSeparator entries for guard/interceptor/module/pipe/resolver.
App Root
src/app/app.component.html, src/app/app.component.ts
Template *ngIf → @if; drop ModalWindow import and change modals type to any[].
Template directive migration
Multiple templates (examples below)
Replace Angular structural directives (*ngIf/*ngFor/ng-template) with @if/@for/@else/@defer-style blocks across many templates (home, recent-playlists, header, mpv-player-bar, multi-epg, video-player, web-player-view, settings, navigation-bar, season-container, serial-details, vod-details, etc.).
Component imports cleanup
Examples: src/app/*/*/*.component.ts (many)
Remove CommonModule, NgIf, NgFor from standalone component imports where no longer needed; keep remaining pipes/modules.
Recent Playlists
src/app/home/recent-playlists/*
Migrate template to @if/@for; make searchQuery readonly; add ghostElements readonly array; remove NgIf/NgFor imports.
Home
src/app/home/home.component.*
Template *ngIf → @if; remove NgIf from standalone imports.
Header
src/app/shared/components/header/*
Rewrite menu/modal structures to @if/@for; inline menus; remove NgIf import; change .active-sort background color.
WhatsNew service
src/app/services/whats-new.service.ts
Comment out ModalWindow import; remove explicit return type annotations from getModalsByVersion and getLatestChanges.
Audio Player
src/app/player/components/audio-player/*
Template conditionals → @if; remove NgIf import; mark @Input({ required: true }) url: string; removed a console.log.
Player components (minor metadata changes)
e.g., art-player, d-player, epg-item-description, web-player-view, loading-overlay, account-info, season-container, serial-details, vod-details
Removed NgIf/CommonModule/other module imports from standalone imports; templates updated to new block syntax where applicable; added total input signal to LoadingOverlayComponent.
Multi-EPG & EPG templates
src/app/player/components/multi-epg/*, .../epg-item-description/*
Convert nested *ngFor/*ngIf to @for/@if; adjust trackBy logic (stringified key) and remove debug logs.
MPV Player Bar
src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html
Replace *ngIf/*ngFor with @if/@for; explicit thumbnail fallback handling.
Stalker resources & components
src/app/stalker/stalker.store.ts, src/app/stalker/*
Replace rxResource usages: requestparams, loaderloader({ params }) or stream where applicable; update IPC payload construction to use params.* and switch loaders to streams in components.
Xtream UI tweaks
src/app/xtream-tauri/category-content-view/*, xtream-main-container, player-dialog.scss
pageSizeOptions now conditional on isStalker; PlayerDialog open calls add maxWidth/maxHeight; .link-input width changed to 100%.
Tauri native (Rust)
src-tauri/src/commands/media.rs
Add non-empty origin handling for VLC invocation by appending --http-origin=<origin> when origin provided.
Styles
various .scss files
Minor CSS adjustments: radio logo sizing for small screens; header .active-sort background color; other small formatting changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Browser
  participant AppModule as Angular App
  Note over Browser,AppModule: Simplified bootstrap
  User->>Browser: request app
  Browser->>AppModule: platformBrowser().bootstrapModule(AppModule)
  AppModule-->>Browser: app initialized (no SW registration)
Loading
sequenceDiagram
  autonumber
  participant Component as UI Component
  participant Resource as rxResource
  participant Service as IPC/Playlist Service
  Note over Component,Resource: Resource API refactor (request/loader → params/stream)
  Component->>Resource: params(): { ... }
  alt stream-based loader
    Resource->>Service: stream(params)
    Service-->>Resource: Observable<Data>
    Resource-->>Component: emits values/status
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

Hoppity hop through templates I prance,
Swapping ngs for @ifs in a tidy new dance.
Bundler-bound boots and players set right,
Params whisper streams in the Stalker’s night.
A rabbit-sized stamp: v20—what a delight! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "chore: update to angular v20" is concise, a single short sentence, and accurately summarizes the primary change in the PR (upgrade/migration to Angular v20 and related dependency/tooling updates visible in package.json, angular.json, and tsconfig). It is specific enough for teammates to understand the main intent without listing files or extraneous details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/update-angular-to-v20

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/app/stalker/stalker.store.ts (2)

186-186: Null check order bug: response.js is accessed before verifying response.

Move patchState inside the response check and harden the error path.

-                patchState(store, { totalCount: response.js.total_items });
-
-                if (response) {
+                if (response) {
+                    patchState(store, { totalCount: response.js?.total_items ?? 0 });
                     const newItems = response.js.data.map((item) => ({
                         ...item,
                         cover: item.screenshot_uri,
                     }));
@@
-                } else {
-                    throw new Error(
-                        `Error: ${response.message} (Status: ${response.status})`
-                    );
-                }
+                }
+                else {
+                    throw new Error('STALKER_REQUEST returned no response for content list');
+                }

Also applies to: 216-220


141-146: Error construction dereferences undefined response.

Throw a fixed message (or use optional chaining) when response is falsy.

-                } else {
-                    throw new Error(
-                        `Error: ${response.message} (Status: ${response.status})`
-                    );
-                }
+                } else {
+                    throw new Error('STALKER_REQUEST returned no response for categories');
+                }

Apply the same pattern to the getContentResource error block (lines 217–220).

Also applies to: 217-220

src/app/home/home.component.ts (1)

14-24: Add standalone: true; imports require standalone components.

This component declares an imports array but isn’t marked standalone. In Angular, imports on @component are only valid for standalone components. Add standalone: true to avoid a compile error and ensure the listed imports are honored. Also verify that TranslatePipe is available as a standalone import in your ngx-translate version; if not, import TranslateModule instead. (v18.angular.dev)

Apply this diff:

 @Component({
   selector: 'app-home',
   templateUrl: './home.component.html',
   styleUrls: ['./home.component.scss'],
+  standalone: true,
   imports: [
     HeaderComponent,
     MatProgressBarModule,
     RecentPlaylistsComponent,
     TranslatePipe
   ],
 })
src/app/player/components/audio-player/audio-player.component.ts (1)

107-111: Avoid using ViewChild in ngOnChanges; guard and don’t reassign src imperatively.

@ViewChild may not be ready for the first ngOnChanges. Also, with [src] bound, you don’t need to set it manually.

-  ngOnChanges(changes: SimpleChanges): void {
-      this.audio.nativeElement.src = changes.url.currentValue;
-      this.audio.nativeElement.load();
-      this.play();
-  }
+  ngOnChanges(changes: SimpleChanges): void {
+    if ('url' in changes && this.audio?.nativeElement) {
+      this.audio.nativeElement.load();
+      this.play();
+    }
+  }
src/app/player/components/video-player/video-player.component.html (1)

30-39: Avoid “undefined” in stream URL when epgParams is missing.

Concatenation can produce ...undefined. Default epgParams to empty string.

-                    [options]="{
-                        sources: [
-                            {
-                                src:
-                                    activeChannel?.url +
-                                    activeChannel?.epgParams,
-                                type: 'application/x-mpegURL',
-                            },
-                        ],
-                    }"
+                    [options]="{
+                        sources: [
+                            {
+                                src: (activeChannel?.url ?? '') + (activeChannel?.epgParams ?? ''),
+                                type: 'application/x-mpegURL'
+                            }
+                        ]
+                    }"
src/app/player/components/multi-epg/multi-epg-container.component.ts (1)

60-68: Unsubscribed input subscription can leak.

Use takeUntilDestroyed() for the playlistChannels subscription.

+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
...
     @Input() set playlistChannels(value: Observable<Channel[]>) {
         if (value) {
-            value.subscribe((channels) => {
+            value.pipe(takeUntilDestroyed()).subscribe((channels) => {
                 this._playlistChannels = channels;
                 this.initializeVisibleChannels();
                 this.requestPrograms();
             });
         }
     }
src/app/shared/components/header/header.component.ts (1)

165-173: Rename opedAddPlaylistDialog → openAddPlaylistDialog and update call sites.

Rename the method in src/app/shared/components/header/header.component.ts and update the template bindings below so the click handlers point to the new name.

Locations to update:

  • src/app/shared/components/header/header.component.ts: lines ~165-173 (method).
  • src/app/shared/components/header/header.component.html: lines 192, 200, 208, 217, 227 (click handlers).

Diff (method rename):

-    opedAddPlaylistDialog(type: PlaylistType) {
+    openAddPlaylistDialog(type: PlaylistType) {
         this.dialog.open<AddPlaylistDialogComponent, { type: PlaylistType }>(
             AddPlaylistDialogComponent,
             {
                 width: '600px',
                 data: { type },
             }
         );
     }
src/app/xtream/vod-details/vod-details.component.ts (1)

40-42: Guard against potential null parent route

Accessing this.route.parent.snapshot can throw when parent is null. Use optional chaining.

Apply this diff:

-  private portalId =
-      this.route.snapshot.paramMap.get('id') ??
-      this.route.parent.snapshot.params.id;
+  private portalId =
+      this.route.snapshot.paramMap.get('id') ??
+      this.route.parent?.snapshot?.params?.id;
🧹 Nitpick comments (49)
src/app/xtream-tauri/player-dialog/player-dialog.component.scss (1)

8-12: Guard against overflow with 100% width in dialog inputs

Bumping to 100% can introduce horizontal scroll in some layouts (borders/padding, flex containers). Add safe constraints so it never exceeds the container.

 .link-input {
-    width: 100%;
+    width: 100%;
+    max-width: 100%;
+    min-width: 0; // avoids flex overflow
+    box-sizing: border-box; // ensures borders/padding don’t spill
     padding-top: 5px;
     @include mat.form-field-density(-5);
 }
src/app/xtream/season-container/season-container.component.html (3)

3-11: Use stable track expression and ensure numeric season ordering

  • Tracking by the whole season object is unstable with keyvalue (new entries are new object refs). Track by season.key to avoid DOM churn.
  • If season keys are numeric strings (e.g., "10", "2"), default keyvalue ordering is lexicographic. Provide a numeric comparator to prevent misordered seasons.

Apply this diff:

-  @if (!selectedSeason) {
-    @for (season of seasons | keyvalue; track season) {
+  @if (!selectedSeason) {
+    @for (season of (seasons | keyvalue: seasonNumberCompare); track season.key) {

Add in the component TS:

// In season-container.component.ts
readonly seasonNumberCompare = (a: {key: string|number}, b: {key: string|number}) =>
  Number(a.key) - Number(b.key);

13-15: Make the “Back to seasons” card accessible

mat-card isn’t a button. Add semantic/keyboard affordances or switch to a real button to satisfy a11y.

-    <mat-card class="episode-item" (click)="selectedSeason = undefined">
+    <mat-card class="episode-item" role="button" tabindex="0"
+              (click)="selectedSeason = undefined"
+              (keydown.enter)="selectedSeason = undefined"
+              (keydown.space)="selectedSeason = undefined; $event.preventDefault()">
       <div class="episode-title">< Back to seasons</div>
     </mat-card>

16-30: Track episodes by a stable id and harden null‑safety/a11y on image

  • track episode risks re-renders; track by a stable key (e.g., episode.id or episode_num).
  • Guard optional paths in @if to avoid runtime errors if info is missing.
  • Add alt and loading="lazy" to the image for a11y and perf.
-    @for (episode of seasons[selectedSeason]; track episode) {
+    @for (episode of seasons[selectedSeason]; track episode.id ?? episode.episode_num) {
       <mat-card
         class="episode-item"
         (click)="selectEpisode(episode)"
         >
-        @if (episode.info.movie_image) {
+        @if (episode?.info?.movie_image) {
           <mat-card-content
-            ><img [src]="episode.info.movie_image" class="episode-cover"
-          /></mat-card-content>
+            ><img [src]="episode?.info?.movie_image"
+                  class="episode-cover"
+                  loading="lazy"
+                  [alt]="episode?.title || ('Episode ' + episode?.episode_num)" /></mat-card-content>
         }
         <div class="episode-title">
           {{ episode.episode_num }}. {{ episode.title }}
         </div>
       </mat-card>
     }
src/app/stalker/stalker.store.ts (1)

88-96: Minor: prefer params() returning undefined to idle the resource.

Instead of checking currentPlaylist in the loader, compute params as undefined when playlist is missing; loader won’t run and status stays idle. This matches Angular’s resource docs. (angular.dev)

-            params: () => ({
-                contentType: store.selectedContentType(),
-                action: StalkerPortalActions.GetCategories,
-                currentPlaylist: store.currentPlaylist(),
-            }),
-            loader: async ({ params }) => {
-                if (params.currentPlaylist === undefined) return;
+            params: () => {
+                const cp = store.currentPlaylist();
+                if (!cp) return undefined;
+                return {
+                    contentType: store.selectedContentType(),
+                    action: StalkerPortalActions.GetCategories,
+                    currentPlaylist: cp,
+                };
+            },
+            loader: async ({ params }) => {
src/app/xtream-tauri/category-content-view/category-content-view.component.ts (1)

40-40: Namespace the page-size localStorage key to avoid cross-feature bleed.
category-content-view.component.ts sets 'xtream-page-size' (src/app/xtream-tauri/category-content-view/category-content-view.component.ts:58) and xtream.store.ts reads it for limit (src/app/xtream-tauri/xtream.store.ts:50); use distinct keys per API/mode (e.g., 'xtream:{apiId}:page-size' or 'stalker-page-size').

src/app/player/components/video-player/video-player.component.scss (1)

1-6: Ensure .main-container spans full height: Re-add top: 0 or specify height: 100% / 100vh for the absolute <mat-drawer-container class="main-container">; without it, the container will size to its content and may collapse.

src/app/home/home.component.ts (1)

86-90: Use undefined (or omit) the snack-bar action instead of null.

MatSnackBar.open’s second parameter is an optional string. Passing null can trip strict typing; use undefined or omit the arg. (v12.material.angular.io)

-    this.snackBar.open(message, null, {
+    this.snackBar.open(message, undefined, {
       duration,
     });
src/main.ts (1)

10-14: preserveWhitespaces here has no effect with AOT bootstrap.

With platformBrowser().bootstrapModule, preserveWhitespaces in CompilerOptions only affected JIT. Either remove it or configure whitespace at build time (tsconfig/angular.json) if needed.

 platformBrowser()
-    .bootstrapModule(AppModule, {
-        preserveWhitespaces: false,
-    })
+    .bootstrapModule(AppModule)
     .catch((err) => console.error(err));
src/app/xtream-tauri/account-info/account-info.component.ts (2)

25-139: Localize remaining static strings.

Most of the dialog content is static English. Consider piping these through translate for consistency with the Close button.

Example:

-<h2 mat-dialog-title>Account Information</h2>
+<h2 mat-dialog-title>{{ 'ACCOUNT_INFO.TITLE' | translate }}</h2>

170-195: Remove debug logs and harden date parsing.

Drop console.log calls and parse epoch strings with an explicit radix (or Number) to avoid NaN in edge cases.

-        console.log(playlist);
+        // noop
@@
-            console.log(this.accountInfo);
             if (this.accountInfo) {
-                this.formattedExpDate = new Date(
-                    parseInt(this.accountInfo.user_info.exp_date) * 1000
-                ).toLocaleDateString();
-                this.formattedCreatedDate = new Date(
-                    parseInt(this.accountInfo.user_info.created_at) * 1000
-                ).toLocaleDateString();
+                const exp = Number.parseInt(this.accountInfo.user_info.exp_date, 10);
+                const created = Number.parseInt(this.accountInfo.user_info.created_at, 10);
+                this.formattedExpDate = new Date(exp * 1000).toLocaleDateString();
+                this.formattedCreatedDate = new Date(created * 1000).toLocaleDateString();
             }
src/app/xtream-tauri/loading-overlay/loading-overlay.component.ts (1)

12-20: Prefer determinate when total > 0 (show 0% instead of indeterminate).

Rendering an indeterminate bar when current is 0 can feel jumpy. Gate determinate solely on total() > 0 and compute 0% naturally.

-                @if (current() !== 0 && total() !== 0) {
+                @if (total() > 0) {
                     <mat-progress-bar
                         mode="determinate"
                         [value]="(current() / total()) * 100"
                     />
                     <p>{{ current() }} / {{ total() }}</p>
src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html (3)

1-4: Avoid double async subscriptions; alias once.

Use @if (... | async; as processes) and iterate over processes to prevent re-subscribing and re-evaluating the observable. (angular.dev)

-@if ((activeProcesses$ | async)?.length) {
-  <div class="mpv-player-bar">
-    @for (process of activeProcesses$ | async; track process.id) {
+@if (activeProcesses$ | async; as processes) {
+  @if (processes?.length) {
+  <div class="mpv-player-bar">
+    @for (process of processes; track process.id) {

7-15: Move fallback image logic into a method to avoid direct DOM mutation in templates.

Inline assignment to $event.target.src is brittle. Delegate to a handler.

-            <img
+            <img
               [src]="process.thumbnail"
               [alt]="process.title"
               class="media-thumbnail"
-                        (error)="
-                            $event.target.src =
-                                './assets/images/default-poster.png'
-                        "
+              (error)="onImgError($event)"
             />

Add in the component TS:

onImgError(e: Event) {
  (e.target as HTMLImageElement).src = './assets/images/default-poster.png';
}

41-47: Localize the button title.

For consistency with the rest of the app, consider translating the Close tooltip.

-          title="Close"
+          [title]="'CLOSE' | translate"
src/app/xtream/serial-details/serial-details.component.html (2)

12-14: Add alt/decoding/lazy to cover image for a11y and perf.

-      <img [src]="item.info.cover" />
+      <img [src]="item.info.cover" [attr.alt]="item.info.name || 'cover'" decoding="async" loading="lazy" />

41-46: Prefer semantic containers over label elements.

<label> without a control hurts a11y. Use <p> or <div>.

Also applies to: 48-51, 54-60

src/app/player/components/audio-player/audio-player.component.ts (1)

137-139: Remove console log or gate behind env.

Leftover console.log in production code.

-        console.log(direction);
         this.store.dispatch(setAdjacentChannelAsActive({ direction }));
src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html (1)

10-16: Don’t render empty language row when lang is missing.

Move the <p data-test="lang"> inside the @if (epgProgram.title[0].lang) block.

-    @if (epgProgram.title[0].lang) {
-      <div class="subheading-2">
-        {{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
-      </div>
-    }
-    <p data-test="lang">{{ epgProgram.title[0].lang }}</p>
+    @if (epgProgram.title[0].lang) {
+      <div class="subheading-2">
+        {{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
+      </div>
+      <p data-test="lang">{{ epgProgram.title[0].lang }}</p>
+    }
src/app/player/components/video-player/video-player.component.html (2)

72-74: Add alt text to illustrative images.

-          <img src="./assets/images/custom-player.png" />
+          <img src="./assets/images/custom-player.png" alt="External player illustration" />

Also applies to: 92-93


21-26: Consider boolean coercion for radio flag.

If radio can be 'true' | 'false' | boolean, coerce once in the controller and use a boolean in the template.

src/app/xtream/navigation-bar/navigation-bar.component.html (2)

15-23: Track function stability.

Using a function call in track can create unnecessary churn. Prefer a stable key: track item.contentType or a unique id.

-      @for (
-        item of contentTypeNavigationItems; track trackByValue($index,
-        item)) {
+      @for (item of contentTypeNavigationItems; track item.contentType) {

93-99: Translate the search placeholder.

Hardcoded placeholder breaks i18n consistency.

-              placeholder="Search by name"
+              [placeholder]="'CHANNELS.SEARCH_BY_NAME' | translate"
package.json (1)

92-103: Align Angular packages between deps and devDeps.

Core Angular libs (@angular/core/common/router/...) are in devDependencies while others are in dependencies. For clarity and to avoid packaging pitfalls (Electron/Tauri), keep Angular runtime libs aligned (same major and group). Consider moving all @angular/* to devDependencies or dependencies consistently.

Also applies to: 37-49

src/app/settings/settings.component.html (9)

43-46: Remove stray matLine attributes from buttons.

matLine is for list items, not mat-icon-button; drop it to avoid unknown-attr noise.

-                <button
-                  matLine
+                <button
                   mat-icon-button
                   color="accent"
...
-                <button
-                  mat-icon-button
-                  matLine
+                <button
+                  mat-icon-button
                   color="accent"

Also applies to: 54-57


57-60: Fix tooltip text for “Remove EPG source”.

Tooltip reuses REFRESH_EPG; use a remove-specific key.

-                  [matTooltip]="'SETTINGS.REFRESH_EPG' | translate"
+                  [matTooltip]="'SETTINGS.REMOVE_EPG_SOURCE' | translate"

Please confirm the i18n key exists (or provide the correct one).


48-51: Harden disabled condition (trim whitespace).

Prevents “Refresh” enabled when input has only spaces.

-                  [disabled]="epgField.value === ''"
+                  [disabled]="!epgField.value?.trim()"

113-115: Remove unsupported “for” on mat-label.

mat-label doesn’t support “for”; rely on form-field semantics or aria-labelledby.

-            <mat-label for="mpvPlayerPath">{{ 'SETTINGS.MPV_PLAYER_PATH' | translate }}</mat-label>
+            <mat-label>{{ 'SETTINGS.MPV_PLAYER_PATH' | translate }}</mat-label>
...
-            <mat-label for="vlcPlayerPath">{{ 'SETTINGS.VLC_PLAYER_PATH' | translate }}</mat-label>
+            <mat-label>{{ 'SETTINGS.VLC_PLAYER_PATH' | translate }}</mat-label>

Also applies to: 136-138


92-99: Use stable, primitive track expressions.

Track by id/value to reduce DOM churn when arrays reallocate.

-            @for (player of players; track player) {
+            @for (player of players; track player.id) {
...
-            @for (streamFormat of streamFormatEnum | keyvalue; track streamFormat) {
+            @for (streamFormat of streamFormatEnum | keyvalue; track streamFormat.value) {
...
-            @for (language of languageEnum | keyvalue; track language) {
+            @for (language of languageEnum | keyvalue; track language.value) {
...
-              @for (theme of themeEnum | keyvalue; track theme) {
+              @for (theme of themeEnum | keyvalue; track theme.value) {

Also applies to: 164-170, 190-198, 218-226


42-52: Add aria-labels to icon buttons.

Improves a11y for screen readers.

-                <button
+                <button
                   mat-icon-button
                   color="accent"
                   [matTooltip]="'SETTINGS.REFRESH_EPG' | translate"
                   type="button"
                   [disabled]="!epgField.value?.trim()"
                   (click)="refreshEpg(epgUrl.value[i])"
+                  [attr.aria-label]="'SETTINGS.REFRESH_EPG' | translate"
                   >
...
-                <button
+                <button
                   mat-icon-button
                   color="accent"
-                  [matTooltip]="'SETTINGS.REFRESH_EPG' | translate"
+                  [matTooltip]="'SETTINGS.REMOVE_EPG_SOURCE' | translate"
                   type="button"
                   (click)="removeEpgSource(i)"
+                  [attr.aria-label]="'SETTINGS.REMOVE_EPG_SOURCE' | translate"
                   >

Also applies to: 53-62


157-159: Placeholders reference VIDEO_PLAYER key; use specific keys for each select.

Avoids confusing UX/i18n.

-          <mat-label>{{ 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate }}</mat-label>
+          <mat-label>{{ 'SETTINGS.STREAM_FORMAT_PLACEHOLDER' | translate }}</mat-label>
...
-          <mat-label>{{ 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate }}</mat-label>
+          <mat-label>{{ 'SETTINGS.LANGUAGE_PLACEHOLDER' | translate }}</mat-label>
...
-            <mat-label>{{ 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate }}</mat-label>
+            <mat-label>{{ 'SETTINGS.THEME_PLACEHOLDER' | translate }}</mat-label>

Please confirm these keys exist (or provide the intended ones).

Also applies to: 183-185, 211-213


277-283: Port input should be numeric with sensible bounds.

Tighten validation to avoid invalid ports.

-                <input
-                  matInput
-                  type="text"
+                <input
+                  matInput
+                  type="number"
+                  inputmode="numeric"
+                  min="1024"
+                  max="65535"
                   id="remoteControlPort"
                   formControlName="remoteControlPort"
                   />

355-357: Open external link safely in a new tab.

Add target and rel to prevent tab-napping.

-            &nbsp;<a href="https://github.com/4gray/iptvnator/releases">{{
+            &nbsp;<a href="https://github.com/4gray/iptvnator/releases" target="_blank" rel="noopener noreferrer">{{
src/app/shared/components/header/header.component.ts (1)

114-117: Avoid bare subscribe; add automatic teardown.

Prevents leaks on component destroy.

+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
...
-        this.sortService.getSortOptions().subscribe((options) => {
+        this.sortService
+            .getSortOptions()
+            .pipe(takeUntilDestroyed())
+            .subscribe((options) => {
             this.currentSortOptions = options;
-        });
+        });
src/app/player/components/multi-epg/multi-epg-container.component.ts (2)

213-221: Date parsing robustness.

new Date(dateStr) is format/locale-dependent; prefer date-fns parse with an explicit pattern and TZ, with a safe fallback.

// Example:
import { parse } from 'date-fns';
// parse '20250101123000 +0000' as 'yyyyMMddHHmmss xx'
const safeParse = (s: string) => parse(s, 'yyyyMMddHHmmss xx', new Date());

Apply in getCachedDate() and when creating start/stop dates.


138-140: Replace console.log with a logger or guard by env.

Noisy logs in production degrade UX.

if (!ngDevMode) { /* skip logs */ }
// or use an injected LoggerService with levels

Also applies to: 167-171, 181-189

src/app/app.component.ts (1)

49-49: Avoid any[] for modals; restore/define a typed shape.

Losing types weakens safety; either import the interface (as type-only) or define a local Modal-like type exposed by WhatsNewService.

-    modals: any[] = [];
+    // If ngx-whats-new exports a type:
+    // import type { ModalWindow } from 'ngx-whats-new';
+    // modals: ModalWindow[] = [];
+    // Otherwise define a minimal local shape:
+    type ModalItem = { title: string; content: string; [k: string]: unknown };
+    modals: ModalItem[] = [];
src/app/portals/web-player-view/web-player-view.component.ts (1)

37-40: Type cast on toSignal is too narrow.

settings emits asynchronously; allow undefined in the Signal type.

-    ) as Signal<Settings>;
+    ) as Signal<Settings | undefined>;
src/app/portals/web-player-view/web-player-view.component.html (1)

3-6: Remove redundant nested @if (player === 'html5').

Outer branch already guards html5; inner check is duplicate.

 } @else if (player === 'html5') {
-  @if (player === 'html5') {
-    <app-html-video-player [channel]="channel" />
-  }
+  <app-html-video-player [channel]="channel" />
 }
src/app/xtream/vod-details/vod-details.component.ts (1)

51-76: One-off read should complete subscription

If getPortalFavorites returns a long-lived stream, this leaks. Take a single emission.

Apply this diff:

-    this.playlistService
-        .getPortalFavorites(this.portalId)
-        .subscribe((favorites) => {
+    this.playlistService
+        .getPortalFavorites(this.portalId)
+        .pipe(take(1))
+        .subscribe((favorites) => {

Add import:

-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute } from '@angular/router';
+import { take } from 'rxjs';
src/app/services/whats-new.service.ts (3)

2-2: Restore explicit typing with type-only import (keeps bundler-friendly deep-import avoidance)

Commenting the type import and dropping return types weakens API guarantees.

Apply this diff:

-/* import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface'; */
+import type { ModalWindow } from 'ngx-whats-new';

If ngx-whats-new does not re-export ModalWindow, define a local minimal type used by your templates instead of deep-importing.


303-305: Add explicit return type

Keep method contract stable.

Apply this diff:

-    getModalsByVersion(version: string) {
+    getModalsByVersion(version: string): ModalWindow[] {
         return this.modals[version] || [];
     }

310-314: Add explicit return type

Same reasoning as above.

Apply this diff:

-    getLatestChanges() {
+    getLatestChanges(): ModalWindow[] {
         const modalsLength = Object.keys(this.modals).length;
         const lastVersion = Object.keys(this.modals)[modalsLength - 1];
         return this.modals[lastVersion];
     }
src/app/xtream/vod-details/vod-details.component.html (2)

13-18: Add alt text for poster image

Improve a11y and LCP hints.

Apply this diff:

-      <img
-        [src]="item.info?.movie_image"
+      <img
+        [src]="item.info?.movie_image"
+        [attr.alt]="item.info?.name || 'Poster'"
         (error)="
             $event.target.src = './assets/images/default-poster.png'
         "
         />

122-122: Deprecated attribute

frameborder is obsolete in HTML5.

Apply this diff:

-        frameborder="0"
src/app/home/recent-playlists/recent-playlists.component.html (1)

4-12: Add an explicit aria-label to the search input

Placeholder text isn’t a label; improve screen-reader support.

Apply this diff:

         <input
             matInput
             #searchQuery
             type="search"
             spellcheck="false"
             autocomplete="off"
             [placeholder]="'HOME.PLAYLISTS.SEARCH_PLAYLISTS' | translate"
+            [attr.aria-label]="'HOME.PLAYLISTS.SEARCH_PLAYLISTS' | translate"
             (input)="onSearchQueryUpdate(searchQuery.value)"
         />
src/app/player/components/multi-epg/multi-epg-container.component.html (1)

34-49: Hard-coded English labels in navigation buttons

Localize these strings for consistency.

Apply this diff:

-      <mat-icon>keyboard_arrow_up</mat-icon> Previous channels
+      <mat-icon>keyboard_arrow_up</mat-icon> {{ 'EPG.PREVIOUS_CHANNELS' | translate }}
...
-      <mat-icon>keyboard_arrow_down</mat-icon> Next channels
+      <mat-icon>keyboard_arrow_down</mat-icon> {{ 'EPG.NEXT_CHANNELS' | translate }}

And likewise replace matTooltip="Previous channels"/"Next channels" with translated keys.

src/app/shared/components/header/header.component.html (2)

73-75: Consider translating the GitHub label

All other items use i18n; keep this consistent if a key exists.

Apply this diff (assuming MENU.GITHUB exists):

-                        <span>GitHub</span>
+                        <span>{{ 'MENU.GITHUB' | translate }}</span>

189-234: Minor naming nit: opedAddPlaylistDialog

Method name has a typo and is used in multiple actions. Consider renaming to openAddPlaylistDialog and updating call sites.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe40a7 and 773faee.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (44)
  • angular.json (1 hunks)
  • package.json (1 hunks)
  • src/app/app.component.html (1 hunks)
  • src/app/app.component.ts (1 hunks)
  • src/app/home/home.component.html (1 hunks)
  • src/app/home/home.component.ts (2 hunks)
  • src/app/home/recent-playlists/recent-playlists.component.html (1 hunks)
  • src/app/home/recent-playlists/recent-playlists.component.ts (2 hunks)
  • src/app/player/components/art-player/art-player.component.ts (2 hunks)
  • src/app/player/components/audio-player/audio-player.component.ts (2 hunks)
  • src/app/player/components/d-player/d-player.component.ts (2 hunks)
  • src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html (1 hunks)
  • src/app/player/components/epg-list/epg-item-description/epg-item-description.component.ts (2 hunks)
  • src/app/player/components/multi-epg/multi-epg-container.component.html (1 hunks)
  • src/app/player/components/multi-epg/multi-epg-container.component.ts (1 hunks)
  • src/app/player/components/video-player/video-player.component.html (2 hunks)
  • src/app/player/components/video-player/video-player.component.scss (1 hunks)
  • src/app/portals/web-player-view/web-player-view.component.html (1 hunks)
  • src/app/portals/web-player-view/web-player-view.component.ts (2 hunks)
  • src/app/services/whats-new.service.ts (2 hunks)
  • src/app/settings/settings.component.html (1 hunks)
  • src/app/shared/components/header/header.component.html (1 hunks)
  • src/app/shared/components/header/header.component.scss (1 hunks)
  • src/app/shared/components/header/header.component.ts (1 hunks)
  • src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html (1 hunks)
  • src/app/stalker/recently-viewed/recently-viewed.component.ts (1 hunks)
  • src/app/stalker/stalker-favorites/stalker-favorites.component.ts (1 hunks)
  • src/app/stalker/stalker-search/stalker-search.component.ts (2 hunks)
  • src/app/stalker/stalker.store.ts (6 hunks)
  • src/app/xtream-tauri/account-info/account-info.component.ts (2 hunks)
  • src/app/xtream-tauri/category-content-view/category-content-view.component.ts (1 hunks)
  • src/app/xtream-tauri/loading-overlay/loading-overlay.component.ts (1 hunks)
  • src/app/xtream-tauri/player-dialog/player-dialog.component.scss (2 hunks)
  • src/app/xtream/navigation-bar/navigation-bar.component.html (1 hunks)
  • src/app/xtream/navigation-bar/navigation-bar.component.ts (2 hunks)
  • src/app/xtream/season-container/season-container.component.html (1 hunks)
  • src/app/xtream/season-container/season-container.component.ts (2 hunks)
  • src/app/xtream/serial-details/serial-details.component.html (1 hunks)
  • src/app/xtream/serial-details/serial-details.component.ts (2 hunks)
  • src/app/xtream/vod-details/vod-details.component.html (2 hunks)
  • src/app/xtream/vod-details/vod-details.component.ts (2 hunks)
  • src/app/xtream/xtream-main-container.component.ts (3 hunks)
  • src/main.ts (1 hunks)
  • tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main.ts (4)
src/environments/environment.dev.ts (1)
  • AppConfig (6-11)
src/environments/environment.prod.ts (1)
  • AppConfig (1-6)
src/environments/environment.web.ts (1)
  • AppConfig (1-6)
src/environments/environment.ts (1)
  • AppConfig (1-6)
src/app/stalker/stalker-search/stalker-search.component.ts (1)
src/app/stalker/stalker-content-types.ts (1)
  • StalkerContentTypes (3-32)
src/app/stalker/stalker.store.ts (2)
shared/ipc-commands.ts (1)
  • STALKER_REQUEST (55-55)
src/app/stalker/stalker-content-types.ts (1)
  • StalkerContentTypes (3-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: e2e-tests (macos-latest)
🔇 Additional comments (29)
src/app/xtream-tauri/player-dialog/player-dialog.component.scss (1)

22-22: EOF newline/hygiene looks good

No functional change; keeping a trailing newline is a good practice.

src/app/stalker/stalker.store.ts (1)

83-148: API shape looks correct for Angular 20 resources (params/loader).

The migration from request→params and loader({params}) aligns with current Angular docs. No issues on the new API surface. (angular.dev)

Also applies to: 149-156

src/app/stalker/stalker-search/stalker-search.component.ts (1)

91-98: API migration to params/loader is correct for Angular 20 resources.

The new shape matches Angular’s resource API. (angular.dev)

src/app/stalker/recently-viewed/recently-viewed.component.ts (1)

47-51: Good migration to rxResource params/stream.

This matches the current rxResource API; using params to trigger refresh via refreshTimestamp is idiomatic.

(angular.dev)

src/app/stalker/stalker-favorites/stalker-favorites.component.ts (1)

47-51: Good migration to rxResource params/stream.

API usage looks correct; refreshTimestamp drives re-subscription.

(angular.dev)

src/app/shared/components/header/header.component.scss (1)

60-62: LGTM!

The styling change from translucent black to solid dark gray for the active sort state aligns with Angular 20's modern control flow syntax migration. The updated color ensures better visual contrast and opaque appearance in the new UI.

tsconfig.json (1)

9-9: Appropriate upgrade to bundler module resolution.

The change from "node" to "bundler" module resolution aligns with the broader TypeScript 5.9 upgrade and improves compatibility with modern build tools. This configuration supports Angular 20's build optimizations.

src/app/player/components/d-player/d-player.component.ts (1)

15-16: LGTM!

The removal of CommonModule import and setting imports to an empty array is consistent with the Angular 20 migration pattern observed across multiple components. Since this component uses an inline template without structural directives, no additional changes are needed.

src/app/player/components/art-player/art-player.component.ts (1)

15-17: LGTM!

The removal of CommonModule import aligns with the new control flow syntax that reduces runtime footprint and improves bundle size. This component's inline template doesn't use structural directives, so the change is safe.

src/app/home/home.component.html (1)

7-9: LGTM! Clean migration to new control flow syntax.

The migration from *ngIf="isLoading" to @if (isLoading) follows Angular 20's new control flow syntax. This provides more ergonomic and intuitive syntax that's closer to JavaScript.

src/app/xtream/season-container/season-container.component.ts (1)

1-10: Confirmed: template uses Angular 20 control-flow (@if/@for)
src/app/xtream/season-container/season-container.component.html contains @if/@for (lines 3, 4, 16, 21) and no *ngIf/*ngFor.

src/app/xtream/navigation-bar/navigation-bar.component.ts (1)

46-58: Confirmed: template updated to Angular 20 control-flow (@if/@for). src/app/xtream/navigation-bar/navigation-bar.component.html uses @if/@for (lines 7,15,49,54,64,76,88 from rg output); no *ngIf/*ngFor were found.

src/main.ts (1)

2-2: Good move: platformBrowser over platformBrowserDynamic.

platformBrowserDynamic is deprecated in favor of platformBrowser; this aligns with Angular’s guidance. If you still rely on JIT in non-CLI contexts, ensure @angular/compiler is present (unlikely here). (angular.dev)

src/app/xtream-tauri/account-info/account-info.component.ts (1)

140-140: styleUrl usage is fine on Angular 17+.

Using styleUrl (singular) is supported since Angular 17; keep as-is. (blog.ninja-squad.com)

Confirm the project is actually building against Angular ≥17 (this PR targets v20), otherwise switch to styleUrls.

src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html (1)

6-10: Confirm the type of epgProgram.title; likely need the first item’s value.

If title is an array, rendering {{ epgProgram.title }} will print [object Object]. Consider title[0]?.value (or model’s actual field).

[suggest_minor_issue]

src/app/player/components/epg-list/epg-item-description/epg-item-description.component.ts (1)

12-13: Imports tidy-up looks good.

Removal of NgIf import aligns with new control flow syntax.

angular.json (1)

21-43: Config LGTM — serviceWorker set to "ngsw-config.json"; local build required to verify.

jq returned "ngsw-config.json"; sandbox build failed with "sh: 1: ng: not found". Run locally (npm run build:prod or ng build -c production) and confirm the build succeeds and the SW config is picked up.

package.json (1)

86-104: Verify builder/test toolchain compatibility with Angular 20

  • Findings — @angular-builders/jest v17.0.0 confirmed; npm ci emitted no peerDependency warnings but showed multiple deprecation notices and 23 vulnerabilities; jq failed to enumerate @angular/* from package.json during verification.
  • Action — Inspect package.json (lines 86–104) to confirm TypeScript (expect TS 5.9) and ESLint plugin versions, run the full test suite/CI, and if tests or peer warnings appear, upgrade or pin @angular-builders/jest / ESLint / TS to releases known to be compatible with Angular 20.
src/app/shared/components/header/header.component.ts (1)

1-1: LGTM: dropping NgIf import aligns with @if blocks.

No issues spotted; AsyncPipe remains available.

src/app/portals/web-player-view/web-player-view.component.ts (1)

25-29: LGTM: imports narrowed to player components.

Matches template’s @if branches.

src/app/xtream/serial-details/serial-details.component.ts (1)

21-25: LGTM: removed NgIf from imports.

Template should now rely on @if blocks.

src/app/app.component.html (1)

3-9: *LGTM: migrated ngIf to @if for ngx-whats-new.

Bindings unchanged; looks correct.

src/app/xtream/vod-details/vod-details.component.ts (2)

24-29: NgIf removal in standalone imports looks correct

Template has migrated to the new control-flow; dropping NgIf from imports is appropriate.


80-83: ID type for removeFromFavorites

removeFromFavoritesClicked is EventEmitter<number>, but fallback (this.item as any)?.id could be non-numeric in stalker mode. Please verify types end-to-end.

src/app/xtream/xtream-main-container.component.ts (3)

111-111: Trailing comma in imports

No behavioral change; fine.


360-364: Dialog bounds for VOD player

Adding maxWidth/maxHeight is a good UX guard on large screens.


390-392: Dialog bounds for episode player

Same here—sensible limits.

src/app/home/recent-playlists/recent-playlists.component.ts (2)

2-2: CommonModule structural directives removal is fine

Template migrated to new control-flow; keeping only AsyncPipe here is correct.


67-70: Readonly BehaviorSubject + skeleton source look good

Marking searchQuery readonly prevents accidental reassignment; ghostElements enables cheap placeholder loops.

Comment on lines +149 to +156
params: () => ({
contentType: store.selectedContentType(),
category: store.selectedCategoryId(),
action: StalkerPortalActions.GetOrderedList,
search: store.searchPhrase(),
pageIndex: store.page() + 1,
}),
loader: async ({ request }) => {
loader: async ({ params }) => {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing reactivity/guard for currentPlaylist in getContentResource.

The loader reads currentPlaylist() but it's not part of params; it won’t rerun when playlist changes and may crash when undefined. Include playlist in params and use it in the loader.

-            params: () => ({
-                contentType: store.selectedContentType(),
-                category: store.selectedCategoryId(),
-                action: StalkerPortalActions.GetOrderedList,
-                search: store.searchPhrase(),
-                pageIndex: store.page() + 1,
-            }),
+            params: () => {
+                const playlist = store.currentPlaylist();
+                const category = store.selectedCategoryId();
+                if (!playlist || !category) return undefined;
+                return {
+                    contentType: store.selectedContentType(),
+                    category,
+                    action: StalkerPortalActions.GetOrderedList,
+                    search: store.searchPhrase(),
+                    pageIndex: store.page() + 1,
+                    currentPlaylist: playlist,
+                };
+            },
@@
-                        url: currentPlaylist().portalUrl,
-                        macAddress: currentPlaylist().macAddress,
+                        url: params.currentPlaylist.portalUrl,
+                        macAddress: params.currentPlaylist.macAddress,

Also applies to: 180-183

🤖 Prompt for AI Agents
In src/app/stalker/stalker.store.ts around lines 149-156 (and similarly
180-183), the loader reads currentPlaylist() but that value is not included in
the params so the loader won’t rerun when the playlist changes and may access
undefined; add currentPlaylist (or currentPlaylistId) to the params function so
the loader is reactive to playlist changes, and update the loader to guard
against undefined (e.g., return early or throw a controlled error if
params.playlist is missing) before using the playlist value.

Comment on lines +224 to 233
params: () => ({
itemId: store.selectedSerialId(),
}),
loader: async ({ request }) => {
loader: async ({ params }) => {
const { portalUrl, macAddress } = store.currentPlaylist();
const params = {
const queryParams = {
action: StalkerContentTypes.series.getContentAction,
type: 'series',
movie_id: request.itemId,
movie_id: params.itemId,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

serialSeasonsResource: add playlist to params and guard response.

Ensures reactivity to playlist changes and avoids NPEs.

-            params: () => ({
-                itemId: store.selectedSerialId(),
-            }),
-            loader: async ({ params }) => {
-                const { portalUrl, macAddress } = store.currentPlaylist();
+            params: () => {
+                const playlist = store.currentPlaylist();
+                const itemId = store.selectedSerialId();
+                if (!playlist || !itemId) return undefined;
+                return { itemId, currentPlaylist: playlist };
+            },
+            loader: async ({ params }) => {
+                const { portalUrl, macAddress } = params.currentPlaylist;
                 const queryParams = {
                     action: StalkerContentTypes.series.getContentAction,
                     type: 'series',
-                    movie_id: params.itemId,
+                    movie_id: params.itemId,
                 };
                 const response = await dataService.sendIpcEvent(
                     STALKER_REQUEST,
                     {
                         url: portalUrl,
                         macAddress,
                         params: queryParams,
                     }
                 );
-                return sortByNumericValue(response.js.data);
+                if (!response) throw new Error('STALKER_REQUEST returned no response for seasons');
+                return sortByNumericValue(response.js?.data ?? []);

Also applies to: 237-243

🤖 Prompt for AI Agents
In src/app/stalker/stalker.store.ts around lines 224-233 (and also apply same
fix to 237-243), the serialSeasonsResource params only include itemId and the
loader assumes currentPlaylist exists; update params to include the whole
currentPlaylist (or at least portalUrl and macAddress) so the resource reacts to
playlist changes, and in the loader guard the response by checking
currentPlaylist is defined before using its properties and validating the
fetched response (e.g., ensure data exists and has expected shape) before
returning — if missing, return a safe default (empty array/object) or throw a
handled error to avoid NPEs.

Comment on lines +11 to +60
<div class="image">
@if (item.info.cover) {
<img [src]="item.info.cover" />
}
</div>
<div class="details">
<h2>{{ item.info.name }}</h2>
@if (item.info.plot) {
<label>
{{ item.info.plot }}
</label>
}
@if (item.info.releaseDate) {
<label>
<div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div>
{{ item.info.releaseDate }}
</label>
}
@if (item.info.genre) {
<label>
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
{{ item.info.genre }}
</label>
}
@if (item.info.rating) {
<label>
<div class="label">{{ 'XTREAM.RATING' | translate }}:</div>
{{ item.info.rating }}
</label>
}
@if (item.info.cast) {
<div>
<div class="label">{{ 'XTREAM.CAST' | translate }}:</div>
{{ item.info.cast }}
</div>
}
@if (item.info.director) {
<label>
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
{{ item.info.director }}
</label>
}
@if (item.info.episode_run_time) {
<label>
<div class="label">
{{ 'XTREAM.EPISODE_RUN_TIME' | translate }}:
</div>
<label *ngIf="item.info.director">
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
{{ item.info.director }}
</label>
<label *ngIf="item.info.episode_run_time">
<div class="label">
{{ 'XTREAM.EPISODE_RUN_TIME' | translate }}:
</div>
{{ item.info.episode_run_time }}
</label>
<div class="action-buttons">
<button
*ngIf="!isFavorite; else removeFromFavoritesButton"
mat-stroked-button
color="accent"
(click)="toggleFavorite()"
>
<mat-icon>star_outline</mat-icon>
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
</button>
{{ item.info.episode_run_time }}
</label>
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against undefined item/info before dereferencing.

Multiple expressions use item.info.* without a parent guard. Wrap the image/details/actions in a single @if (item?.info) (or as info) block to prevent runtime errors when item or item.info is null.

Apply:

-  <div class="image">
-    @if (item.info.cover) {
+  @if (item?.info) {
+  <div class="image">
+    @if (item.info.cover) {
       <img [src]="item.info.cover" />
     }
   </div>
   <div class="details">
     <h2>{{ item.info.name }}</h2>
     ...
-    </div>
-  </div>
+    </div>
+  </div>
+  }

Also applies to: 82-84

🤖 Prompt for AI Agents
In src/app/xtream/serial-details/serial-details.component.html around lines 11
to 60 (and also apply the same fix at lines 82-84), the template dereferences
item.info properties directly which can throw if item or item.info is undefined;
wrap the sections that access item.info (the image, details and related actions)
in a single parent guard such as @if (item?.info) { ... } or use @if (item?.info
as info) { } and then replace inner references with info.* so all accesses are
safe and the runtime errors are prevented.

- Removed unnecessary imports from header, account-info, 
  loading-overlay, navigation-bar, season-container, serial-details, and 
  vod-details components.
- Updated template syntax to use structural directives more efficiently in mpv-player-bar, account-info, navigation-bar, season-container, serial-details, and vod-details components.
- Enhanced readability and maintainability of the code by organizing imports and using Angular's async pipe and structural directives appropriately.
- Adjusted TypeScript configuration to use "bundler" for module resolution for better compatibility with modern build tools.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/app/player/components/art-player/art-player.component.ts (3)

108-114: MKV playback breaks: type must match customType key.

getVideoType() returns video/matroska, but customType key is mkv. Artplayer uses string match, so MKV won’t trigger the handler.

Apply:

@@
     private getVideoType(url: string): string {
         const extension = url.split('.').pop()?.toLowerCase();
         switch (extension) {
             case 'mkv':
-                return 'video/matroska'; // Changed from 'mkv'
+                return 'mkv';
             case 'm3u8':
                 return 'm3u8';
             case 'mp4':
                 return 'mp4';
             default:
                 return 'auto';
         }
     }

Also applies to: 84-94


84-94: Hls.js instance is leaked; also ensure proper this binding.

const hls = new Hls() isn’t destroyed on channel changes or destroy; use a field and tear it down. Also switch to arrow function to preserve lexical this.

@@
-export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
+export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
@@
-    private player: Artplayer;
+    private player?: Artplayer;
+    private hls?: Hls;
@@
-    ngOnDestroy(): void {
-        if (this.player) {
-            this.player.destroy();
-        }
-    }
+    ngOnDestroy(): void {
+        this.hls?.destroy();
+        this.hls = undefined;
+        if (this.player) {
+            this.player.destroy();
+        }
+    }
@@
-    ngOnChanges(changes: SimpleChanges): void {
-        if (changes['channel'] && !changes['channel'].firstChange) {
-            if (this.player) {
-                this.player.destroy();
-            }
-            this.initPlayer();
-        }
-    }
+    ngOnChanges(changes: SimpleChanges): void {
+        if (changes['channel'] && !changes['channel'].firstChange) {
+            this.hls?.destroy();
+            this.hls = undefined;
+            if (this.player) {
+                this.player.destroy();
+            }
+            this.initPlayer();
+        }
+    }
@@
-            customType: {
-                m3u8: function (video: HTMLVideoElement, url: string) {
-                    if (Hls.isSupported()) {
-                        const hls = new Hls();
-                        hls.loadSource(url);
-                        hls.attachMedia(video);
-                    } else if (
-                        video.canPlayType('application/vnd.apple.mpegurl')
-                    ) {
-                        video.src = url;
-                    }
-                },
+            customType: {
+                m3u8: (video: HTMLVideoElement, url: string) => {
+                    if (Hls.isSupported()) {
+                        this.hls?.destroy();
+                        this.hls = new Hls();
+                        this.hls.loadSource(url);
+                        this.hls.attachMedia(video);
+                    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
+                        video.src = url;
+                    }
+                },

Also applies to: 46-50, 52-59, 38-41


95-103: MKV error handler can cause retry loops.

Reassigning video.src in onerror to the same URL can loop. Log once and don’t reassign.

-                mkv: function (video: HTMLVideoElement, url: string) {
-                    video.src = url;
-                    // Add error handling
-                    video.onerror = () => {
-                        console.error('Error loading MKV file:', video.error);
-                        // Fallback to treating it as a regular video
-                        video.src = url;
-                    };
-                },
+                mkv: (video: HTMLVideoElement, url: string) => {
+                    video.onerror = () => {
+                        console.error('Error loading MKV file:', video.error);
+                    };
+                    video.src = url;
+                },
src/app/stalker/stalker.store.ts (2)

96-112: Stale categories after playlist change (cache not keyed by playlist).

Early returns reuse old categories even when params.currentPlaylist changes, serving stale data. Either remove the early-return cache or key caches by playlist.

Apply this diff to drop the stale cache:

-                switch (params.contentType) {
-                    case 'itv':
-                        if (store.itvCategories().length > 0) {
-                            return store.itvCategories();
-                        }
-                        break;
-                    case 'vod':
-                        if (store.vodCategories().length > 0) {
-                            return store.vodCategories();
-                        }
-                        break;
-                    case 'series':
-                        if (store.seriesCategories().length > 0) {
-                            return store.seriesCategories();
-                        }
-                        break;
-                }
+                // Always fetch categories for the current playlist/content type to avoid stale cache.

Optional (outside this hunk): reset caches when playlist changes:

@@
             setCurrentPlaylist(playlist: PlaylistMeta | undefined) {
                 patchState(store, { currentPlaylist: playlist });
+                // Reset per‑type caches to avoid cross‑playlist leakage.
+                patchState(store, {
+                    vodCategories: [],
+                    seriesCategories: [],
+                    itvCategories: [],
+                    itvChannels: [],
+                    page: 0,
+                    totalCount: 0,
+                });
             },

141-145: Error path dereferences a falsy response.

Else branch tries to read response.message/status when response is falsy, causing a new error.

Apply this diff:

-                } else {
-                    throw new Error(
-                        `Error: ${response.message} (Status: ${response.status})`
-                    );
-                }
+                } else {
+                    const message = response?.message ?? 'No response';
+                    const status = response?.status ? ` (Status: ${response.status})` : '';
+                    throw new Error(`Error: ${message}${status}`);
+                }
src-tauri/src/commands/media.rs (1)

131-143: Use wait() instead of wait_with_output() to avoid error when stdio isn’t piped.

wait_with_output() requires stdout/stderr to be piped; here they’re not configured, so this can error and skip exit-status handling. Use wait() and check the ExitStatus.

-        let status = child.wait_with_output();
+        let status = child.wait();
@@
-            if let Ok(output) = status {
-                if !output.status.success() {
-                    let error_msg = format!("MPV exited with error code: {}", output.status);
+            if let Ok(exit_status) = status {
+                if !exit_status.success() {
+                    let error_msg = format!("MPV exited with error code: {}", exit_status);
                     let _ = app_handle_clone.emit("player-error", error_msg);
                 }
             }
🧹 Nitpick comments (26)
src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html (3)

1-3: Avoid double subscription to activeProcesses$; capture once with as and reuse.

Current template subscribes twice (in @if and @for). Capture the stream once and iterate over the local to prevent duplicate subscriptions and microtasks.

Apply this diff:

-@if ((activeProcesses$ | async)?.length) {
-  <div class="mpv-player-bar">
-    @for (process of activeProcesses$ | async; track process.id) {
+@if (activeProcesses$ | async; as processes) {
+  @if (processes?.length) {
+    <div class="mpv-player-bar">
+      @for (process of processes; track process.id) {
@@
-  }
-</div>
+    }
+  </div>
+  }
 }

Also applies to: 50-52


7-15: Make fallback image path robust and enable lazy loading.

Use “assets/...” (no leading ./) to avoid route-relative resolution issues; add loading="lazy" for perf.

Apply this diff:

             <img
               [src]="process.thumbnail"
-              [alt]="process.title"
+              [attr.alt]="process.title || 'Stream thumbnail'"
+              loading="lazy"
               class="media-thumbnail"
-                        (error)="
-                            $event.target.src =
-                                './assets/images/default-poster.png'
-                        "
+              (error)="($event.target as HTMLImageElement).src = 'assets/images/default-poster.png'"
               />

27-40: Clean up commented-out controls or gate behind a feature flag.

Long commented blocks tend to linger and confuse. Remove or guard via a flag.

Example minimal cleanup:

-          <!-- <button
-          mat-icon-button
-          (click)="pauseStream(process.id)"
-          title="Pause"
-          >
-          <mat-icon>pause</mat-icon>
-        </button>
-        <button
-          mat-icon-button
-          (click)="playStream(process.id)"
-          title="Play"
-          >
-          <mat-icon>play_arrow</mat-icon>
-        </button> -->
+          <!-- TODO: Re-enable pause/play when backend supports stream control -->
src/app/shared/components/header/header.component.html (4)

11-19: Add aria-labels to icon buttons for a11y parity with tooltips

Tooltips don’t serve screen readers; mirror labels via aria.

 <button
   mat-icon-button
   class="add-playlist-btn"
   [matMenuTriggerFor]="addPlaylistMenu"
   [matTooltip]="'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate"
+  [attr.aria-label]="'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate"
   data-test-id="add-playlist"
 >

 <button
   mat-icon-button
   [matMenuTriggerFor]="sortMenu"
   [matTooltip]="'HOME.SORT_PLAYLISTS' | translate"
+  [attr.aria-label]="'HOME.SORT_PLAYLISTS' | translate"
   data-test-id="sort-playlists"
 >

 <button
   mat-icon-button
   [matMenuTriggerFor]="filterPlaylistMenu"
   [matTooltip]="'HOME.FILTER_BY_TYPE' | translate"
+  [attr.aria-label]="'HOME.FILTER_BY_TYPE' | translate"
   data-test-id="filter-playlist-by-type"
 >

Also applies to: 21-28, 29-36


56-67: Redundant isHome guard inside Home‑only block

This menu is already under @if (isHome). Remove the inner guard for simplicity.

-@if (isHome) {
   <button
     mat-menu-item
     [attr.aria-label]="'MENU.SETTINGS_ARIA' | translate"
     (click)="openSettings()"
     data-test-id="pwa-open-settings"
   >
     <mat-icon>settings</mat-icon>
     <span>{{ 'MENU.SETTINGS' | translate }}</span>
   </button>
   <mat-divider></mat-divider>
-}

3-3: Add alt text to logo image

Improves accessibility and Lighthouse scores.

-<img src="./assets/icons/icon-tv-256.png" height="100" />
+<img src="./assets/icons/icon-tv-256.png" height="100" alt="IPTVnator logo" />

175-175: Prefer stable trackBy over $index

Using $index harms DOM reuse on reordering. Track by a stable key (e.g., type.id or type.value).

-@for (type of playlistTypes; track $index) {
+@for (type of playlistTypes; track type.id) {

If no id exists, add one or track by a unique property.

src/app/home/recent-playlists/recent-playlists.component.html (3)

42-51: Remove unused loop variable; verify track key stability.

let last = $last is unused; drop it. Also ensure item._id is unique and stable; otherwise consider a safer track key.

-                    @for (item of playlists; track item._id; let last = $last) {
+                    @for (item of playlists; track item._id) {

57-74: Rename unused loop variable for clarity.

a isn’t used; underscore makes intent explicit.

-            @for (a of ghostElements; track $index) {
+            @for (_ of ghostElements; track $index) {

1-13: Consider debouncing search input to reduce churn on large lists.

onSearchQueryUpdate fires every keystroke; if it triggers heavy work, debounce in the component.

src/app/player/components/multi-epg/multi-epg-container.component.html (4)

38-49: Localize “Previous/Next channels” labels for consistency.

Other labels use i18n keys. Use translate pipe for these too.

-      [disabled]="channelsLowerRange === 0"
-      matTooltip="Previous channels"
+      [disabled]="channelsLowerRange === 0"
+      [matTooltip]="'EPG.PREVIOUS_CHANNELS' | translate"
       >
-      <mat-icon>keyboard_arrow_up</mat-icon> Previous channels
+      <mat-icon>keyboard_arrow_up</mat-icon> {{ 'EPG.PREVIOUS_CHANNELS' | translate }}
...
-      [disabled]="isLastPage"
-      matTooltip="Next channels"
+      [disabled]="isLastPage"
+      [matTooltip]="'EPG.NEXT_CHANNELS' | translate"
       >
-      <mat-icon>keyboard_arrow_down</mat-icon> Next channels
+      <mat-icon>keyboard_arrow_down</mat-icon> {{ 'EPG.NEXT_CHANNELS' | translate }}

Please confirm the keys exist in your translation files.


54-55: Track expression must be stable and unique; also avoid multiple async subscriptions.

  • Ensure $any(item).channel is globally unique and stable across pagination; prefer an immutable id if available (e.g., item.id).
  • Using channels$ | async multiple times creates multiple subscriptions if channels$ is cold. Consider hoisting once with @let channels = (channels$ | async) ?? []; and iterate channels.

Example:

-    @for (item of channels$ | async; track $any(item).channel; let i = $index) {
+    @let channels = (channels$ | async) ?? [];
+    @for (item of channels; track item.id ?? $any(item).channel; let i = $index) {

70-76: Lazy-load channel icons.

Small perf win and less layout jank.

-                <img
+                <img
+                  loading="lazy"
+                  decoding="async"
                   [src]="item.icon"
                   [alt]="item.name"
                   style="height: 20px; width: 20px; margin-right: 8px;"
                   >

111-121: Prefer stable property for tracking over calling a function each iteration.

track trackByProgram($index, program) recomputes per cycle. If programs have a stable id, track that directly.

-          @for (program of item.programs; track trackByProgram($index, program)) {
+          @for (program of item.programs; track program.id ?? program.uid) {

If no id exists, keep the function but ensure it’s pure and stable.

src/app/player/components/art-player/art-player.component.ts (3)

61-70: Null-safety and typings: guard against missing channel and type ElementRef.

Avoid NPEs if channel isn’t yet set; tighten typings.

-    constructor(private elementRef: ElementRef) {}
+    constructor(private elementRef: ElementRef<HTMLElement>) {}
@@
-    private initPlayer(): void {
+    private initPlayer(): void {
+        if (!this.channel?.url) {
+            return;
+        }

Also applies to: 40-41


65-73: isLive heuristic is brittle.

includes('m3u8') marks all HLS as live (also VOD). Prefer an explicit flag from Channel (e.g., channel.isLive) if available.


36-37: Unused input.

showCaptions isn’t used. Remove or implement before release.

package.json (3)

40-41: Align Angular minors to avoid subtle incompatibilities.

Bump CDK/Material to match core 20.3.1.

-    "@angular/cdk": "20.2.4",
-    "@angular/material": "20.2.4",
+    "@angular/cdk": "20.3.1",
+    "@angular/material": "20.3.1",

38-38: jest builder should be a devDependency.

Move @angular-builders/jest under devDependencies; it’s only needed for tooling.

@@ "dependencies": {
-    "@angular-builders/jest": "17.0.0",
@@ "devDependencies": {
+    "@angular-builders/jest": "17.0.0",

83-83: Loosen zone.js pin to receive patch fixes.

Use caret to pick up 0.15.x patches automatically.

-    "zone.js": "~0.15.0"
+    "zone.js": "^0.15.0"
src/app/stalker/stalker.store.ts (1)

196-213: Nit: totalLoaded computation relies on immediate signal read.

Use explicit math to avoid relying on synchronous signal updates.

See updated computation in the loader diff above (totalLoaded = params.pageIndex === 1 ? newItems.length : store.itvChannels().length + newItems.length).

src/app/xtream/vod-details/vod-details.component.html (4)

13-18: Add alt and loading hints to the poster image.

Improve a11y and performance by providing descriptive alt text and lazy loading.

-      <img
-        [src]="item.info?.movie_image"
+      <img
+        [src]="item.info?.movie_image"
+        [attr.alt]="item.info?.name || 'Poster'"
+        loading="lazy"
+        decoding="async"

26-29: Don’t use for non-form content.

<label> is for form controls; use semantic containers (<div>/<p>) to avoid a11y/semantics issues.

-      <label>
+      <div>
         {{ item.info?.description }}
-      </label>
+      </div>

-      <label>
+      <div>
         <div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div>
         {{ item.info?.releasedate }}
-      </label>
+      </div>

-      <label>
+      <div>
         <div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
         {{ item.info?.genre }}
-      </label>
+      </div>

-      <label>
+      <div>
         <div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
         {{ item.info?.country }}
-      </label>
+      </div>

-      <label>
+      <div>
         <div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
         {{ item.info?.director }}
-      </label>
+      </div>

-      <label>
+      <div>
         <div class="label">{{ 'XTREAM.DURATION' | translate }}:</div>
         {{ item.info?.duration }}
-      </label>
+      </div>

-      <label>
+      <div>
         <div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div>
         {{ item.info?.rating_imdb }}
-      </label>
+      </div>

-      <label>
+      <div>
         <div class="label">
           {{ 'XTREAM.KINOPOISK_RATING' | translate }}:
         </div>
         {{ item.info?.rating_kinopoisk }}
-      </label>
+      </div>

Also applies to: 31-35, 37-41, 43-47, 55-59, 61-65, 67-71, 73-79


80-89: Replace &nbsp; with layout spacing.

Use flex gap instead of a non-breaking space for spacing between buttons.

-    <div class="action-buttons">
+    <div class="action-buttons" style="display: flex; gap: 8px;">
...
-        {{ 'XTREAM.PLAY' | translate }}</button
-        >&nbsp;
+        {{ 'XTREAM.PLAY' | translate }}</button>

If you prefer CSS, add display:flex; gap:8px; in the stylesheet for .action-buttons instead of inline style.


115-126: Make iframe title descriptive and drop obsolete attributes.

Improve a11y; frameborder is obsolete in HTML5.

-        title="YouTube video player"
-        frameborder="0"
+        [attr.title]="'YouTube trailer — ' + (item.info?.name || '')"
src-tauri/src/commands/media.rs (1)

266-276: Avoid logging full command with sensitive URL/headers at info level.

URLs and header args can carry tokens. Prefer debug level (or redact query/headers) to reduce accidental leakage in production logs.

-    info!("Complete VLC command: {}", command_str);
+    log::debug!("Complete VLC command: {}", command_str);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 773faee and cd2dd2e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (46)
  • angular.json (1 hunks)
  • package.json (1 hunks)
  • src-tauri/src/commands/media.rs (1 hunks)
  • src/app/app.component.html (1 hunks)
  • src/app/app.component.ts (1 hunks)
  • src/app/home/home.component.html (1 hunks)
  • src/app/home/home.component.ts (2 hunks)
  • src/app/home/recent-playlists/recent-playlists.component.html (1 hunks)
  • src/app/home/recent-playlists/recent-playlists.component.ts (2 hunks)
  • src/app/player/components/art-player/art-player.component.ts (2 hunks)
  • src/app/player/components/audio-player/audio-player.component.scss (1 hunks)
  • src/app/player/components/audio-player/audio-player.component.ts (1 hunks)
  • src/app/player/components/d-player/d-player.component.ts (2 hunks)
  • src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html (1 hunks)
  • src/app/player/components/epg-list/epg-item-description/epg-item-description.component.ts (2 hunks)
  • src/app/player/components/multi-epg/multi-epg-container.component.html (1 hunks)
  • src/app/player/components/multi-epg/multi-epg-container.component.ts (1 hunks)
  • src/app/player/components/video-player/video-player.component.html (1 hunks)
  • src/app/player/components/video-player/video-player.component.scss (1 hunks)
  • src/app/portals/web-player-view/web-player-view.component.html (1 hunks)
  • src/app/portals/web-player-view/web-player-view.component.ts (2 hunks)
  • src/app/services/whats-new.service.ts (2 hunks)
  • src/app/settings/settings.component.html (1 hunks)
  • src/app/shared/components/header/header.component.html (1 hunks)
  • src/app/shared/components/header/header.component.scss (1 hunks)
  • src/app/shared/components/header/header.component.ts (1 hunks)
  • src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html (1 hunks)
  • src/app/stalker/recently-viewed/recently-viewed.component.ts (1 hunks)
  • src/app/stalker/stalker-favorites/stalker-favorites.component.ts (1 hunks)
  • src/app/stalker/stalker-search/stalker-search.component.ts (1 hunks)
  • src/app/stalker/stalker.store.ts (6 hunks)
  • src/app/xtream-tauri/account-info/account-info.component.ts (2 hunks)
  • src/app/xtream-tauri/category-content-view/category-content-view.component.ts (1 hunks)
  • src/app/xtream-tauri/loading-overlay/loading-overlay.component.ts (2 hunks)
  • src/app/xtream-tauri/player-dialog/player-dialog.component.scss (2 hunks)
  • src/app/xtream/navigation-bar/navigation-bar.component.html (1 hunks)
  • src/app/xtream/navigation-bar/navigation-bar.component.ts (2 hunks)
  • src/app/xtream/season-container/season-container.component.html (1 hunks)
  • src/app/xtream/season-container/season-container.component.ts (2 hunks)
  • src/app/xtream/serial-details/serial-details.component.html (1 hunks)
  • src/app/xtream/serial-details/serial-details.component.ts (2 hunks)
  • src/app/xtream/vod-details/vod-details.component.html (2 hunks)
  • src/app/xtream/vod-details/vod-details.component.ts (2 hunks)
  • src/app/xtream/xtream-main-container.component.ts (3 hunks)
  • src/main.ts (1 hunks)
  • tsconfig.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/app/player/components/audio-player/audio-player.component.scss
🚧 Files skipped from review as they are similar to previous changes (34)
  • src/app/player/components/multi-epg/multi-epg-container.component.ts
  • src/app/xtream-tauri/category-content-view/category-content-view.component.ts
  • src/app/xtream/season-container/season-container.component.html
  • src/app/home/home.component.ts
  • src/app/stalker/stalker-search/stalker-search.component.ts
  • src/app/xtream-tauri/loading-overlay/loading-overlay.component.ts
  • src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html
  • src/app/home/recent-playlists/recent-playlists.component.ts
  • src/app/xtream/season-container/season-container.component.ts
  • tsconfig.json
  • src/app/player/components/epg-list/epg-item-description/epg-item-description.component.ts
  • src/app/player/components/audio-player/audio-player.component.ts
  • src/app/app.component.ts
  • src/app/stalker/stalker-favorites/stalker-favorites.component.ts
  • src/app/player/components/d-player/d-player.component.ts
  • src/app/player/components/video-player/video-player.component.html
  • src/app/xtream/navigation-bar/navigation-bar.component.html
  • src/app/xtream/navigation-bar/navigation-bar.component.ts
  • src/app/stalker/recently-viewed/recently-viewed.component.ts
  • src/app/player/components/video-player/video-player.component.scss
  • src/app/xtream-tauri/account-info/account-info.component.ts
  • src/app/services/whats-new.service.ts
  • src/app/shared/components/header/header.component.scss
  • src/app/xtream/xtream-main-container.component.ts
  • src/app/portals/web-player-view/web-player-view.component.ts
  • angular.json
  • src/app/home/home.component.html
  • src/app/portals/web-player-view/web-player-view.component.html
  • src/app/xtream/vod-details/vod-details.component.ts
  • src/app/xtream/serial-details/serial-details.component.ts
  • src/app/shared/components/header/header.component.ts
  • src/main.ts
  • src/app/app.component.html
  • src/app/xtream-tauri/player-dialog/player-dialog.component.scss
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/stalker/stalker.store.ts (2)
shared/ipc-commands.ts (1)
  • STALKER_REQUEST (55-55)
src/app/stalker/stalker-content-types.ts (1)
  • StalkerContentTypes (3-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: e2e-tests (macos-latest)
🔇 Additional comments (19)
src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html (1)

1-53: Overall: migration to new control flow looks solid.

Use of @if/@for and per-item markup reads well; track expression is appropriate.

Please run the component with Angular’s template type-checking at full strictness to ensure the inline $event.target cast passes. If it fails, I can provide a tiny appFallbackImg directive to handle errors cleanly.

src/app/shared/components/header/header.component.html (2)

69-75: Broken GitHub URL (extra slash)

-(click)="openUrl('https:///github.com/4gray/iptvnator')"
+(click)="openUrl('https://github.com/4gray/iptvnator')"

189-234: Incorrect — template matches the defined method 'opedAddPlaylistDialog'

src/app/shared/components/header/header.component.ts:165 defines opedAddPlaylistDialog and src/app/shared/components/header/header.component.html calls it (lines ~192–227); no runtime break. If the intended name is openAddPlaylistDialog, rename both TS and HTML.

Likely an incorrect or invalid review comment.

src/app/xtream/serial-details/serial-details.component.html (2)

11-83: Verify that the existing safety issue has been resolved.

I noticed from the past review comments that there was an issue with dereferencing item.info.* without proper null guards. While the current template uses the new Angular @if syntax instead of *ngIf, the same safety concern remains: multiple expressions use item.info.* without ensuring item or item.info exists first.

The template still accesses item.info.cover, item.info.name, item.info.plot, etc., directly without a parent guard around the entire block.

Apply this fix to wrap all item.info access in a single guard:

-  <div class="image">
-    @if (item.info.cover) {
-      <img [src]="item.info.cover" />
-    }
-  </div>
-  <div class="details">
-    <h2>{{ item.info.name }}</h2>
-    @if (item.info.plot) {
+  @if (item?.info) {
+  <div class="image">
+    @if (item.info.cover) {
+      <img [src]="item.info.cover" />
+    }
+  </div>
+  <div class="details">
+    <h2>{{ item.info.name }}</h2>
+    @if (item.info.plot) {
       <label>
         {{ item.info.plot }}
       </label>
     }
     <!-- ... rest of the details content ... -->
     <div class="action-buttons">
       <!-- ... buttons content ... -->
     </div>
   </div>
+  }

2-10: LGTM! Successful migration to Angular's new control flow syntax.

The template has been successfully migrated from Angular's structural directives (*ngIf, *ngFor) to the new built-in control flow syntax (@if, @for) introduced in Angular 17. The new syntax provides better readability and maintains the same functionality while being more familiar to developers coming from other frameworks.

Notable improvements:

  • Clean block-style syntax instead of structural directive asterisks
  • Consistent use of @if blocks for conditional rendering
  • Proper handling of the @else clause for the favorite/unfavorite button logic

Also applies to: 16-83

src/app/settings/settings.component.html (5)

1-12: LGTM! Clean conditional header rendering.

The header section properly uses the new @if/@else block syntax to conditionally render either the app-header component or the dialog title. This is a clean migration from the old structural directive approach.


16-77: LGTM! Excellent EPG URL management implementation.

The EPG URL section demonstrates proper use of the new @for control flow with the required track expression for optimal performance. The implementation correctly:

  • Uses @for (_ of epgUrl.controls; track _; let i = $index) for iterating over form array controls
  • Provides proper form validation and error handling
  • Includes intuitive UI controls (refresh, remove, add) for managing EPG sources
  • Properly scopes the functionality within the @if (isTauri) condition

78-148: LGTM! Well-structured video player configuration.

The video player selection and path configuration sections effectively use the new control flow syntax:

  • The @for (player of players; track player) loop properly implements the required track expression
  • Conditional sections for MPV and VLC player paths use @if (settingsForm.value.player === 'mpv') and @if (settingsForm.value.player === 'vlc') respectively
  • Clean separation of concerns with proper form control bindings

352-359: LGTM! Clean EPG note rendering.

The EPG note section correctly uses @if (isPwa) to conditionally display the note with proper link handling. The template structure is clean and maintains proper semantic HTML.


362-383: LGTM! Well-structured action buttons.

The action buttons section is properly organized with:

  • Clear separation using mat-divider
  • Proper alignment with align="end"
  • Appropriate button states and disabled logic
  • Clean use of Angular Material icons and translation pipes
src/app/home/recent-playlists/recent-playlists.component.html (1)

14-55: Approve — template bindings verified

All bindings used by the template exist and are correctly typed in src/app/home/recent-playlists/recent-playlists.component.ts (playlists$, ghostElements, allPlaylistsLoaded, sidebarMode, openInfoDialog, getGlobalFavorites, getPlaylist, refreshPlaylist, removeClicked, trackByFn). PlaylistMeta._id is present and used consistently. No issues found.

src/app/player/components/multi-epg/multi-epg-container.component.html (1)

10-23: Prev/Next day tooltips now align with actions.

The reversal noted earlier is fixed. Good catch.

package.json (1)

39-39: Resolved: previous skew and tslib floor.

Good fixes upgrading @angular/animations to v20 and raising tslib to ≥2.6.2.

Also applies to: 140-140

src/app/stalker/stalker.store.ts (4)

88-96: Good: params now include playlist and guard against undefined.

This fixes reactivity and avoids NPEs in getCategoryResource.


149-155: Still missing playlist in getContentResource params; loader is non‑reactive and can NPE.

This was flagged previously; loader reads store.currentPlaylist() but params omit it, so the resource won’t reload on playlist change and may crash when playlist is undefined.

Apply this diff:

-            params: () => ({
-                contentType: store.selectedContentType(),
-                category: store.selectedCategoryId(),
-                action: StalkerPortalActions.GetOrderedList,
-                search: store.searchPhrase(),
-                pageIndex: store.page() + 1,
-            }),
+            params: () => {
+                const playlist = store.currentPlaylist();
+                const category = store.selectedCategoryId();
+                const contentType = store.selectedContentType();
+                if (!playlist || !contentType) return undefined;
+                return {
+                    contentType,
+                    category,
+                    action: StalkerPortalActions.GetOrderedList,
+                    search: store.searchPhrase(),
+                    pageIndex: store.page() + 1,
+                    currentPlaylist: playlist,
+                };
+            },

224-241: serialSeasonsResource: add playlist to params and guard response.

Same issue as before: missing playlist in params and no guards; can be non‑reactive and crash.

Apply this diff:

-            params: () => ({
-                itemId: store.selectedSerialId(),
-            }),
-            loader: async ({ params }) => {
-                const { portalUrl, macAddress } = store.currentPlaylist();
+            params: () => {
+                const playlist = store.currentPlaylist();
+                const itemId = store.selectedSerialId();
+                if (!playlist || !itemId) return undefined;
+                return { itemId, currentPlaylist: playlist };
+            },
+            loader: async ({ params }) => {
+                if (!params?.currentPlaylist) return undefined;
+                const { portalUrl, macAddress } = params.currentPlaylist;
                 const queryParams = {
                     action: StalkerContentTypes.series.getContentAction,
                     type: 'series',
                     movie_id: params.itemId,
                 };
                 const response = await dataService.sendIpcEvent(
                     STALKER_REQUEST,
                     {
                         url: portalUrl,
                         macAddress,
                         params: queryParams,
                     }
                 );
-                return sortByNumericValue(response.js.data);
+                if (!response) return [];
+                return sortByNumericValue(response.js?.data ?? []);

88-147: Verification inconclusive — confirm no loaders read store.currentPlaylist() directly

Search returned no matches; cannot confirm. Verify that:

  • All resource loaders read params.currentPlaylist (not store.currentPlaylist).
  • All params() functions return undefined when required inputs are missing.
src/app/xtream/vod-details/vod-details.component.html (2)

118-121: Confirm safe pipe returns SafeResourceUrl.

No implementation for the safe pipe was found in the repo (search returned no files). Confirm the pipe calls DomSanitizer.bypassSecurityTrustResourceUrl(...) and returns SafeResourceUrl to avoid security warnings.

Location: src/app/xtream/vod-details/vod-details.component.html lines 118–121


81-107: Verify Material/Translate imports for this template.

Both standalone components import Material + ngx-translate but use mixed symbols — confirm your Angular/Material version and standardize imports:

  • src/app/xtream/vod-details/vod-details.component.ts — imports: MatButtonModule, MatIcon, TranslatePipe.
  • src/app/xtream-tauri/vod-details/vod-details.component.ts — imports: MatButton, MatIcon, TranslateModule.

Action: if using an Angular Material release that does not export MatButton/MatIcon as standalone directives, replace MatButton/MatIcon with MatButtonModule and MatIconModule; for ngx-translate prefer TranslatePipe/TranslateDirective in standalone components or TranslateModule for NgModule-based usage.

"tauri": "tauri"
},
"dependencies": {
"@angular-builders/jest": "17.0.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Verify Jest stack compatibility with Angular 20/TS 5.9.

Ensure @angular-builders/jest 17 + jest-preset-angular 14.5.3 work with Angular 20/TypeScript 5.9; you may need newer major versions.

Run:

Optionally confirm via web:

Also applies to: 129-131


🏁 Script executed:

#!/bin/bash
# Check declared versions
jq -r '.dependencies["@angular-builders/jest"], .devDependencies["jest-preset-angular"], .devDependencies.typescript' package.json

# Grep for test builder usage
rg -nC2 '"test"\s*:\s*\{[^\}]*"builder"|@"builder"' -g angular.json || rg -nC2 '"test"' angular.json

# Print peer deps hints (no install)
node -e 'console.log("Check release notes for jest-preset-angular vs TS 5.9 / NG 20");'

Length of output: 263


🌐 Web query:

Is jest-preset-angular 14.x compatible with Angular 20 and TypeScript 5.9?

💡 Result:

Short answer: No — jest-preset-angular 14.x is not officially compatible with Angular 20. Its published peerDependencies cap Angular versions below 20, and while its TypeScript peer is permissive (>=4.8) that does not guarantee full, tested compatibility with TypeScript 5.9. See sources:

  • package/compatibility listings and changelog showing v14 peer range targeting Angular <18–<20 (peer cap under 20). [1][2]
  • docs / installation page for v14 (current v14.x docs). [3]
  • GitHub issues discussing Angular 19/peerDependency and TypeScript/typedoc conflicts (shows practical incompatibility risks with newer Angular/TS releases). [4][5]

If you must run Angular 20 + TS 5.9, either:

  • upgrade to a jest-preset-angular release that explicitly adds Angular 20 support (check for v15 stable/RC notes), or
  • use the latest release candidate that advertises support and test thoroughly; otherwise you will hit peer-dependency warnings or runtime/test failures.

Sources:
[1] npmpeer / jest-preset-angular compatibility table.
[2] socket.dev / package diff & v14 changelog.
[3] jest-preset-angular docs (v14 installation).
[4] GitHub issue: Angular 19 support / peerDependencies.
[5] GitHub issue: typescript/typedoc peer-dependency conflicts.


Upgrade jest-preset-angular to one that supports Angular 20 and verify the Jest stack

  • Problem: [email protected] is not officially compatible with Angular 20 (peer deps cap <20); package.json shows jest-preset-angular 14.5.3 and typescript 5.9.2, and angular.json uses @angular-builders/jest:run (lines ~112–114).
  • Action: upgrade jest-preset-angular to a release that explicitly adds Angular 20 support (v15+ or latest RC), confirm @angular-builders/jest compatibility (upgrade if required), run CI/local tests and fix any test/runtime failures or peer-dependency issues.
🤖 Prompt for AI Agents
In package.json around line 38, update jest-preset-angular (currently 14.5.3) to
a version that explicitly supports Angular 20 (v15+ or the latest stable/RC),
verify and if necessary upgrade @angular-builders/jest to a compatible release,
then run CI/local test suite to catch and fix any test/runtime failures or
peer-dependency issues (adjust typescript or other peer deps as required)
ensuring Jest stack in angular.json (@angular-builders/jest:run) works with the
upgraded presets.

"iptv-playlist-parser": "github:4gray/iptv-playlist-parser",
"lodash": "4.17.21",
"moment": "2.30.1",
"ngx-indexed-db": "19.0.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Upgrade ngx-indexed-db for Angular 20 peer deps.

v19 targets Angular 19; use the Angular 20-compatible release.

-    "ngx-indexed-db": "19.0.0",
+    "ngx-indexed-db": "^20.0.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"ngx-indexed-db": "19.0.0",
"ngx-indexed-db": "^20.0.0",
🤖 Prompt for AI Agents
In package.json line 73, the dependency "ngx-indexed-db": "19.0.0" targets
Angular 19; update it to the Angular 20-compatible release (e.g.,
"ngx-indexed-db": "^20.0.0" or the exact 20.x version required by your app),
then run npm/yarn install to update package-lock.json or yarn.lock and run the
test/build to ensure peer deps resolve cleanly.

Comment on lines +86 to +90
"@angular-eslint/builder": "19.2.0",
"@angular-eslint/eslint-plugin": "19.2.0",
"@angular-eslint/eslint-plugin-template": "19.2.0",
"@angular-eslint/schematics": "19.2.0",
"@angular-eslint/template-parser": "19.2.0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

@angular-eslint v19 + ESLint 9 can conflict.

You’re on ESLint ^9.9.1; upgrade the Angular ESLint suite to v20 to ensure compatibility.

-    "@angular-eslint/builder": "19.2.0",
-    "@angular-eslint/eslint-plugin": "19.2.0",
-    "@angular-eslint/eslint-plugin-template": "19.2.0",
-    "@angular-eslint/schematics": "19.2.0",
-    "@angular-eslint/template-parser": "19.2.0",
+    "@angular-eslint/builder": "^20.0.0",
+    "@angular-eslint/eslint-plugin": "^20.0.0",
+    "@angular-eslint/eslint-plugin-template": "^20.0.0",
+    "@angular-eslint/schematics": "^20.0.0",
+    "@angular-eslint/template-parser": "^20.0.0",

Would you confirm the recommended versions per the latest docs?


🌐 Web query:

What versions of @angular-eslint packages are compatible with ESLint 9 and Angular 20?

💡 Result:

Use @angular-eslint packages with major version 20 (i.e., @angular-eslint/*@^20.0.0). The angular-eslint project aligns its major with Angular, and v18+ added ESLint v9/TypeScript-ESLint v8 flat-config support—so for Angular 20 + ESLint 9 you should use the 20.x @angular-eslint packages. [1][2][3]

Sources:

  • npm angular-eslint (notes about v18+ supporting ESLint v9 / typescript-eslint v8). [1]
  • angular-eslint GitHub (major version aligns with Angular CLI; supported ESLint versions). [2]
  • @angular-eslint/eslint-plugin v20 release (20.x) showing Angular v20 support. [3]

Upgrade @angular-eslint to v20 for ESLint 9 / Angular 20 compatibility

Confirmed: @angular-eslint v20.x is the correct major to use with Angular 20 and ESLint 9 — update package.json dependencies below.

File: package.json
Lines: 86-90

-    "@angular-eslint/builder": "19.2.0",
-    "@angular-eslint/eslint-plugin": "19.2.0",
-    "@angular-eslint/eslint-plugin-template": "19.2.0",
-    "@angular-eslint/schematics": "19.2.0",
-    "@angular-eslint/template-parser": "19.2.0",
+    "@angular-eslint/builder": "^20.0.0",
+    "@angular-eslint/eslint-plugin": "^20.0.0",
+    "@angular-eslint/eslint-plugin-template": "^20.0.0",
+    "@angular-eslint/schematics": "^20.0.0",
+    "@angular-eslint/template-parser": "^20.0.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@angular-eslint/builder": "19.2.0",
"@angular-eslint/eslint-plugin": "19.2.0",
"@angular-eslint/eslint-plugin-template": "19.2.0",
"@angular-eslint/schematics": "19.2.0",
"@angular-eslint/template-parser": "19.2.0",
"@angular-eslint/builder": "^20.0.0",
"@angular-eslint/eslint-plugin": "^20.0.0",
"@angular-eslint/eslint-plugin-template": "^20.0.0",
"@angular-eslint/schematics": "^20.0.0",
"@angular-eslint/template-parser": "^20.0.0",
🤖 Prompt for AI Agents
In package.json around lines 86 to 90, the @angular-eslint packages are pinned
to 19.2.0 but need to be upgraded to v20 for Angular 20 and ESLint 9
compatibility; update the versions for "@angular-eslint/builder",
"@angular-eslint/eslint-plugin", "@angular-eslint/eslint-plugin-template",
"@angular-eslint/schematics", and "@angular-eslint/template-parser" to a v20
range (e.g. "^20.0.0" or the specific 20.x release you prefer) and then run your
package manager (npm/yarn/pnpm) to reinstall and verify no peer dependency
conflicts.

Comment on lines +140 to +141
"tslib": "^2.6.2",
"typescript": "5.9.2",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Move tslib to runtime dependencies (Angular apps import helpers at runtime).

tslib should be in "dependencies", not "devDependencies", otherwise builds can reference a package that isn’t installed in production installs.

Apply:

@@ "dependencies": {
   "videojs-hls-quality-selector": "1.1.4",
-  "zone.js": "~0.15.0"
+  "zone.js": "~0.15.0",
+  "tslib": "^2.6.2"
 },
@@ "devDependencies": {
-  "tslib": "^2.6.2",
   "typescript": "5.9.2",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"tslib": "^2.6.2",
"typescript": "5.9.2",
"dependencies": {
"videojs-hls-quality-selector": "1.1.4",
"zone.js": "~0.15.0",
"tslib": "^2.6.2"
},
"devDependencies": {
"typescript": "5.9.2",
🤖 Prompt for AI Agents
In package.json around lines 140-141, tslib is currently listed under
devDependencies but must be moved to dependencies because Angular and other TS
runtime helpers are imported at runtime; update package.json by removing tslib
from devDependencies and adding the same version specifier ("^2.6.2") under the
top-level "dependencies" section, then run npm/yarn install (or update lockfile)
to ensure production installs include tslib.

Comment on lines +260 to +265
if let Some(origin_url) = origin {
if !origin_url.is_empty() {
command.arg(format!("--http-origin={}", origin_url));
}
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

❓ Verification inconclusive

Remove unsupported VLC flag --http-origin; warn and ignore instead.

VLC does not expose an --http-origin CLI option. Known HTTP CLI options include --http-user-agent and --http-referrer; arbitrary headers (like Origin) aren’t supported. Passing an unknown option may be ignored or cause runtime warnings. Replace with a warning and skip applying Origin for VLC.

Apply this diff:

-    if let Some(origin_url) = origin {
-        if !origin_url.is_empty() {
-            command.arg(format!("--http-origin={}", origin_url));
-        }
-    }
+    if let Some(origin_url) = origin {
+        if !origin_url.is_empty() {
+            log::warn!("VLC does not support setting an HTTP Origin header via CLI; ignoring origin '{}'.", origin_url);
+        }
+    }

References: VideoLAN wiki documents http-user-agent and http-referrer but not http-origin. Community threads also note VLC cannot add arbitrary HTTP headers. (wiki.videolan.org)


Remove unsupported VLC flag --http-origin; log warning instead.

VLC’s HTTP input options include --http-user-agent and --http-referrer but not --http-origin (wiki.videolan.org)

 File: src-tauri/src/commands/media.rs
@@ -260,7 +260,10 @@
-    if let Some(origin_url) = origin {
-        if !origin_url.is_empty() {
-            command.arg(format!("--http-origin={}", origin_url));
-        }
-    }
+    if let Some(origin_url) = origin {
+        if !origin_url.is_empty() {
+            log::warn!("VLC does not support setting an HTTP Origin header via CLI; ignoring origin '{}'.", origin_url);
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(origin_url) = origin {
if !origin_url.is_empty() {
command.arg(format!("--http-origin={}", origin_url));
}
}
if let Some(origin_url) = origin {
if !origin_url.is_empty() {
log::warn!("VLC does not support setting an HTTP Origin header via CLI; ignoring origin '{}'.", origin_url);
}
}
🤖 Prompt for AI Agents
In src-tauri/src/commands/media.rs around lines 260 to 265, remove the
unsupported VLC flag insertion that adds "--http-origin=..."; instead, when an
origin value is present and non-empty, do not append the flag to the command and
emit a warning log indicating the origin option is unsupported by VLC (include
the origin value in the log for debugging). Ensure the change preserves existing
control flow (check for Some(origin) and non-empty) but replaces
command.arg(...) with a call to the project's logging facility (e.g., warn!) to
notify that "--http-origin" is unsupported.

Comment on lines +239 to +245
@if (!isElectron && isDialogVisible$ | async) {
<ngx-whats-new
(closeModal)="setDialogVisibility(false)"
[items]="modals"
[options]="options"
/>
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix async pipe precedence to avoid runtime errors

Pipe has the lowest precedence; current expression pipes the entire (!isElectron && isDialogVisible$) into async. Parenthesize the observable.

-@if (!isElectron && isDialogVisible$ | async) {
+@if (!isElectron && (isDialogVisible$ | async)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@if (!isElectron && isDialogVisible$ | async) {
<ngx-whats-new
(closeModal)="setDialogVisibility(false)"
[items]="modals"
[options]="options"
/>
}
@if (!isElectron && (isDialogVisible$ | async)) {
<ngx-whats-new
(closeModal)="setDialogVisibility(false)"
[items]="modals"
[options]="options"
/>
}
🤖 Prompt for AI Agents
In src/app/shared/components/header/header.component.html around lines 239 to
245, the current conditional incorrectly applies the async pipe to the whole
expression due to pipe precedence; change the template expression so the async
pipe only applies to the observable by parenthesizing it (e.g. use (!isElectron
&& (isDialogVisible$ | async)) in the *ngIf), ensuring the boolean check
evaluates correctly and preventing runtime errors.

Comment on lines +41 to +47
<button
mat-icon-button
(click)="closeStream(process.id)"
title="Close"
>
<mat-icon>close</mat-icon>
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add accessible names to icon-only buttons.

Icon-only “Close” button lacks an explicit accessible name; title is unreliable. Add aria-label and hide the decorative icon from AT.

Apply this diff:

-        <button
-          mat-icon-button
-          (click)="closeStream(process.id)"
-          title="Close"
-          >
-          <mat-icon>close</mat-icon>
-        </button>
+        <button
+          mat-icon-button
+          type="button"
+          aria-label="Close stream"
+          (click)="closeStream(process.id)"
+          title="Close"
+        >
+          <mat-icon aria-hidden="true">close</mat-icon>
+        </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
mat-icon-button
(click)="closeStream(process.id)"
title="Close"
>
<mat-icon>close</mat-icon>
</button>
<button
mat-icon-button
type="button"
aria-label="Close stream"
(click)="closeStream(process.id)"
title="Close"
>
<mat-icon aria-hidden="true">close</mat-icon>
</button>
🤖 Prompt for AI Agents
In src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html around
lines 41 to 47, the icon-only Close button lacks an explicit accessible name and
the decorative icon is exposed to assistive tech; add an aria-label (e.g.,
aria-label="Close stream") to the button element and mark the mat-icon
decorative by adding aria-hidden="true" so screen readers read the button label
but ignore the icon; preserve the (click)="closeStream(process.id)" handler and
any title if desired.

Comment on lines +156 to 175
loader: async ({ params }) => {
if (
!request.category ||
request.category === null ||
request.category === ''
!params.category ||
params.category === null ||
params.category === ''
) {
return Promise.resolve(undefined);
}

const currentPlaylist = store.currentPlaylist;
const params = {
action: StalkerContentTypes[request.contentType]
const queryParams = {
action: StalkerContentTypes[params.contentType]
.getContentAction,
type: request.contentType,
category: request.category ?? '',
genre: request.category ?? '',
type: params.contentType,
category: params.category ?? '',
genre: params.category ?? '',
sortby: 'added',
...(request.search !== ''
? { search: request.search }
: {}),
p: request.pageIndex,
...(params.search !== '' ? { search: params.search } : {}),
p: params.pageIndex,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Null‑safety and response handling in getContentResource loader.

  • Uses response.js before verifying response (Line 186). Critical NPE risk.
  • Uses store.currentPlaylist() instead of params.currentPlaylist.
  • Minor: simplify empty checks; compute hasMore using known totals without relying on immediate signal update.

Apply this diff:

-            loader: async ({ params }) => {
-                if (
-                    !params.category ||
-                    params.category === null ||
-                    params.category === ''
-                ) {
-                    return Promise.resolve(undefined);
-                }
-
-                const currentPlaylist = store.currentPlaylist;
+            loader: async ({ params }) => {
+                if (!params?.category || params.category === '') {
+                    return undefined;
+                }
+                if (!params?.currentPlaylist) return undefined;
                 const queryParams = {
                     action: StalkerContentTypes[params.contentType]
                         .getContentAction,
-                    type: params.contentType,
-                    category: params.category ?? '',
-                    genre: params.category ?? '',
+                    type: params.contentType,
+                    category: params.category ?? '',
+                    genre: params.category ?? '',
                     sortby: 'added',
-                    ...(params.search !== '' ? { search: params.search } : {}),
+                    ...(params.search ? { search: params.search } : {}),
                     p: params.pageIndex,
                 };
 
                 const response = await dataService.sendIpcEvent(
                     STALKER_REQUEST,
                     {
-                        url: currentPlaylist().portalUrl,
-                        macAddress: currentPlaylist().macAddress,
+                        url: params.currentPlaylist.portalUrl,
+                        macAddress: params.currentPlaylist.macAddress,
                         params: queryParams,
                     }
                 );
 
-                patchState(store, { totalCount: response.js.total_items });
+                if (!response) {
+                    throw new Error('Error: no response from STALKER_REQUEST');
+                }
+                const totalItems = Number(response.js?.total_items ?? 0);
+                patchState(store, { totalCount: totalItems });
 
-                if (response) {
-                    const newItems = response.js.data.map((item) => ({
+                if (response) {
+                    const data = Array.isArray(response.js?.data) ? response.js.data : [];
+                    const newItems = data.map((item) => ({
                         ...item,
                         cover: item.screenshot_uri,
                     }));
 
                     if (store.selectedContentType() === 'itv') {
                         // Check if we're loading the first page or loading more
-                        if (params.pageIndex === 1) {
+                        if (params.pageIndex === 1) {
                             patchState(store, { itvChannels: newItems });
                         } else {
                             patchState(store, {
                                 itvChannels: [
                                     ...store.itvChannels(),
                                     ...newItems,
                                 ],
                             });
                         }
 
                         // Update hasMoreItems based on total count and current items
-                        const totalLoaded = store.itvChannels().length;
+                        const totalLoaded =
+                            params.pageIndex === 1
+                                ? newItems.length
+                                : store.itvChannels().length + newItems.length;
                         patchState(store, {
-                            hasMoreChannels:
-                                totalLoaded < response.js.total_items,
+                            hasMoreChannels: totalLoaded < totalItems,
                         });
                     }
 
                     return newItems;
-                } else {
-                    throw new Error(
-                        `Error: ${response.message} (Status: ${response.status})`
-                    );
-                }
+                }

Also applies to: 180-186, 188-213

🤖 Prompt for AI Agents
In src/app/stalker/stalker.store.ts around lines 156-175 (also applies to
180-186 and 188-213), the loader needs null-safety and correct playlist usage:
replace the verbose category emptiness checks with a simple falsy check (e.g. if
(!params.category) return undefined), use params.currentPlaylist instead of
store.currentPlaylist, guard usage of response.js/response.json by first
checking response.ok and that response is non-null before parsing, and compute
hasMore from returned totals (e.g. compare total items to pageIndex*pageSize)
rather than relying on immediate signal updates; ensure error/empty responses
return a safe undefined or empty result and log or throw a descriptive error
where appropriate.

Comment on lines 15 to +18
(error)="
$event.target.src = './assets/images/default-poster.png'
"
/>
} @else {
<div class="placeholder-cover"></div>
}
</div>
<div class="details">
<h2>{{ item.info.name }}</h2>
<label *ngIf="item.info.description">
{{ item.info.description }}
</label>
<label *ngIf="item.info.releasedate">
<div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div>
{{ item.info.releasedate }}
</label>
<label *ngIf="item.info.genre">
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
{{ item.info.genre }}
</label>
<label *ngIf="item.info.country">
<div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
{{ item.info.country }}
</label>
<div *ngIf="item.info.actors">
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
{{ item.info.actors }}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix template type error in (error) handler.

$event.target is EventTarget and has no src in strict template checking. Cast or delegate to a method.

-        (error)="
-                    $event.target.src = './assets/images/default-poster.png'
-                "
+        (error)="($event.target as HTMLImageElement).src = './assets/images/default-poster.png'"

Or: (error)="onImgError($event)" and set (event.target as HTMLImageElement).src in TS.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/xtream/vod-details/vod-details.component.html around lines 15-18 the
(error) handler tries to set $event.target.src but template type checking sees
$event.target as EventTarget without a src property; change the template to call
a component method (e.g. (error)="onImgError($event)") or cast the target
in-line, and implement the onImgError handler in the component TS to set
(event.target as HTMLImageElement).src = 'assets/images/default-poster.png' (or
otherwise guard/cast before assigning).

}
</div>
<div class="details">
<h2>{{ item.info.name }}</h2>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against undefined item.info throughout (@if conditions and interpolations).

Several blocks access item.info.* without safe navigation, which will throw if info is undefined during initial render. Use ?. in conditions and interpolations or wrap the section with a single guard (@if (item?.info as info) { ... }).

Apply this minimal fix:

-    <h2>{{ item.info.name }}</h2>
+    <h2>{{ item.info?.name }}</h2>

-    @if (item.info.description) {
+    @if (item.info?.description) {
-        {{ item.info.description }}
+        {{ item.info?.description }}
     }

-    @if (item.info.releasedate) {
+    @if (item.info?.releasedate) {
-        {{ item.info.releasedate }}
+        {{ item.info?.releasedate }}
     }

-    @if (item.info.genre) {
+    @if (item.info?.genre) {
-        {{ item.info.genre }}
+        {{ item.info?.genre }}
     }

-    @if (item.info.country) {
+    @if (item.info?.country) {
-        {{ item.info.country }}
+        {{ item.info?.country }}
     }

-    @if (item.info.actors) {
+    @if (item.info?.actors) {
-        {{ item.info.actors }}
+        {{ item.info?.actors }}
     }

-    @if (item.info.director) {
+    @if (item.info?.director) {
-        {{ item.info.director }}
+        {{ item.info?.director }}
     }

-    @if (item.info.duration) {
+    @if (item.info?.duration) {
-        {{ item.info.duration }}
+        {{ item.info?.duration }}
     }

-    @if (item.info.rating_imdb) {
+    @if (item.info?.rating_imdb) {
-        {{ item.info.rating_imdb }}
+        {{ item.info?.rating_imdb }}
     }

-    @if (item.info.rating_kinopoisk) {
+    @if (item.info?.rating_kinopoisk) {
-        {{ item.info.rating_kinopoisk }}
+        {{ item.info?.rating_kinopoisk }}
     }

-  @if (item.info.youtube_trailer) {
+  @if (item.info?.youtube_trailer) {
-            'https://www.youtube.com/embed/' + item.info?.youtube_trailer | safe
+            'https://www.youtube.com/embed/' + item.info?.youtube_trailer | safe
   }

Alternatively, wrap once and alias:

@if (item?.info as info) {
  <h2>{{ info.name }}</h2>
  ...
  [src]="'https://www.youtube.com/embed/' + info.youtube_trailer | safe"
}

Also applies to: 25-29, 30-35, 36-41, 42-47, 48-53, 54-59, 60-65, 66-71, 72-79, 112-128

🤖 Prompt for AI Agents
In src/app/xtream/vod-details/vod-details.component.html around line 24 (and
also covering lines
25-29,30-35,36-41,42-47,48-53,54-59,60-65,66-71,72-79,112-128), multiple
bindings access item.info.* directly which will throw if item.info is undefined;
update the template to guard against undefined by either adding safe-navigation
operators to each access (e.g., item?.info?.prop) or, preferably, wrap the whole
section in a single guard that aliases info (e.g., @if (item?.info as info) {
... } ) and then use info.prop throughout, and ensure any attribute bindings
(like [src]) use the aliased info and existing pipes safely.

@4gray 4gray merged commit e56ebce into master Sep 21, 2025
7 of 8 checks passed
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.

2 participants