Skip to content

Feature/delete sync playlists#172

Merged
jamcalli merged 8 commits intodevelopfrom
feature/delete-sync-playlists
May 11, 2025
Merged

Feature/delete sync playlists#172
jamcalli merged 8 commits intodevelopfrom
feature/delete-sync-playlists

Conversation

@jamcalli
Copy link
Copy Markdown
Owner

@jamcalli jamcalli commented May 11, 2025

Description

Add deletion prevention playlists
Auto create playlists for all Plex users
Anything added to these playlists will be checked against to prevent deletion during delete sync

Related Issues

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Performance improvement
  • Code refactoring
  • Documentation update
  • Dependency update

Testing Performed

Screenshots

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • My changes work with existing functionality

Summary by CodeRabbit

  • New Features

    • Introduced Plex playlist protection to prevent deletion of media items included in specified Plex playlists.
    • Added new configuration options for enabling Plex playlist protection, specifying the protection playlist name, and setting the Plex server URL.
    • Delete sync form now includes controls for enabling playlist protection and naming the protection playlist.
  • Improvements

    • Delete sync results now display counts of protected items that were skipped from deletion.
    • Enhanced error handling for configuration endpoints with clearer, structured error responses.
  • Bug Fixes

    • Improved handling of Plex API 404 responses to avoid unnecessary retries and log appropriate warnings.
  • Chores

    • Added the fast-xml-parser dependency for improved XML parsing capabilities.

jamcalli added 2 commits May 11, 2025 00:14
- Introduced migration to add `enablePlexPlaylistProtection`, `plexProtectionPlaylistName`, and `plexServerUrl` columns to the `configs` table.
- The `plexServerUrl` is optional, allowing for auto-detection of the Plex server URL.

feat(plex-server): implement PlexServerService for playlist protection

- Created `PlexServerService` class to manage interactions with Plex Media Server.
- Added methods for user authentication, playlist management, and protection checks.
- Implemented caching for server connections, users, and shared server info to optimize performance.
- Introduced functionality to create and manage protection playlists for users.
- Added methods to retrieve and check protected items based on user playlists.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented May 11, 2025

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.

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

Walkthrough

The changes introduce Plex playlist protection to the application's delete synchronization workflow. This includes new configuration options, database schema updates, UI form and result enhancements, and a comprehensive Plex server integration service. The delete sync logic now checks Plex protection playlists to prevent deletion of protected media. Error handling in configuration routes is standardized, and relevant schemas and types are updated throughout the codebase.

Changes

File(s) Change Summary
migrations/migrations/022_20250508_add_plex_playlist_protection.ts Adds a migration to introduce three new columns (enablePlexPlaylistProtection, plexProtectionPlaylistName, plexServerUrl) to the configs table for Plex playlist protection support.
package.json Adds fast-xml-parser dependency for XML parsing needs in Plex integration.
src/client/features/utilities/components/delete-sync/delete-sync-dry-run-modal.tsx Updates results grid to show a protected items card if any are present; adjusts grid columns accordingly.
src/client/features/utilities/components/delete-sync/delete-sync-form.tsx Adds form fields for enabling Plex playlist protection and specifying the protection playlist name.
src/client/features/utilities/hooks/useDeleteSyncForm.ts Integrates new fields into the form schema, default values, and form state management for Plex playlist protection.
src/plugins/external/env.ts Extends configuration schema with Plex playlist protection options and server URL.
src/routes/v1/config/config.ts Refactors GET/PUT /config handlers to return structured error responses instead of throwing, updating response types.
src/schemas/config/config.schema.ts Adds optional fields for Plex playlist protection and server URL to the config schema.
src/schemas/scheduler/scheduler.schema.ts Adds optional protected count fields to deletion result schemas.
src/services/database.service.ts Updates config retrieval and creation to include Plex playlist protection fields.
src/services/delete-sync.service.ts Integrates Plex playlist protection into delete sync logic, including initialization, GUID checks, and protected count tracking. Adds caching and safety checks.
src/types/config.types.ts Adds Plex playlist protection fields to the Config interface.
src/types/delete-sync.types.ts Adds optional protected count fields to DeleteSyncResult type.
src/utils/plex-server.ts Introduces PlexServerService class for managing Plex server connections, users, playlists, and protection logic, with caching and robust error handling.
src/utils/plex.ts Makes toItemsSingle exported and adds handling to skip retries on HTTP 404 errors from Plex API.

