feat: Jellyfin Plugin UI Integration #51

Merged
devitq merged 13 commits from feature/jellyfin-plugin-ui-integration-2323592149917875874 into feat/implement-jellyfin-plugin-46 2026-05-22 13:36:25 +00:00
devitq commented 2026-05-20 17:35:52 +00:00 (Migrated from github.com)

This change refactors and completes the MovieNight Jellyfin plugin. Key enhancements include:

  1. UI Integration: A new ui.js script is injected into the Jellyfin web UI using a MutationObserver. It adds a "Recommend Film" button to library headers and a MovieNight-specific rating selector to item details pages.
  2. Full Library Sync: The plugin now performs a comprehensive sync of movie metadata (title, year, genres, external IDs) and user-specific library states (viewed status, play count, ratings) to the MovieNight backend.
  3. Backend Contract Compliance: MovieNightBackendClient and MovieNightController were updated to support the full set of required APIs: Sync, Recommendations, Ratings, and Viewed status, using the X-MovieNight-Plugin-Token for authentication.
  4. Improved Architecture: Sync logic was extracted into a dedicated MovieNightSyncService to ensure consistency between periodic background syncs and manual triggers.
  5. Build Compatibility: The code was verified to build successfully with .NET 9.0 and Jellyfin 10.11.x APIs.

PR created automatically by Jules for task 2323592149917875874 started by @devitq

