devitq
merged 13 commits from feature/jellyfin-plugin-ui-integration-2323592149917875874 into feat/implement-jellyfin-plugin-462026-05-22 13:36:25 +00:00
This change refactors and completes the MovieNight Jellyfin plugin. Key enhancements include:
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.
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.
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.
Improved Architecture: Sync logic was extracted into a dedicated MovieNightSyncService to ensure consistency between periodic background syncs and manual triggers.
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*
👋 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!
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._*
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.
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
Copy Link
Copy Source
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.
## 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>.
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.
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).
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.
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.
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.
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.
If you need me to access, download, or install something from one of these locations, you can either:
Configure 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 (admins only)
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
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
This change refactors and completes the MovieNight Jellyfin plugin. Key enhancements include:
ui.jsscript is injected into the Jellyfin web UI using aMutationObserver. It adds a "Recommend Film" button to library headers and a MovieNight-specific rating selector to item details pages.MovieNightBackendClientandMovieNightControllerwere updated to support the full set of required APIs: Sync, Recommendations, Ratings, and Viewed status, using theX-MovieNight-Plugin-Tokenfor authentication.MovieNightSyncServiceto ensure consistency between periodic background syncs and manual triggers.PR created automatically by Jules for task 2323592149917875874 started by @devitq
👋 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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
feature/jellyfin-plugin-ui-integration-2323592149917875874Thanks 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 helpto get the list of available commands and usage tips.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:
MovieNightSyncServiceand wired periodic/manual sync to push full library + per-user state to the backend.MovieNightBackendClient+MovieNightControllerto cover sync, recommendations, ratings, and viewed-status endpoints.ui.jsscript 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
MovieNightSyncServiceMovieNightSyncServicein DIui.jsas an embedded plugin pageConfiguration/ui.jsas a resourceMutationObserver(Recommend + rating UI)💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
@@ -0,0 +1,450 @@(function () {The
MutationObserverrunsinjectUIon every DOM mutation across the entire subtree. On dynamic Jellyfin pages this can fire very frequently and repeatedly execute multiplequerySelectorcalls, impacting UI responsiveness. Consider debouncing/throttlinginjectUI, 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');}itemIdis 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 usingToString("N")(no dashes), so this can lead to inconsistent identifiers being sent to the backend depending on URL format. Consider normalizing the URLidto the same canonical format before calling the API (and applyingencodeURIComponentwhen 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")]These endpoints are documented as operating on the “current user”, but they accept an arbitrary
userIdin 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 whereuserIddoesn’t match the authenticated principal.@@ -1,6 +1,14 @@using System;This file includes several unused
usingdirectives (e.g.,System.Collections.Generic,System.Linq, and theMediaBrowser.*/Jellyfin.Data.Enumsimports). 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;PerformSyncAsynccan run for a long time (full library walk + per-user lookups) but the loop never checkscancellationToken. This can delay shutdown/cancellation even after a stop request. Consider callingcancellationToken.ThrowIfCancellationRequested()(or equivalent) inside theforeachand before expensive work.@@ -0,0 +99,4 @@syncItems.Add(itemData);}await _backendClient.SyncAsync(new { items = syncItems }, cancellationToken).ConfigureAwait(false);The sync payload builds
userStatesby 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.Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.
@@ -0,0 +69,4 @@foreach (var item in items){if (item is not Movie movie) continue;Implemented in
07c9995: added an earlycancellationToken.ThrowIfCancellationRequested()at the start ofPerformSyncAsyncso cancellation is honored before the full library query begins, in addition to the existing checks inside the loops.