From e28c96c8b021cd2e2b284382cd6b893db1c7b4c1 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 16 Sep 2026 19:09:09 +0000 Subject: [PATCH] fix(plex): Re-add missing component artwork api route Fixes #718 --- src/backend/server/api.ts | 28 ++++++++++++++++++++++++++++ src/backend/sources/PlexApiSource.ts | 9 ++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index 066fecc8..33570f06 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -37,6 +37,7 @@ import type { createTypedRouter, TypedMiddleware } from "@minisylar/express-type import { hasMetricRepositories, registerMetrics, setMetricRepositories } from "./promMetrics.ts"; import pMap from "p-map"; import type { PlayWith } from "../common/database/drizzle/drizzleTypes.ts"; +import { stripIndents } from "common-tags"; const maxBufferSize = 300; const output: Record> = {}; @@ -385,6 +386,33 @@ export const setupApi = (args: ApiArgs, opts: ApiOptions = {}) => { } }); + router.get('/api/components/:id/art', { + middleware: [componentAwareMiddle], + querySchema: z.looseObject({}), + tags: ['Source/Client'], + summary: 'Get External Art', + description: stripIndents`Fetches and returns art from the upstream/downstream external service +Note: this is only supported by some components.` + }, async (req, res, next) => { + const { + component, + query + } = req; + + if('getExternalArt' in component && typeof component.getExternalArt === 'function') { + const [stream, contentType] = await component.getExternalArt(query); + res.writeHead(200, {'Content-Type': contentType}); + try { + return stream.pipe(res); + } catch (e) { + logger.error(new Error(`Error occurred while trying to stream art for Component ${component.componentId}`, {cause: e})); + return res.status(500).json({message: 'Error during art retrieval'}); + } + } + + return res.status(501).json({error: {message: `Component ${component.componentId} does not support art retrieval`}}); + }); + router.get('/api/components/:id/plays', { middleware: [componentAwareMiddle], tags: ['Plays'], diff --git a/src/backend/sources/PlexApiSource.ts b/src/backend/sources/PlexApiSource.ts index 81902f65..4449f17e 100644 --- a/src/backend/sources/PlexApiSource.ts +++ b/src/backend/sources/PlexApiSource.ts @@ -83,7 +83,6 @@ export default class PlexApiSource extends MemoryPositionalSource { } protected async doBuildInitData(): Promise { - this.regexCache const { data: { token, @@ -262,7 +261,7 @@ export default class PlexApiSource extends MemoryPositionalSource { } catch (e) { this.logger.error(new Error('Cannot start polling because Plex prerequisite data could not be built', {cause: e})); return false; - }5 + } } isActivityValid = (state: PlayerStateDataMaybePlay, session: GetSessionsMetadata): boolean | string => { @@ -328,7 +327,7 @@ export default class PlexApiSource extends MemoryPositionalSource { meta: { ...play.meta, art: { - track: `/api/source/art?name=${this.name}&type=${this.type}&data=${res.named.ratingkey}` + track: `/api/components/${this.componentId}/art?ratingKey=${res.named.ratingkey}` } } } @@ -474,10 +473,10 @@ export default class PlexApiSource extends MemoryPositionalSource { return await this.processRecentPlays(validSessions); } - getSourceArt = async (data: string): Promise<[Readable, string]> => { + getExternalArt = async (data: {ratingKey: string}): Promise<[Readable, string]> => { try { const resp = await this.plexApi.media.getThumbImage({ - ratingKey: parseInt(data), + ratingKey: parseInt(data.ratingKey), width: 250, height: 250, minSize: 1, -- 2.51.2