Initial release: n8n Plex Media Server node
CI / build (push) Failing after 46s

Features:
- Server: Get identity, info, preferences, sessions, butler tasks
- Library: Get all, get contents, recently added, on deck, refresh, analyze
- Media: Get, update, delete, refresh, mark watched/unwatched
- Playlist: CRUD operations, add/remove items
- Collection: CRUD operations, add/remove items
- Search: Global and library-specific search
- Session: Get active sessions, history, terminate

Includes CI/CD pipeline for automatic npm publishing on release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-14 10:12:36 +01:00
co-authored by Claude
commit ed55175994
22 changed files with 7347 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
IDataObject,
IHttpRequestMethods,
IHttpRequestOptions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
export async function plexApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs: IDataObject = {},
): Promise<IDataObject | IDataObject[]> {
const credentials = await this.getCredentials('plexApi');
const options: IHttpRequestOptions = {
method,
url: `${credentials.serverUrl}${endpoint}`,
headers: {
'X-Plex-Token': credentials.plexToken as string,
'X-Plex-Client-Identifier': credentials.clientIdentifier as string,
'X-Plex-Product': credentials.product as string,
'X-Plex-Version': credentials.version as string,
'X-Plex-Platform': credentials.platform as string,
Accept: 'application/json',
},
qs,
json: true,
};
if (Object.keys(body).length > 0) {
options.body = body;
}
try {
const response = await this.helpers.httpRequest(options);
return response as IDataObject;
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function plexApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs: IDataObject = {},
containerKey: string = 'MediaContainer',
itemsKey: string = 'Metadata',
): Promise<IDataObject[]> {
const returnData: IDataObject[] = [];
let responseData: IDataObject;
let start = 0;
const size = 100;
do {
qs['X-Plex-Container-Start'] = start;
qs['X-Plex-Container-Size'] = size;
responseData = (await plexApiRequest.call(this, method, endpoint, body, qs)) as IDataObject;
const container = responseData[containerKey] as IDataObject;
if (container && container[itemsKey]) {
const items = container[itemsKey] as IDataObject[];
returnData.push(...items);
}
const totalSize = (container?.totalSize as number) || 0;
if (returnData.length >= totalSize || !container || !container[itemsKey]) {
break;
}
start += size;
} while (true);
return returnData;
}
export function buildPlexUrl(baseUrl: string, endpoint: string, params: IDataObject = {}): string {
const url = new URL(endpoint, baseUrl);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== '') {
url.searchParams.append(key, String(value));
}
}
return url.toString();
}
+700
View File
@@ -0,0 +1,700 @@
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { plexApiRequest, plexApiRequestAllItems } from './GenericFunctions';
import {
serverOperations,
serverFields,
libraryOperations,
libraryFields,
mediaOperations,
mediaFields,
playlistOperations,
playlistFields,
searchOperations,
searchFields,
sessionOperations,
sessionFields,
collectionOperations,
collectionFields,
} from './descriptions';
export class Plex implements INodeType {
description: INodeTypeDescription = {
displayName: 'Plex',
name: 'plex',
icon: 'file:plex.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with Plex Media Server API',
defaults: {
name: 'Plex',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'plexApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Collection',
value: 'collection',
},
{
name: 'Library',
value: 'library',
},
{
name: 'Media',
value: 'media',
},
{
name: 'Playlist',
value: 'playlist',
},
{
name: 'Search',
value: 'search',
},
{
name: 'Server',
value: 'server',
},
{
name: 'Session',
value: 'session',
},
],
default: 'library',
},
// Operations
...serverOperations,
...libraryOperations,
...mediaOperations,
...playlistOperations,
...searchOperations,
...sessionOperations,
...collectionOperations,
// Fields
...serverFields,
...libraryFields,
...mediaFields,
...playlistFields,
...searchFields,
...sessionFields,
...collectionFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) {
try {
let responseData: IDataObject | IDataObject[];
// ==================== SERVER ====================
if (resource === 'server') {
if (operation === 'getIdentity') {
responseData = await plexApiRequest.call(this, 'GET', '/identity');
} else if (operation === 'getInfo') {
responseData = await plexApiRequest.call(this, 'GET', '/');
} else if (operation === 'getPreferences') {
responseData = await plexApiRequest.call(this, 'GET', '/prefs');
} else if (operation === 'getActiveSessions') {
responseData = await plexApiRequest.call(this, 'GET', '/status/sessions');
} else if (operation === 'getTranscodeSessions') {
responseData = await plexApiRequest.call(this, 'GET', '/transcode/sessions');
} else if (operation === 'getButlerTasks') {
responseData = await plexApiRequest.call(this, 'GET', '/butler');
} else if (operation === 'runButlerTask') {
const taskName = this.getNodeParameter('taskName', i) as string;
responseData = await plexApiRequest.call(this, 'POST', `/butler/${taskName}`);
} else if (operation === 'stopButlerTask') {
const taskName = this.getNodeParameter('taskName', i) as string;
responseData = await plexApiRequest.call(this, 'DELETE', `/butler/${taskName}`);
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported for resource "${resource}"`,
{ itemIndex: i },
);
}
}
// ==================== LIBRARY ====================
else if (resource === 'library') {
if (operation === 'getAll') {
responseData = await plexApiRequest.call(this, 'GET', '/library/sections');
} else if (operation === 'get') {
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
responseData = await plexApiRequest.call(this, 'GET', `/library/sections/${libraryKey}`);
} else if (operation === 'getContents') {
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
const qs: IDataObject = {};
if (options.type) {
qs.type = options.type;
}
if (options.sort) {
qs.sort = options.sort;
}
if (options.unwatched) {
qs.unwatched = '1';
}
if (returnAll) {
responseData = await plexApiRequestAllItems.call(
this,
'GET',
`/library/sections/${libraryKey}/all`,
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs['X-Plex-Container-Size'] = limit;
const response = await plexApiRequest.call(
this,
'GET',
`/library/sections/${libraryKey}/all`,
{},
qs,
);
const container = (response as IDataObject).MediaContainer as IDataObject;
responseData = (container?.Metadata as IDataObject[]) || [];
}
} else if (operation === 'getRecentlyAdded') {
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const qs: IDataObject = {};
if (returnAll) {
responseData = await plexApiRequestAllItems.call(
this,
'GET',
`/library/sections/${libraryKey}/recentlyAdded`,
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs['X-Plex-Container-Size'] = limit;
const response = await plexApiRequest.call(
this,
'GET',
`/library/sections/${libraryKey}/recentlyAdded`,
{},
qs,
);
const container = (response as IDataObject).MediaContainer as IDataObject;
responseData = (container?.Metadata as IDataObject[]) || [];
}
} else if (operation === 'getOnDeck') {
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
responseData = await plexApiRequest.call(
this,
'GET',
`/library/sections/${libraryKey}/onDeck`,
);
} else if (operation === 'refresh') {
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
const refreshOptions = this.getNodeParameter('refreshOptions', i) as IDataObject;
const qs: IDataObject = {};
if (refreshOptions.force) {
qs.force = '1';
}
responseData = await plexApiRequest.call(
this,
'GET',
`/library/sections/${libraryKey}/refresh`,
{},
qs,
);
} else if (operation === 'emptyTrash') {
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
responseData = await plexApiRequest.call(
this,
'PUT',
`/library/sections/${libraryKey}/emptyTrash`,
);
} else if (operation === 'analyze') {
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
responseData = await plexApiRequest.call(
this,
'PUT',
`/library/sections/${libraryKey}/analyze`,
);
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported for resource "${resource}"`,
{ itemIndex: i },
);
}
}
// ==================== MEDIA ====================
else if (resource === 'media') {
const ratingKey = this.getNodeParameter('ratingKey', i) as string;
if (operation === 'get') {
responseData = await plexApiRequest.call(
this,
'GET',
`/library/metadata/${ratingKey}`,
);
} else if (operation === 'getChildren') {
responseData = await plexApiRequest.call(
this,
'GET',
`/library/metadata/${ratingKey}/children`,
);
} else if (operation === 'getRelated') {
const options = this.getNodeParameter('options', i) as IDataObject;
const qs: IDataObject = {};
if (options.limit) {
qs.count = options.limit;
}
responseData = await plexApiRequest.call(
this,
'GET',
`/library/metadata/${ratingKey}/related`,
{},
qs,
);
} else if (operation === 'getSimilar') {
const options = this.getNodeParameter('options', i) as IDataObject;
const qs: IDataObject = {};
if (options.limit) {
qs.limit = options.limit;
}
responseData = await plexApiRequest.call(
this,
'GET',
`/library/metadata/${ratingKey}/similar`,
{},
qs,
);
} else if (operation === 'update') {
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const qs: IDataObject = {};
if (updateFields.title) {
qs['title.value'] = updateFields.title;
}
if (updateFields.titleSort) {
qs['titleSort.value'] = updateFields.titleSort;
}
if (updateFields.originallyAvailableAt) {
qs['originallyAvailableAt.value'] = updateFields.originallyAvailableAt;
}
if (updateFields.studio) {
qs['studio.value'] = updateFields.studio;
}
if (updateFields.contentRating) {
qs['contentRating.value'] = updateFields.contentRating;
}
if (updateFields.summary) {
qs['summary.value'] = updateFields.summary;
}
if (updateFields.tagline) {
qs['tagline.value'] = updateFields.tagline;
}
if (updateFields.rating !== undefined) {
qs['rating.value'] = updateFields.rating;
}
responseData = await plexApiRequest.call(
this,
'PUT',
`/library/metadata/${ratingKey}`,
{},
qs,
);
} else if (operation === 'delete') {
responseData = await plexApiRequest.call(
this,
'DELETE',
`/library/metadata/${ratingKey}`,
);
} else if (operation === 'refresh') {
responseData = await plexApiRequest.call(
this,
'PUT',
`/library/metadata/${ratingKey}/refresh`,
);
} else if (operation === 'analyze') {
responseData = await plexApiRequest.call(
this,
'PUT',
`/library/metadata/${ratingKey}/analyze`,
);
} else if (operation === 'markWatched') {
responseData = await plexApiRequest.call(this, 'GET', '/:/scrobble', {}, {
key: ratingKey,
identifier: 'com.plexapp.plugins.library',
});
} else if (operation === 'markUnwatched') {
responseData = await plexApiRequest.call(this, 'GET', '/:/unscrobble', {}, {
key: ratingKey,
identifier: 'com.plexapp.plugins.library',
});
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported for resource "${resource}"`,
{ itemIndex: i },
);
}
}
// ==================== PLAYLIST ====================
else if (resource === 'playlist') {
if (operation === 'getAll') {
const options = this.getNodeParameter('options', i) as IDataObject;
const qs: IDataObject = {};
if (options.playlistType) {
qs.playlistType = options.playlistType;
}
if (options.smart) {
qs.smart = options.smart;
}
responseData = await plexApiRequest.call(this, 'GET', '/playlists', {}, qs);
} else if (operation === 'get') {
const playlistRatingKey = this.getNodeParameter('playlistRatingKey', i) as string;
responseData = await plexApiRequest.call(
this,
'GET',
`/playlists/${playlistRatingKey}`,
);
} else if (operation === 'getItems') {
const playlistRatingKey = this.getNodeParameter('playlistRatingKey', i) as string;
responseData = await plexApiRequest.call(
this,
'GET',
`/playlists/${playlistRatingKey}/items`,
);
} else if (operation === 'create') {
const title = this.getNodeParameter('title', i) as string;
const playlistType = this.getNodeParameter('playlistType', i) as string;
const uri = this.getNodeParameter('uri', i) as string;
responseData = await plexApiRequest.call(this, 'POST', '/playlists', {}, {
title,
type: playlistType,
uri,
smart: '0',
});
} else if (operation === 'update') {
const playlistRatingKey = this.getNodeParameter('playlistRatingKey', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const qs: IDataObject = {};
if (updateFields.title) {
qs.title = updateFields.title;
}
if (updateFields.summary) {
qs.summary = updateFields.summary;
}
responseData = await plexApiRequest.call(
this,
'PUT',
`/playlists/${playlistRatingKey}`,
{},
qs,
);
} else if (operation === 'delete') {
const playlistRatingKey = this.getNodeParameter('playlistRatingKey', i) as string;
responseData = await plexApiRequest.call(
this,
'DELETE',
`/playlists/${playlistRatingKey}`,
);
} else if (operation === 'addItems') {
const playlistRatingKey = this.getNodeParameter('playlistRatingKey', i) as string;
const itemUri = this.getNodeParameter('itemUri', i) as string;
responseData = await plexApiRequest.call(
this,
'PUT',
`/playlists/${playlistRatingKey}/items`,
{},
{ uri: itemUri },
);
} else if (operation === 'removeItems') {
const playlistRatingKey = this.getNodeParameter('playlistRatingKey', i) as string;
const playlistItemId = this.getNodeParameter('playlistItemId', i) as string;
responseData = await plexApiRequest.call(
this,
'DELETE',
`/playlists/${playlistRatingKey}/items/${playlistItemId}`,
);
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported for resource "${resource}"`,
{ itemIndex: i },
);
}
}
// ==================== SEARCH ====================
else if (resource === 'search') {
if (operation === 'search') {
const query = this.getNodeParameter('query', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const qs: IDataObject = { query };
if (returnAll) {
responseData = await plexApiRequestAllItems.call(
this,
'GET',
'/search',
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs['X-Plex-Container-Size'] = limit;
const response = await plexApiRequest.call(this, 'GET', '/search', {}, qs);
const container = (response as IDataObject).MediaContainer as IDataObject;
responseData = (container?.Metadata as IDataObject[]) || [];
}
} else if (operation === 'searchInLibrary') {
const query = this.getNodeParameter('query', i) as string;
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
const qs: IDataObject = { query };
if (options.type) {
qs.type = options.type;
}
if (returnAll) {
responseData = await plexApiRequestAllItems.call(
this,
'GET',
`/library/sections/${libraryKey}/search`,
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs['X-Plex-Container-Size'] = limit;
const response = await plexApiRequest.call(
this,
'GET',
`/library/sections/${libraryKey}/search`,
{},
qs,
);
const container = (response as IDataObject).MediaContainer as IDataObject;
responseData = (container?.Metadata as IDataObject[]) || [];
}
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported for resource "${resource}"`,
{ itemIndex: i },
);
}
}
// ==================== SESSION ====================
else if (resource === 'session') {
if (operation === 'getAll') {
responseData = await plexApiRequest.call(this, 'GET', '/status/sessions');
} else if (operation === 'getHistory') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
const qs: IDataObject = {};
if (options.sort) {
qs.sort = options.sort;
}
if (options.accountID) {
qs.accountID = options.accountID;
}
if (options.librarySectionID) {
qs.librarySectionID = options.librarySectionID;
}
if (returnAll) {
responseData = await plexApiRequestAllItems.call(
this,
'GET',
'/status/sessions/history/all',
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs['X-Plex-Container-Size'] = limit;
const response = await plexApiRequest.call(
this,
'GET',
'/status/sessions/history/all',
{},
qs,
);
const container = (response as IDataObject).MediaContainer as IDataObject;
responseData = (container?.Metadata as IDataObject[]) || [];
}
} else if (operation === 'terminate') {
const sessionId = this.getNodeParameter('sessionId', i) as string;
const reason = this.getNodeParameter('reason', i, '') as string;
const qs: IDataObject = { sessionId };
if (reason) {
qs.reason = reason;
}
responseData = await plexApiRequest.call(
this,
'GET',
'/status/sessions/terminate',
{},
qs,
);
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported for resource "${resource}"`,
{ itemIndex: i },
);
}
}
// ==================== COLLECTION ====================
else if (resource === 'collection') {
if (operation === 'getAll') {
const libraryKey = this.getNodeParameter('libraryKey', i) as string;
responseData = await plexApiRequest.call(
this,
'GET',
`/library/sections/${libraryKey}/collections`,
);
} else if (operation === 'get') {
const collectionRatingKey = this.getNodeParameter('collectionRatingKey', i) as string;
responseData = await plexApiRequest.call(
this,
'GET',
`/library/collections/${collectionRatingKey}`,
);
} else if (operation === 'getItems') {
const collectionRatingKey = this.getNodeParameter('collectionRatingKey', i) as string;
responseData = await plexApiRequest.call(
this,
'GET',
`/library/collections/${collectionRatingKey}/children`,
);
} else if (operation === 'addItems') {
const collectionRatingKey = this.getNodeParameter('collectionRatingKey', i) as string;
const machineIdentifier = this.getNodeParameter('machineIdentifier', i) as string;
const itemRatingKeys = this.getNodeParameter('itemRatingKeys', i) as string;
const uri = itemRatingKeys
.split(',')
.map((key) => key.trim())
.map(
(key) =>
`server://${machineIdentifier}/com.plexapp.plugins.library/library/metadata/${key}`,
)
.join(',');
responseData = await plexApiRequest.call(
this,
'PUT',
`/library/collections/${collectionRatingKey}/items`,
{},
{ uri },
);
} else if (operation === 'removeItems') {
const collectionRatingKey = this.getNodeParameter('collectionRatingKey', i) as string;
const itemRatingKeys = this.getNodeParameter('itemRatingKeys', i) as string;
const excludeKeys = itemRatingKeys
.split(',')
.map((key) => key.trim())
.join(',');
responseData = await plexApiRequest.call(
this,
'PUT',
`/library/collections/${collectionRatingKey}/items/delete`,
{},
{ excludeRatingKeys: excludeKeys },
);
} else if (operation === 'delete') {
const collectionRatingKey = this.getNodeParameter('collectionRatingKey', i) as string;
responseData = await plexApiRequest.call(
this,
'DELETE',
`/library/collections/${collectionRatingKey}`,
);
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported for resource "${resource}"`,
{ itemIndex: i },
);
}
} else {
throw new NodeOperationError(
this.getNode(),
`The resource "${resource}" is not supported`,
{ itemIndex: i },
);
}
// Handle array or single response
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: (error as Error).message }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,118 @@
import type { INodeProperties } from 'n8n-workflow';
export const collectionOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['collection'],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all collections in a library',
action: 'Get all collections',
},
{
name: 'Get',
value: 'get',
description: 'Get a specific collection',
action: 'Get a collection',
},
{
name: 'Get Items',
value: 'getItems',
description: 'Get items in a collection',
action: 'Get collection items',
},
{
name: 'Add Items',
value: 'addItems',
description: 'Add items to a collection',
action: 'Add items to collection',
},
{
name: 'Remove Items',
value: 'removeItems',
description: 'Remove items from a collection',
action: 'Remove items from collection',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a collection',
action: 'Delete a collection',
},
],
default: 'getAll',
},
];
export const collectionFields: INodeProperties[] = [
// Library Key for getAll
{
displayName: 'Library Key',
name: 'libraryKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['collection'],
operation: ['getAll'],
},
},
default: '',
description: 'The key/ID of the library section',
},
// Collection Rating Key
{
displayName: 'Collection Rating Key',
name: 'collectionRatingKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['collection'],
operation: ['get', 'getItems', 'addItems', 'removeItems', 'delete'],
},
},
default: '',
description: 'The rating key (ID) of the collection',
},
// Machine Identifier (for add/remove items)
{
displayName: 'Machine Identifier',
name: 'machineIdentifier',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['collection'],
operation: ['addItems', 'removeItems'],
},
},
default: '',
description: 'The machine identifier of the Plex server (found in server identity)',
},
// Item Rating Keys for add/remove
{
displayName: 'Item Rating Keys',
name: 'itemRatingKeys',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['collection'],
operation: ['addItems', 'removeItems'],
},
},
default: '',
placeholder: '12345,67890',
description: 'Comma-separated list of rating keys (IDs) of items to add or remove',
},
];
@@ -0,0 +1,250 @@
import type { INodeProperties } from 'n8n-workflow';
export const libraryOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['library'],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all library sections',
action: 'Get all libraries',
},
{
name: 'Get',
value: 'get',
description: 'Get a specific library section',
action: 'Get a library',
},
{
name: 'Get Contents',
value: 'getContents',
description: 'Get all items in a library section',
action: 'Get library contents',
},
{
name: 'Get Recently Added',
value: 'getRecentlyAdded',
description: 'Get recently added items in a library',
action: 'Get recently added items',
},
{
name: 'Get On Deck',
value: 'getOnDeck',
description: 'Get on deck items in a library',
action: 'Get on deck items',
},
{
name: 'Refresh',
value: 'refresh',
description: 'Refresh/scan a library section',
action: 'Refresh a library',
},
{
name: 'Empty Trash',
value: 'emptyTrash',
description: 'Empty the trash for a library section',
action: 'Empty library trash',
},
{
name: 'Analyze',
value: 'analyze',
description: 'Analyze all items in a library section',
action: 'Analyze library',
},
],
default: 'getAll',
},
];
export const libraryFields: INodeProperties[] = [
// Library Key/ID
{
displayName: 'Library Key',
name: 'libraryKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['library'],
operation: [
'get',
'getContents',
'getRecentlyAdded',
'getOnDeck',
'refresh',
'emptyTrash',
'analyze',
],
},
},
default: '',
description: 'The key/ID of the library section',
},
// Return All for getContents
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['library'],
operation: ['getContents', 'getRecentlyAdded'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['library'],
operation: ['getContents', 'getRecentlyAdded'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 50,
description: 'Max number of results to return',
},
// Additional Options for getContents
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['library'],
operation: ['getContents'],
},
},
options: [
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'All',
value: '',
},
{
name: 'Movie',
value: '1',
},
{
name: 'Show',
value: '2',
},
{
name: 'Season',
value: '3',
},
{
name: 'Episode',
value: '4',
},
{
name: 'Trailer',
value: '5',
},
{
name: 'Comic',
value: '6',
},
{
name: 'Person',
value: '7',
},
{
name: 'Artist',
value: '8',
},
{
name: 'Album',
value: '9',
},
{
name: 'Track',
value: '10',
},
{
name: 'Photo Album',
value: '11',
},
{
name: 'Picture',
value: '12',
},
{
name: 'Photo',
value: '13',
},
{
name: 'Clip',
value: '14',
},
{
name: 'Playlist Item',
value: '15',
},
],
default: '',
description: 'Filter by media type',
},
{
displayName: 'Sort',
name: 'sort',
type: 'string',
default: '',
description: 'Sort order (e.g., "titleSort:asc", "addedAt:desc")',
},
{
displayName: 'Unwatched Only',
name: 'unwatched',
type: 'boolean',
default: false,
description: 'Whether to only return unwatched items',
},
],
},
// Refresh Options
{
displayName: 'Options',
name: 'refreshOptions',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['library'],
operation: ['refresh'],
},
},
options: [
{
displayName: 'Force',
name: 'force',
type: 'boolean',
default: false,
description: 'Whether to force a full refresh of all metadata',
},
],
},
];
+214
View File
@@ -0,0 +1,214 @@
import type { INodeProperties } from 'n8n-workflow';
export const mediaOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['media'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get metadata for a specific media item',
action: 'Get media item',
},
{
name: 'Get Children',
value: 'getChildren',
description: 'Get children of a media item (e.g., episodes of a show)',
action: 'Get media children',
},
{
name: 'Get Related',
value: 'getRelated',
description: 'Get related media items',
action: 'Get related media',
},
{
name: 'Get Similar',
value: 'getSimilar',
description: 'Get similar media items',
action: 'Get similar media',
},
{
name: 'Update',
value: 'update',
description: 'Update metadata for a media item',
action: 'Update media item',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a media item',
action: 'Delete media item',
},
{
name: 'Refresh',
value: 'refresh',
description: 'Refresh metadata for a media item',
action: 'Refresh media item',
},
{
name: 'Analyze',
value: 'analyze',
description: 'Analyze a media item',
action: 'Analyze media item',
},
{
name: 'Mark Watched',
value: 'markWatched',
description: 'Mark a media item as watched (scrobble)',
action: 'Mark as watched',
},
{
name: 'Mark Unwatched',
value: 'markUnwatched',
description: 'Mark a media item as unwatched (unscrobble)',
action: 'Mark as unwatched',
},
],
default: 'get',
},
];
export const mediaFields: INodeProperties[] = [
// Rating Key (Media ID)
{
displayName: 'Rating Key',
name: 'ratingKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['media'],
operation: [
'get',
'getChildren',
'getRelated',
'getSimilar',
'update',
'delete',
'refresh',
'analyze',
'markWatched',
'markUnwatched',
],
},
},
default: '',
description: 'The rating key (ID) of the media item',
},
// Update Fields
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['media'],
operation: ['update'],
},
},
options: [
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'The title of the media item',
},
{
displayName: 'Sort Title',
name: 'titleSort',
type: 'string',
default: '',
description: 'The sort title of the media item',
},
{
displayName: 'Originally Available At',
name: 'originallyAvailableAt',
type: 'string',
default: '',
description: 'The original release date (YYYY-MM-DD)',
},
{
displayName: 'Studio',
name: 'studio',
type: 'string',
default: '',
description: 'The studio that produced the media',
},
{
displayName: 'Content Rating',
name: 'contentRating',
type: 'string',
default: '',
description: 'The content rating (e.g., PG-13, TV-MA)',
},
{
displayName: 'Summary',
name: 'summary',
type: 'string',
typeOptions: {
rows: 4,
},
default: '',
description: 'The summary/description of the media item',
},
{
displayName: 'Tagline',
name: 'tagline',
type: 'string',
default: '',
description: 'The tagline of the media item',
},
{
displayName: 'Rating',
name: 'rating',
type: 'number',
typeOptions: {
minValue: 0,
maxValue: 10,
numberPrecision: 1,
},
default: 0,
description: 'The rating of the media item (0-10)',
},
],
},
// Options for related/similar
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['media'],
operation: ['getRelated', 'getSimilar'],
},
},
options: [
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 10,
description: 'Max number of results to return',
},
],
},
];
@@ -0,0 +1,267 @@
import type { INodeProperties } from 'n8n-workflow';
export const playlistOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['playlist'],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all playlists',
action: 'Get all playlists',
},
{
name: 'Get',
value: 'get',
description: 'Get a specific playlist',
action: 'Get a playlist',
},
{
name: 'Get Items',
value: 'getItems',
description: 'Get items in a playlist',
action: 'Get playlist items',
},
{
name: 'Create',
value: 'create',
description: 'Create a new playlist',
action: 'Create a playlist',
},
{
name: 'Update',
value: 'update',
description: 'Update a playlist',
action: 'Update a playlist',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a playlist',
action: 'Delete a playlist',
},
{
name: 'Add Items',
value: 'addItems',
description: 'Add items to a playlist',
action: 'Add items to playlist',
},
{
name: 'Remove Items',
value: 'removeItems',
description: 'Remove items from a playlist',
action: 'Remove items from playlist',
},
],
default: 'getAll',
},
];
export const playlistFields: INodeProperties[] = [
// Playlist Rating Key
{
displayName: 'Playlist Rating Key',
name: 'playlistRatingKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['playlist'],
operation: ['get', 'getItems', 'update', 'delete', 'addItems', 'removeItems'],
},
},
default: '',
description: 'The rating key (ID) of the playlist',
},
// Create Playlist Fields
{
displayName: 'Title',
name: 'title',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['playlist'],
operation: ['create'],
},
},
default: '',
description: 'The title of the new playlist',
},
{
displayName: 'Playlist Type',
name: 'playlistType',
type: 'options',
required: true,
displayOptions: {
show: {
resource: ['playlist'],
operation: ['create'],
},
},
options: [
{
name: 'Video',
value: 'video',
},
{
name: 'Audio',
value: 'audio',
},
{
name: 'Photo',
value: 'photo',
},
],
default: 'video',
description: 'The type of playlist to create',
},
{
displayName: 'URI',
name: 'uri',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['playlist'],
operation: ['create'],
},
},
default: '',
placeholder: 'server://client-id/com.plexapp.plugins.library/library/metadata/12345',
description: 'The URI of the first item to add to the playlist',
},
// Update Fields
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['playlist'],
operation: ['update'],
},
},
options: [
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'The new title of the playlist',
},
{
displayName: 'Summary',
name: 'summary',
type: 'string',
typeOptions: {
rows: 4,
},
default: '',
description: 'The summary/description of the playlist',
},
],
},
// Add/Remove Items
{
displayName: 'Item URI',
name: 'itemUri',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['playlist'],
operation: ['addItems'],
},
},
default: '',
placeholder: 'server://client-id/com.plexapp.plugins.library/library/metadata/12345',
description: 'The URI of the item to add to the playlist',
},
{
displayName: 'Playlist Item ID',
name: 'playlistItemId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['playlist'],
operation: ['removeItems'],
},
},
default: '',
description: 'The playlist item ID to remove',
},
// Options for getAll
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['playlist'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Playlist Type',
name: 'playlistType',
type: 'options',
options: [
{
name: 'All',
value: '',
},
{
name: 'Audio',
value: 'audio',
},
{
name: 'Photo',
value: 'photo',
},
{
name: 'Video',
value: 'video',
},
],
default: '',
description: 'Filter playlists by type',
},
{
displayName: 'Smart',
name: 'smart',
type: 'options',
options: [
{
name: 'All',
value: '',
},
{
name: 'Smart Only',
value: '1',
},
{
name: 'Regular Only',
value: '0',
},
],
default: '',
description: 'Filter by smart/regular playlists',
},
],
},
];
@@ -0,0 +1,152 @@
import type { INodeProperties } from 'n8n-workflow';
export const searchOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['search'],
},
},
options: [
{
name: 'Search',
value: 'search',
description: 'Search across all libraries',
action: 'Search all libraries',
},
{
name: 'Search in Library',
value: 'searchInLibrary',
description: 'Search within a specific library',
action: 'Search in library',
},
],
default: 'search',
},
];
export const searchFields: INodeProperties[] = [
// Query
{
displayName: 'Query',
name: 'query',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['search'],
operation: ['search', 'searchInLibrary'],
},
},
default: '',
description: 'The search query',
},
// Library Key for searchInLibrary
{
displayName: 'Library Key',
name: 'libraryKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['search'],
operation: ['searchInLibrary'],
},
},
default: '',
description: 'The key/ID of the library section to search in',
},
// Return All
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['search'],
operation: ['search', 'searchInLibrary'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['search'],
operation: ['search', 'searchInLibrary'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 50,
description: 'Max number of results to return',
},
// Search Options
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['search'],
operation: ['searchInLibrary'],
},
},
options: [
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'All',
value: '',
},
{
name: 'Movie',
value: '1',
},
{
name: 'Show',
value: '2',
},
{
name: 'Season',
value: '3',
},
{
name: 'Episode',
value: '4',
},
{
name: 'Artist',
value: '8',
},
{
name: 'Album',
value: '9',
},
{
name: 'Track',
value: '10',
},
],
default: '',
description: 'Filter search results by media type',
},
],
},
];
@@ -0,0 +1,142 @@
import type { INodeProperties } from 'n8n-workflow';
export const serverOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['server'],
},
},
options: [
{
name: 'Get Identity',
value: 'getIdentity',
description: 'Get server identity information',
action: 'Get server identity',
},
{
name: 'Get Info',
value: 'getInfo',
description: 'Get server information',
action: 'Get server info',
},
{
name: 'Get Preferences',
value: 'getPreferences',
description: 'Get server preferences',
action: 'Get server preferences',
},
{
name: 'Get Active Sessions',
value: 'getActiveSessions',
description: 'Get all active playback sessions',
action: 'Get active sessions',
},
{
name: 'Get Transcode Sessions',
value: 'getTranscodeSessions',
description: 'Get all active transcode sessions',
action: 'Get transcode sessions',
},
{
name: 'Get Butler Tasks',
value: 'getButlerTasks',
description: 'Get scheduled butler tasks',
action: 'Get butler tasks',
},
{
name: 'Run Butler Task',
value: 'runButlerTask',
description: 'Run a specific butler task',
action: 'Run butler task',
},
{
name: 'Stop Butler Task',
value: 'stopButlerTask',
description: 'Stop a specific butler task',
action: 'Stop butler task',
},
],
default: 'getInfo',
},
];
export const serverFields: INodeProperties[] = [
// Butler Task Name
{
displayName: 'Task Name',
name: 'taskName',
type: 'options',
required: true,
displayOptions: {
show: {
resource: ['server'],
operation: ['runButlerTask', 'stopButlerTask'],
},
},
options: [
{
name: 'Backup Database',
value: 'BackupDatabase',
},
{
name: 'Build Gracenote Collections',
value: 'BuildGracenoteCollections',
},
{
name: 'Check For Updates',
value: 'CheckForUpdates',
},
{
name: 'Clean Old Bundles',
value: 'CleanOldBundles',
},
{
name: 'Clean Old Cache Files',
value: 'CleanOldCacheFiles',
},
{
name: 'Deep Media Analysis',
value: 'DeepMediaAnalysis',
},
{
name: 'Generate Auto Tags',
value: 'GenerateAutoTags',
},
{
name: 'Generate Chapter Thumbs',
value: 'GenerateChapterThumbs',
},
{
name: 'Generate Media Index Files',
value: 'GenerateMediaIndexFiles',
},
{
name: 'Optimize Database',
value: 'OptimizeDatabase',
},
{
name: 'Refresh Libraries',
value: 'RefreshLibraries',
},
{
name: 'Refresh Local Media',
value: 'RefreshLocalMedia',
},
{
name: 'Refresh Periodic Metadata',
value: 'RefreshPeriodicMetadata',
},
{
name: 'Upgrade Media Analysis',
value: 'UpgradeMediaAnalysis',
},
],
default: 'RefreshLibraries',
description: 'The butler task to run or stop',
},
];
@@ -0,0 +1,146 @@
import type { INodeProperties } from 'n8n-workflow';
export const sessionOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['session'],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all active playback sessions',
action: 'Get all sessions',
},
{
name: 'Get History',
value: 'getHistory',
description: 'Get playback history',
action: 'Get playback history',
},
{
name: 'Terminate',
value: 'terminate',
description: 'Terminate a playback session',
action: 'Terminate session',
},
],
default: 'getAll',
},
];
export const sessionFields: INodeProperties[] = [
// Session ID for terminate
{
displayName: 'Session ID',
name: 'sessionId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['session'],
operation: ['terminate'],
},
},
default: '',
description: 'The ID of the session to terminate',
},
// Terminate Options
{
displayName: 'Reason',
name: 'reason',
type: 'string',
displayOptions: {
show: {
resource: ['session'],
operation: ['terminate'],
},
},
default: '',
description: 'The reason for terminating the session (shown to user)',
},
// History Options
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['session'],
operation: ['getHistory'],
},
},
options: [
{
displayName: 'Sort',
name: 'sort',
type: 'options',
options: [
{
name: 'Newest First',
value: 'viewedAt:desc',
},
{
name: 'Oldest First',
value: 'viewedAt:asc',
},
],
default: 'viewedAt:desc',
description: 'Sort order for history',
},
{
displayName: 'Account ID',
name: 'accountID',
type: 'number',
default: 0,
description: 'Filter by account ID (0 for all accounts)',
},
{
displayName: 'Library Section ID',
name: 'librarySectionID',
type: 'string',
default: '',
description: 'Filter by library section',
},
],
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['session'],
operation: ['getHistory'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['session'],
operation: ['getHistory'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 50,
description: 'Max number of results to return',
},
];
+7
View File
@@ -0,0 +1,7 @@
export * from './ServerDescription';
export * from './LibraryDescription';
export * from './MediaDescription';
export * from './PlaylistDescription';
export * from './SearchDescription';
export * from './SessionDescription';
export * from './CollectionDescription';
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<linearGradient id="plexGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#E5A00D"/>
<stop offset="100%" style="stop-color:#CC7B19"/>
</linearGradient>
</defs>
<rect width="100" height="100" rx="15" fill="#282828"/>
<polygon points="30,20 75,50 30,80" fill="url(#plexGradient)"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B