Sequence Diagram(s)

Plex Playlist Protection in Delete Sync Workflow

sequenceDiagram
    participant User
    participant UI
    participant DeleteSyncService
    participant PlexServerService
    participant Database

    User->>UI: Enable Plex playlist protection and configure playlist name
    UI->>Database: Save config (enablePlexPlaylistProtection, playlist name, server URL)
    User->>UI: Start delete sync
    UI->>DeleteSyncService: Trigger delete sync
    DeleteSyncService->>PlexServerService: initialize()
    PlexServerService->>PlexServerService: Discover server, load users, playlists, protected GUIDs
    DeleteSyncService->>PlexServerService: getProtectedItems()
    PlexServerService-->>DeleteSyncService: Set of protected GUIDs
    loop For each item to delete
        DeleteSyncService->>PlexServerService: isItemProtected(item GUIDs)
        alt Item is protected
            DeleteSyncService-->>DeleteSyncService: Increment protected count, skip deletion
        else Not protected
            DeleteSyncService-->>DeleteSyncService: Proceed with deletion
        end
    end
    DeleteSyncService->>UI: Return deletion summary (including protected counts)
Loading

Configuration Error Handling (GET/PUT /config)

sequenceDiagram
    participant Client
    participant ConfigRouteHandler
    participant Database

    Client->>ConfigRouteHandler: GET /config or PUT /config
    ConfigRouteHandler->>Database: Retrieve or update config
    alt Error occurs
        ConfigRouteHandler-->>Client: Return error object with status code and message
    else Success
        ConfigRouteHandler-->>Client: Return config object
    end
Loading

PlexServerService: Protection GUIDs Gathering

sequenceDiagram
    participant PlexServerService
    participant PlexAPI
    participant Users

    PlexServerService->>PlexAPI: Discover server connections
    PlexServerService->>PlexAPI: Fetch users and tokens
    loop For each user
        PlexServerService->>PlexAPI: Find or create protection playlist
        PlexServerService->>PlexAPI: Fetch playlist items
        PlexServerService->>PlexAPI: Fetch item metadata for GUIDs
    end
    PlexServerService-->>PlexServerService: Cache protected GUIDs
Loading

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@jamcalli
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented May 11, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@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: 0

🔭 Outside diff range comments (3)
src/routes/v1/config/config.ts (2)

18-22: ⚠️ Potential issue

Add 400-response schema for consistency with runtime behaviour

The catch-block below can return a 400 (e.g. if upstream helpers throw fastify.badRequest).
The current response schema only lists 200|404|500, which causes Fastify to return 500 validation errors for otherwise valid 400 responses.

         response: {
+          400: ConfigErrorSchema,
         },

75-79: ⚠️ Potential issue

PUT handler returns 404 but schema doesn’t declare it

Later in the handler (reply.status(404)), the code legitimately answers with a 404 when the config cannot be re-loaded.
Declare the status so Fastify’s response validation passes.

         response: {
+          404: ConfigErrorSchema,
         },
src/services/delete-sync.service.ts (1)

770-813: 🛠️ Refactor suggestion

Iterating over raw movie.guids / show.guids risks character-by-character loops

RadarrItem.guids and SonarrItem.guids can be either string (JSON / CSV) or string[].
Directly doing for … of movie.guids or movie.guids.some():

  1. Treats a single string as an iterable of characters, producing false positives.
  2. Silently bypasses the imported parseGuids helper, leaving malformed data un-normalised.

