diff --git a/docs/using-seerr/plex/index.md b/docs/using-seerr/plex/index.md index 68b4d1f549..bad7af9526 100644 --- a/docs/using-seerr/plex/index.md +++ b/docs/using-seerr/plex/index.md @@ -11,6 +11,7 @@ Seerr provides integration features that connect with your Plex media server to ## Available Features - [Watchlist Auto Request](./watchlist-auto-request) - Automatically request media from your Plex Watchlist +- [Recently Added Processing](#recently-added-processing) - Process newly added Plex media from external tools - More features coming soon! ## Prerequisites @@ -34,3 +35,33 @@ To use any Plex integration features, you must have logged into Seerr at least o :::note Server Configuration Plex server configuration is handled by your administrator. If you cannot log in with your Plex account, contact your administrator to verify the server setup. ::: + +## Recently Added Processing + +Seerr can process a recently added Plex item from an external tool such as Tautulli. Configure the tool to send a `POST` request to: + +```text +https://seerr.example.com/api/v1/plex/recently-added +``` + +Include your Seerr API key as an HTTP header: + +```text +X-Api-Key: YOUR_SEERR_API_KEY +``` + +The webhook body must be JSON and include the Plex rating key: + +```json +{ + "ratingKey": "12345" +} +``` + +For Tautulli recently added notifications, configure a webhook notification using this custom JSON body: + +```json +{ + "ratingKey": "{rating_key}" +} +``` diff --git a/seerr-api.yml b/seerr-api.yml index 18f3361d6a..83c7087fde 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -2455,6 +2455,41 @@ paths: application/json: schema: $ref: '#/components/schemas/PlexSettings' + /plex/recently-added: + post: + summary: Process recently added Plex media + description: Processes a recently added Plex media item by rating key. This endpoint can be called by external tools such as Tautulli. + tags: + - plex + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + ratingKey: + type: string + required: + - ratingKey + responses: + '200': + description: Plex media was processed + content: + application/json: + schema: + type: object + properties: + ratingKey: + type: string + type: + type: string + title: + type: string + '400': + description: Plex rating key is missing + '500': + description: Plex media could not be processed /settings/plex/library: get: summary: Get Plex libraries diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index 617203447f..fec5e40b58 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -50,8 +50,11 @@ interface PlexLibrariesResponse { export interface PlexMetadata { ratingKey: string; parentRatingKey?: string; + grandparentRatingKey?: string; guid: string; - type: 'movie' | 'show' | 'season'; + parentGuid?: string; + grandparentGuid?: string; + type: 'movie' | 'show' | 'season' | 'episode'; title: string; Guid: { id: string; diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index ce746e71c9..2a06e33f47 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -159,6 +159,34 @@ class PlexScanner } } + public async processRatingKey(ratingKey: string): Promise { + const userRepository = getRepository(User); + const admin = await userRepository.findOne({ + select: { id: true, plexToken: true }, + where: { id: 1 }, + }); + + if (!admin) { + throw new Error('No admin configured. Plex media processing skipped.'); + } + + const settings = getSettings(); + this.plexClient = new PlexAPI({ plexToken: admin.plexToken }); + this.libraries = settings.plex.libraries.filter( + (library) => library.enabled + ); + + const hasHama = await this.hasHamaAgent(); + if (hasHama) { + await animeList.sync(); + } + + const metadata = await this.plexClient.getMetadata(ratingKey); + await this.processItem(metadata); + + return metadata; + } + private async paginateLibrary( library: Library, { start = 0, sessionId }: { start?: number; sessionId: string } diff --git a/server/routes/index.ts b/server/routes/index.ts index f701acf968..004fe38b1e 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -37,6 +37,7 @@ import issueCommentRoutes from './issueComment'; import mediaRoutes from './media'; import movieRoutes from './movie'; import personRoutes from './person'; +import plexRoutes from './plex'; import requestRoutes from './request'; import searchRoutes from './search'; import serviceRoutes from './service'; @@ -148,6 +149,7 @@ router.get( } ); router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes); +router.use('/plex', isAuthenticated(Permission.ADMIN), plexRoutes); router.use('/search', isAuthenticated(), searchRoutes); router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/request', isAuthenticated(), requestRoutes); diff --git a/server/routes/plex.ts b/server/routes/plex.ts new file mode 100644 index 0000000000..0c21038d67 --- /dev/null +++ b/server/routes/plex.ts @@ -0,0 +1,39 @@ +import { plexRecentScanner } from '@server/lib/scanners/plex'; +import logger from '@server/logger'; +import { Router } from 'express'; + +const plexRoutes = Router(); + +plexRoutes.post('/recently-added', async (req, res, next) => { + const ratingKey = req.body?.ratingKey; + + if (!ratingKey || typeof ratingKey !== 'string') { + return next({ + status: 400, + message: 'Plex ratingKey is required.', + }); + } + + try { + const metadata = await plexRecentScanner.processRatingKey(ratingKey); + + return res.status(200).json({ + ratingKey: metadata.ratingKey, + type: metadata.type, + title: metadata.title, + }); + } catch (e) { + logger.error('Failed to process pushed Plex recently added media', { + label: 'Plex Scan', + ratingKey, + errorMessage: e.message, + }); + + return next({ + status: 500, + message: 'Unable to process Plex media.', + }); + } +}); + +export default plexRoutes;