This change refactors and completes the MovieNight Jellyfin plugin. Key enhancements include: 1. **UI Integration:** A new `ui.js` script is injected into the Jellyfin web UI using a `MutationObserver`. It adds a "Recommend Film" button to library headers and a MovieNight-specific rating selector to item details pages. 2. **Full Library Sync:** The plugin now performs a comprehensive sync of movie metadata (title, year, genres, external IDs) and user-specific library states (viewed status, play count, ratings) to the MovieNight backend. 3. **Backend Contract Compliance:** `MovieNightBackendClient` and `MovieNightController` were updated to support the full set of required APIs: Sync, Recommendations, Ratings, and Viewed status, using the `X-MovieNight-Plugin-Token` for authentication. 4. **Improved Architecture:** Sync logic was extracted into a dedicated `MovieNightSyncService` to ensure consistency between periodic background syncs and manual triggers. 5. **Build Compatibility:** The code was verified to build successfully with .NET 9.0 and Jellyfin 10.11.x APIs. --- *PR created automatically by Jules for task [2323592149917875874](https://jules.google.com/task/2323592149917875874) started by @devitq*
google-labs-jules[bot] commented 2026-05-20 17:35:54 +00:00 (Migrated from github.com)

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to **Reactive Mode**. When this mode is on, I will only act on comments where you specifically mention me with `@jules`. You can find this option in the **Pull Request** section of your [global Jules UI settings](https://jules.google.com/settings). You can always switch back! New to Jules? Learn more at [jules.google/docs](https://jules.google/docs). --- *_For security, I will only act on instructions from the user who triggered this task._*
coderabbitai[bot] commented 2026-05-20 17:35:59 +00:00 (Migrated from github.com)

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fd9af6fc-b333-448c-97ab-d0d7cff6903a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/jellyfin-plugin-ui-integration-2323592149917875874

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

<!-- This is an auto-generated comment: summarize by coderabbit.ai --> <!-- This is an auto-generated comment: skip review by coderabbit.ai --> > [!IMPORTANT] > ## Review skipped > > Auto reviews are disabled on base/target branches other than the default branch. > > > > Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command. > > <details> > <summary>⚙️ Run configuration</summary> > > **Configuration used**: Organization UI > > **Review profile**: ASSERTIVE > > **Plan**: Pro > > **Run ID**: `fd9af6fc-b333-448c-97ab-d0d7cff6903a` > > </details> > > You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file. > > Use the checkbox below for a quick retry: > - [ ] <!-- {"checkboxId": "e9bb8d72-00e8-4f67-9cb2-caf3b22574fe"} --> 🔍 Trigger review <!-- end of auto-generated comment: skip review by coderabbit.ai --> <!-- finishing_touch_checkbox_start --> <details> <summary>✨ Finishing Touches</summary> <details> <summary>🧪 Generate unit tests (beta)</summary> - [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests - [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit unit tests in branch `feature/jellyfin-plugin-ui-integration-2323592149917875874` </details> </details> <!-- finishing_touch_checkbox_end --> <!-- tips_start --> --- Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=devitq/movienight-backend&utm_content=51)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. <details> <summary>❤️ Share</summary> - [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) - [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) - [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) - [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code) </details> <sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub> <!-- tips_end -->
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-05-20 17:42:38 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

This PR completes the Jellyfin plugin integration by adding a UI injection script and implementing the plugin-side contract needed to sync Jellyfin library/user state data and proxy recommendation/rating/viewed operations to the MovieNight backend.

Changes:

  • Added a dedicated MovieNightSyncService and wired periodic/manual sync to push full library + per-user state to the backend.
  • Expanded MovieNightBackendClient + MovieNightController to cover sync, recommendations, ratings, and viewed-status endpoints.
  • Embedded and documented a ui.js script for Jellyfin Web UI integration (Recommend button + rating selector).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightSyncService.cs New full-library sync implementation (movies + user states) pushed to backend
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightPeriodicSyncService.cs Periodic sync now delegates to MovieNightSyncService
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightBackendClient.cs Adds HTTP calls for sync, recommendations, ratings, and viewed status
plugins/jellyfin/Jellyfin.Plugin.MovieNight/PluginServiceRegistrator.cs Registers MovieNightSyncService in DI
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Plugin.cs Exposes ui.js as an embedded plugin page
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Jellyfin.Plugin.MovieNight.csproj Embeds Configuration/ui.js as a resource
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Controllers/MovieNightController.cs Adds proxy endpoints for recommendations/ratings/viewed + manual sync trigger
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js UI injection via MutationObserver (Recommend + rating UI)
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/configPage.html Adds UI integration instructions + script URL placeholder
plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/config.js Populates the UI script URL to copy into Jellyfin “Custom JavaScript”

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Pull request overview This PR completes the Jellyfin plugin integration by adding a UI injection script and implementing the plugin-side contract needed to sync Jellyfin library/user state data and proxy recommendation/rating/viewed operations to the MovieNight backend. **Changes:** - Added a dedicated `MovieNightSyncService` and wired periodic/manual sync to push full library + per-user state to the backend. - Expanded `MovieNightBackendClient` + `MovieNightController` to cover sync, recommendations, ratings, and viewed-status endpoints. - Embedded and documented a `ui.js` script for Jellyfin Web UI integration (Recommend button + rating selector). ### Reviewed changes Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments. <details> <summary>Show a summary per file</summary> | File | Description | | ---- | ----------- | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightSyncService.cs | New full-library sync implementation (movies + user states) pushed to backend | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightPeriodicSyncService.cs | Periodic sync now delegates to `MovieNightSyncService` | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightBackendClient.cs | Adds HTTP calls for sync, recommendations, ratings, and viewed status | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/PluginServiceRegistrator.cs | Registers `MovieNightSyncService` in DI | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Plugin.cs | Exposes `ui.js` as an embedded plugin page | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Jellyfin.Plugin.MovieNight.csproj | Embeds `Configuration/ui.js` as a resource | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Controllers/MovieNightController.cs | Adds proxy endpoints for recommendations/ratings/viewed + manual sync trigger | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js | UI injection via `MutationObserver` (Recommend + rating UI) | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/configPage.html | Adds UI integration instructions + script URL placeholder | | plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/config.js | Populates the UI script URL to copy into Jellyfin “Custom JavaScript” | </details> --- 💡 <a href="/devitq/movienight-backend/new/feat/implement-jellyfin-plugin-46?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
@@ -0,0 +1,450 @@
(function () {
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-20 17:42:38 +00:00

The MutationObserver runs injectUI on every DOM mutation across the entire subtree. On dynamic Jellyfin pages this can fire very frequently and repeatedly execute multiple querySelector calls, impacting UI responsiveness. Consider debouncing/throttling injectUI, narrowing the observed subtree, and/or disconnecting the observer once the UI elements have been injected for the current view.

The `MutationObserver` runs `injectUI` on every DOM mutation across the entire subtree. On dynamic Jellyfin pages this can fire very frequently and repeatedly execute multiple `querySelector` calls, impacting UI responsiveness. Consider debouncing/throttling `injectUI`, narrowing the observed subtree, and/or disconnecting the observer once the UI elements have been injected for the current view.
@@ -0,0 +110,4 @@
const queryString = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : window.location.search;
const params = new URLSearchParams(queryString);
return params.get('id') || params.get('itemId');
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-20 17:42:37 +00:00

itemId is taken directly from the page URL and then used as {filmId} in the rating POST path. Elsewhere in the plugin (sync/playback events) Jellyfin ids are serialized using ToString("N") (no dashes), so this can lead to inconsistent identifiers being sent to the backend depending on URL format. Consider normalizing the URL id to the same canonical format before calling the API (and applying encodeURIComponent when interpolating path segments).

`itemId` is taken directly from the page URL and then used as `{filmId}` in the rating POST path. Elsewhere in the plugin (sync/playback events) Jellyfin ids are serialized using `ToString("N")` (no dashes), so this can lead to inconsistent identifiers being sent to the backend depending on URL format. Consider normalizing the URL `id` to the same canonical format before calling the API (and applying `encodeURIComponent` when interpolating path segments).
@@ -77,0 +167,4 @@
/// Creates a new film by generating a .strm file in a folder-per-movie structure.
/// Structure: Movie Name (Year) [imdbid-ttXXXXXXX]/Movie Name (Year) [imdbid-ttXXXXXXX].strm
/// </summary>
[HttpPost("Films")]
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-20 17:42:37 +00:00

These endpoints are documented as operating on the “current user”, but they accept an arbitrary userId in the route and do not validate it against the authenticated Jellyfin user. Any authenticated user could request recommendations/submit ratings/mark viewed on behalf of another user by changing the URL. Prefer deriving the user id from the auth context (and removing the route param) or explicitly rejecting requests where userId doesn’t match the authenticated principal.

These endpoints are documented as operating on the “current user”, but they accept an arbitrary `userId` in the route and do not validate it against the authenticated Jellyfin user. Any authenticated user could request recommendations/submit ratings/mark viewed on behalf of another user by changing the URL. Prefer deriving the user id from the auth context (and removing the route param) or explicitly rejecting requests where `userId` doesn’t match the authenticated principal.
@@ -1,6 +1,14 @@
using System;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-20 17:42:38 +00:00

This file includes several unused using directives (e.g., System.Collections.Generic, System.Linq, and the MediaBrowser.* / Jellyfin.Data.Enums imports). Please remove unused usings to reduce noise and avoid analyzer warnings.

This file includes several unused `using` directives (e.g., `System.Collections.Generic`, `System.Linq`, and the `MediaBrowser.*` / `Jellyfin.Data.Enums` imports). Please remove unused usings to reduce noise and avoid analyzer warnings.
@@ -0,0 +69,4 @@
foreach (var item in items)
{
if (item is not Movie movie) continue;
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-20 17:42:36 +00:00

PerformSyncAsync can run for a long time (full library walk + per-user lookups) but the loop never checks cancellationToken. This can delay shutdown/cancellation even after a stop request. Consider calling cancellationToken.ThrowIfCancellationRequested() (or equivalent) inside the foreach and before expensive work.

`PerformSyncAsync` can run for a long time (full library walk + per-user lookups) but the loop never checks `cancellationToken`. This can delay shutdown/cancellation even after a stop request. Consider calling `cancellationToken.ThrowIfCancellationRequested()` (or equivalent) inside the `foreach` and before expensive work.
@@ -0,0 +99,4 @@
syncItems.Add(itemData);
}
await _backendClient.SyncAsync(new { items = syncItems }, cancellationToken).ConfigureAwait(false);
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-20 17:42:37 +00:00

The sync payload builds userStates by calling _userDataManager.GetUserData(u, movie) for every (movie × user) pair. This is an O(N*M) pattern and can become very expensive on large libraries / many users (and can also create very large payloads). Consider batching user-data retrieval (if Jellyfin exposes a bulk API) and/or syncing user state per-user instead of embedding all users in every item.

The sync payload builds `userStates` by calling `_userDataManager.GetUserData(u, movie)` for every (movie × user) pair. This is an O(N*M) pattern and can become very expensive on large libraries / many users (and can also create very large payloads). Consider batching user-data retrieval (if Jellyfin exposes a bulk API) and/or syncing user state per-user instead of embedding all users in every item.
Copilot commented 2026-05-20 18:26:52 +00:00 (Migrated from github.com)

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/devitq/movienight-backend/pulls/51/comments
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `https://api.github.com/repos/devitq/movienight-backend/pulls/51/comments` > - Triggering command: `/home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js` (http block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/devitq/movienight-backend/settings/copilot/coding_agent) (admins only) > > </details>
copilot-swe-agent[bot] (Migrated from github.com) reviewed 2026-05-20 18:53:26 +00:00
@@ -0,0 +69,4 @@
foreach (var item in items)
{
if (item is not Movie movie) continue;
copilot-swe-agent[bot] (Migrated from github.com) commented 2026-05-20 18:53:26 +00:00

Implemented in 07c9995: added an early cancellationToken.ThrowIfCancellationRequested() at the start of PerformSyncAsync so cancellation is honored before the full library query begins, in addition to the existing checks inside the loops.

Implemented in 07c9995: added an early `cancellationToken.ThrowIfCancellationRequested()` at the start of `PerformSyncAsync` so cancellation is honored before the full library query begins, in addition to the existing checks inside the loops.
Sign in to join this conversation.