Fix by parsing once and re-using the result:

-        const exists = movie.guids.some((guid) => watchlistGuids.has(guid))
+        const movieGuidList = parseGuids(movie.guids)
+        const exists = movieGuidList.some((guid) => watchlistGuids.has(guid))-              for (const guid of movie.guids) {
+              for (const guid of movieGuidList) {
                 if (this.protectedGuids.has(guid)) {
…
-        const exists = show.guids.some((guid) => watchlistGuids.has(guid))
+        const showGuidList = parseGuids(show.guids)
+        const exists = showGuidList.some((guid) => watchlistGuids.has(guid))-              for (const guid of show.guids) {
+              for (const guid of showGuidList) {
                 if (this.protectedGuids.has(guid)) {

This eliminates incorrect character-level comparisons and leverages the already-imported utility.
Don’t forget to use movieGuidList[0] / showGuidList[0] when pushing to the summary arrays.

Also applies to: 909-977

🧹 Nitpick comments (3)
src/utils/plex.ts (1)

1114-1120: Improved error handling for non-existent items

This change optimizes performance by immediately returning an empty set when a 404 is encountered rather than retrying. Both direct HTTP 404 responses and error messages containing "HTTP 404" are now properly handled.

Consider consolidating the duplicate warning messages for 404 errors to maintain consistency:

-        log.warn(
-          `Item "${item.title}" not found in Plex database (HTTP 404) - skipping retries`,
-        )
+        log.warn(
+          `Item "${item.title}" not found in Plex's database (HTTP 404) - skipping retries`,
+        )

Also applies to: 1200-1206

src/services/delete-sync.service.ts (1)

1131-1134: Clear local protectedGuids cache between runs

plexServer.clearWorkflowCaches() purges its internal caches, but the service-level this.protectedGuids set persists, leading to stale protection data on the next run if playlist protection is later disabled or playlists change.

     // Release cached resources after processing completes
     this.plexServer.clearWorkflowCaches()
+    this.protectedGuids = null
src/utils/plex-server.ts (1)

286-297: Add request timeout to avoid hanging on Plex.tv

A network glitch can leave the fetch to /api/v2/resources unresolved indefinitely, stalling initialisation.
Pass an AbortSignal.timeout(...) (Node 18+) or a manual timeout wrapper.

-      const resourcesResponse = await fetch(resourcesUrl.toString(), {
+      const resourcesResponse = await fetch(resourcesUrl.toString(), {
         headers: {
           Accept: 'application/json',
           'X-Plex-Token': adminToken,
           'X-Plex-Client-Identifier': 'Pulsarr',
-        },
+        },
+        signal: AbortSignal.timeout(8000),
       })

Apply the same pattern to other external fetch calls for resiliency.

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 088860e and 7bc7c95.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • migrations/migrations/022_20250508_add_plex_playlist_protection.ts (1 hunks)
  • package.json (1 hunks)
  • src/client/features/utilities/components/delete-sync/delete-sync-dry-run-modal.tsx (2 hunks)
  • src/client/features/utilities/components/delete-sync/delete-sync-form.tsx (1 hunks)
  • src/client/features/utilities/hooks/useDeleteSyncForm.ts (6 hunks)
  • src/plugins/external/env.ts (1 hunks)
  • src/routes/v1/config/config.ts (6 hunks)
  • src/schemas/config/config.schema.ts (1 hunks)
  • src/schemas/scheduler/scheduler.schema.ts (2 hunks)
  • src/services/database.service.ts (2 hunks)
  • src/services/delete-sync.service.ts (21 hunks)
  • src/types/config.types.ts (1 hunks)
  • src/types/delete-sync.types.ts (1 hunks)
  • src/utils/plex-server.ts (1 hunks)
  • src/utils/plex.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/services/delete-sync.service.ts (1)
Learnt from: jamcalli
PR: jamcalli/Pulsarr#110
File: src/services/watchlist-workflow.service.ts:345-349
Timestamp: 2025-04-25T06:50:49.751Z
Learning: In the watchlist workflow service, the `createItemMap` function intentionally uses only the first GUID from `parseGuids(item.guids)` as the map key. This is by design because the change detection logic relies on a 1:1 mapping between items and their primary GUIDs. The `parseGuids()` function handles GUID normalization and prioritization, ensuring consistent primary GUID selection that matches how Plex, Sonarr, and Radarr handle primary identifiers.
🧬 Code Graph Analysis (5)
src/client/features/utilities/components/delete-sync/delete-sync-form.tsx (3)
src/client/components/ui/form.tsx (5)
  • FormField (182-182)
  • FormItem (177-177)
  • FormControl (179-179)
  • FormLabel (178-178)
  • FormMessage (181-181)
src/client/components/ui/switch.tsx (1)
  • Switch (46-46)
src/client/components/ui/input.tsx (1)
  • Input (25-25)
src/routes/v1/config/config.ts (1)
src/schemas/config/config.schema.ts (3)
  • ConfigResponseSchema (72-75)
  • ConfigErrorSchema (77-79)
  • ConfigSchema (28-70)
src/services/delete-sync.service.ts (1)
src/utils/plex-server.ts (1)
  • PlexServerService (158-1518)
src/utils/plex-server.ts (4)
src/services/delete-sync.service.ts (1)
  • config (79-81)
src/types/config.types.ts (1)
  • Config (38-106)
src/utils/plex.ts (1)
  • toItemsSingle (1054-1231)
src/utils/guid-handler.ts (1)
  • parseGuids (8-61)
src/services/database.service.ts (5)
src/services/delete-sync.service.ts (1)
  • config (79-81)
src/services/discord-notifications.service.ts (1)
  • config (49-51)
src/services/apprise-notifications.service.ts (1)
  • config (21-23)
src/services/watchlist-workflow.service.ts (1)
  • config (110-112)
src/services/plex-watchlist.service.ts (1)
  • config (35-37)
🔇 Additional comments (23)
package.json (1)

96-96:

Details

✅ Verification successful

Good addition of the XML parser dependency.

The fast-xml-parser dependency is appropriate for handling Plex API responses, which are typically in XML format. This is necessary for the new Plex playlist protection feature.


🏁 Script executed:

#!/bin/bash
# Check for the latest version of fast-xml-parser and any security issues

# Get the latest version from npm
echo "Current specified version: ^5.2.2"
echo "Checking latest version on npm..."
npm view fast-xml-parser version

# Check for any security advisories
echo -e "\nChecking for security advisories..."
npm audit --json fast-xml-parser | grep -i vulner || echo "No direct vulnerabilities found"

Length of output: 755


fast-xml-parser dependency version is up-to-date and secure

  • package.json (line 96): "fast-xml-parser": "^5.2.2" has been confirmed as the latest release (v5.2.2).
  • No direct vulnerabilities were reported by npm audit (pending creation of a lockfile).

This dependency is well-suited for parsing Plex’s XML responses and supports the new playlist protection feature.

src/schemas/config/config.schema.ts (1)

62-65: Well-structured schema additions for Plex playlist protection.

The new configuration options are properly organized and follow the codebase's existing patterns. All fields are correctly marked as optional.

A few observations:

  • The enablePlexPlaylistProtection boolean toggle controls the feature
  • The plexProtectionPlaylistName allows customizing the playlist name
  • The plexServerUrl provides the connection endpoint for the Plex server
src/client/features/utilities/components/delete-sync/delete-sync-dry-run-modal.tsx (2)

262-264: Good responsive grid adaptation for the new card.

The conditional grid layout adjustment based on the presence of protected items is well-implemented. It maintains proper spacing and alignment while accommodating the new information card.


292-306: Well-implemented protected items card with appropriate conditional rendering.

The card follows the same design pattern as other summary cards and only appears when there are protected items to report. This keeps the UI clean and focused.

src/schemas/scheduler/scheduler.schema.ts (2)

103-104: Good schema extension for tracking protected items.

Adding the optional protected field to the content type result schema maintains backward compatibility while supporting the new feature.


112-113: Well-integrated protection tracking in total summary.

The optional protected field in the total object ensures consistent reporting of protected items at all levels of the result schema.

src/types/config.types.ts (1)

92-94: Well-structured Plex protection config additions

The new configuration properties for Plex playlist protection are clearly named and well-documented, especially the helpful comment explaining when to set the optional plexServerUrl.

src/types/delete-sync.types.ts (1)

6-6: Good approach with optional protected counts

Making the protected properties optional ensures backward compatibility with existing code that might not be aware of this new field.

Also applies to: 11-11, 17-17

src/plugins/external/env.ts (1)

168-179: Sensible defaults for Plex protection settings

The default values are well-chosen:

  • Protection disabled by default (safer option)
  • Clear and intuitive "Do Not Delete" playlist name
  • Standard Plex port for server URL
src/utils/plex.ts (1)

1054-1054: Function now exported for use by other modules

The toItemsSingle function has been exported, making it available to other modules that need to use it, like the new PlexServerService.

src/client/features/utilities/components/delete-sync/delete-sync-form.tsx (2)

433-449: LGTM - Plex playlist protection toggle implemented correctly.

The implementation of the toggle switch for enabling Plex playlist protection follows the established pattern of other safety settings in the form.


451-485: Input field correctly handles dependent state.

The implementation properly:

  • Disables the input when protection is turned off
  • Uses consistent placeholder value ("Do Not Delete")
  • Handles form submission state
  • Adapts to mobile layouts
  • Includes proper error message display

The conditional disabling based on the enablePlexPlaylistProtection toggle is a good UX choice.

migrations/migrations/022_20250508_add_plex_playlist_protection.ts (3)

3-15: Excellent documentation of the migration purpose.

The comments clearly explain the purpose of each added column and provide context about the auto-detection feature for the Plex server URL.


16-22: LGTM - Proper implementation of database schema changes.

The migration correctly adds the three required columns with appropriate default values:

  • enablePlexPlaylistProtection defaulting to false
  • plexProtectionPlaylistName defaulting to "Do Not Delete"
  • plexServerUrl defaulting to "http://localhost:32400"

24-33: LGTM - Down migration is properly implemented.

The down migration correctly drops all columns added in the up migration, enabling clean rollback if needed.

src/client/features/utilities/hooks/useDeleteSyncForm.ts (6)

17-18: Schema validation is properly defined.

The schema correctly defines:

  • enablePlexPlaylistProtection as a boolean
  • plexProtectionPlaylistName as a string with minimum length validation

130-132: Default values align with database defaults.

The default values match those defined in the migration:

  • enablePlexPlaylistProtection defaults to false
  • plexProtectionPlaylistName defaults to "Do Not Delete"

159-163: Form reset logic handles all fields consistently.

The form reset logic properly includes the new Plex protection fields with appropriate fallbacks to default values.


197-199: Config update includes new protection fields.

The form submission properly includes the Plex protection fields in the configuration update payload.


262-266: Success handler properly resets form with updated values.

After a successful save, the form is correctly reset with the updated configuration values for the Plex protection fields.


308-312: Cancel handler properly resets to current config values.

The cancel handler correctly resets the form to the current configuration values for all fields, including the Plex protection fields.

src/services/database.service.ts (2)

547-553: Appropriate implementation of Plex playlist protection configuration.

The new properties for Plex playlist protection follow the existing patterns in the codebase with proper default values and type conversions.


607-613: Well-implemented default values for Plex playlist protection.

The configuration creation logic correctly handles defaults and properly maintains consistency with the retrieval method.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant