From 9137f6850dbb40aeed6008bdb5f8638a79c73c5e Mon Sep 17 00:00:00 2001 From: Christian Benincasa Date: Tue, 26 Mar 2024 13:38:25 -0400 Subject: [PATCH] Add skeleton Plex webhook impl --- server/package.json | 2 +- server/src/api/index.ts | 4 +++- server/src/api/plexWebhookApi.ts | 36 ++++++++++++++++++++++++++++++++ types/src/plex/index.ts | 30 ++++++++++++++++++++++++++ types/src/plex/webhooks.ts | 36 ++++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 server/src/api/plexWebhookApi.ts create mode 100644 types/src/plex/webhooks.ts diff --git a/server/package.json b/server/package.json index 3956bcf6..68779fdd 100644 --- a/server/package.json +++ b/server/package.json @@ -14,7 +14,7 @@ "bundle": "tsx scripts/bundle.ts", "clean": "rimraf build", "debug": "tsx watch --tsconfig ./tsconfig.build.json --ignore 'src/streams' --inspect-brk src", - "dev": "NODE_ENV=development tsx watch --tsconfig ./tsconfig.build.json --ignore 'build' --ignore 'src/streams' src", + "dev": "NODE_ENV=development TUNARR_BIND_ADDR='0.0.0.0' tsx watch --tsconfig ./tsconfig.build.json --ignore 'build' --ignore 'src/streams' src", "make-exec": "tsx scripts/makeExecutable.ts", "generate-db-cache": "mikro-orm-esm cache:generate --combined --ts", "mikro-orm": "mikro-orm-esm", diff --git a/server/src/api/index.ts b/server/src/api/index.ts index ca55c11f..e6dbb21c 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -18,6 +18,7 @@ import { customShowsApiV2 } from './customShowsApi.js'; import { debugApi } from './debugApi.js'; import { fillerListsApi } from './fillerListsApi.js'; import { metadataApiRouter } from './metadataApi.js'; +import { plexWebhookRouter } from './plexWebhookApi.js'; import { programmingApi } from './programmingApi.js'; import { tasksApiRouter } from './tasksApi.js'; @@ -40,7 +41,8 @@ export const apiRouter: RouterPluginAsyncCallback = async (fastify) => { .register(fillerListsApi) .register(programmingApi) .register(debugApi) - .register(metadataApiRouter); + .register(metadataApiRouter) + .register(plexWebhookRouter); fastify.get( '/version', diff --git a/server/src/api/plexWebhookApi.ts b/server/src/api/plexWebhookApi.ts new file mode 100644 index 00000000..cb206108 --- /dev/null +++ b/server/src/api/plexWebhookApi.ts @@ -0,0 +1,36 @@ +import { PlexWebhookPayloadSchema } from '@tunarr/types/plex'; +import createLogger from '../logger'; +import { RouterPluginAsyncCallback } from '../types/serverType'; + +const logger = createLogger(import.meta); + +// eslint-disable-next-line @typescript-eslint/require-await +export const plexWebhookRouter: RouterPluginAsyncCallback = async (f) => { + f.post( + '/plex/webhook', + { + schema: { + consumes: ['multipart/form-data'], + }, + }, + async (req, res) => { + for await (const part of req.parts()) { + if (part.type === 'field' && part.mimetype === 'application/json') { + const parseResult = await PlexWebhookPayloadSchema.safeParseAsync( + part.value, + ); + logger.info('Payload: %O', part.value); + if (!parseResult.success) { + logger.error( + 'Unable to parse Plex webhook payload: %O', + parseResult.error, + ); + return res.status(400).send(); + } + } + } + + return res.send(); + }, + ); +}; diff --git a/types/src/plex/index.ts b/types/src/plex/index.ts index 1bccf069..e1d20439 100644 --- a/types/src/plex/index.ts +++ b/types/src/plex/index.ts @@ -1,5 +1,6 @@ export * from './dvr.js'; import z from 'zod'; +import { PlexWebhookBasePayloadSchema } from './webhooks.js'; type Alias = t & { _?: never }; @@ -589,6 +590,29 @@ export type PlexCollectionContents = | PlexMovieCollectionContents | PlexTvShowCollectionContents; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +// const PlexMediaSchema = z.discriminatedUnion('type', [ +// PlexMovieSchema, +// PlexTvShowSchema, +// PlexTvSeasonSchema, +// PlexEpisodeSchema, +// PlexLibraryCollectionSchema, +// PlexMusicArtistSchema, +// PlexMusicAlbumSchema, +// PlexMusicTrackSchema, +// ]); + +const PartialPlexMediaSchema = z.discriminatedUnion('type', [ + PlexMovieSchema.partial().required({ type: true }), + PlexTvShowSchema.partial().required({ type: true }), + PlexTvSeasonSchema.partial().required({ type: true }), + PlexEpisodeSchema.partial().required({ type: true }), + PlexLibraryCollectionSchema.partial().required({ type: true }), + PlexMusicArtistSchema.partial().required({ type: true }), + PlexMusicAlbumSchema.partial().required({ type: true }), + PlexMusicTrackSchema.partial().required({ type: true }), +]); + export type PlexMedia = Alias< | PlexMovie | PlexTvShow @@ -703,3 +727,9 @@ export const PlexResourcesResponseSchema = z.array(PlexResourceSchema); export type PlexResourcesResponse = Alias< z.infer >; + +export * from './webhooks.js'; + +export const PlexWebhookPayloadSchema = PlexWebhookBasePayloadSchema.extend({ + Metadata: PartialPlexMediaSchema, +}); diff --git a/types/src/plex/webhooks.ts b/types/src/plex/webhooks.ts new file mode 100644 index 00000000..569a5c0e --- /dev/null +++ b/types/src/plex/webhooks.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +// Extend with Metadata value in the main file... once we better organize +// the schemas here we can split files +export const PlexWebhookBasePayloadSchema = z.object({ + event: z.union([ + z.literal('library.on.deck'), + z.literal('library.new'), + z.literal('media.pause'), + z.literal('media.play'), + z.literal('media.rate'), + z.literal('media.resume'), + z.literal('media.scrobble'), + z.literal('media.stop'), + // We don't care about the server owner events, at the momen + ]), + user: z.boolean(), + owner: z.boolean(), + Account: z.object({ + id: z.number(), + thumb: z.string(), + title: z.string(), + }), + Server: z.object({ + title: z.string(), // Name + uuid: z.string(), // 'client_identifier' + }), + Player: z + .object({ + local: z.boolean(), + publicAddress: z.string(), + title: z.string(), + uuid: z.string(), + }) + .optional(), // Only defined on play events +});