From 8c215e1c351292866102ded86672ecc525b79b78 Mon Sep 17 00:00:00 2001 From: Jess Latimer Date: Tue, 16 May 2023 12:51:47 -0700 Subject: [PATCH] auto tagging and collection creation works --- README.md | 2 -- package.json | 60 +++++++++++++++++++-------------------- src/index.ts | 80 +++++++++++++++++++++++++++++++++++++++++++--------- src/plex.ts | 45 ++++++++++++++--------------- 4 files changed, 119 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index f6ba2d2..4a76680 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -### ⚠️ UNDER DEVELOPMENT, INCOMPLETE ⚠️ - # Plex Requester Collections This app automatically creates Collections in Plex Media Server for the content that each user requests in the media request management system Overseerr. It will add a label to all TV Shows and Movies in Plex that a user has requested in Overseerr, using the label format `requester:plex_username`. It will also create Smart Collections for each user containing their requested items. The collections will have labels in the format `owner:plex_username`, which you can use to create sharing restrictions if desired, for example for personalized home screens. diff --git a/package.json b/package.json index 17fadec..e218032 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,32 @@ { - "name": "plex-requester-collections", - "version": "0.0.1", - "description": "Node app to automatically create Plex Collections for content requested by users in Overseerr.", - "main": "dist/index.js", - "scripts": { - "start": "tsc && node dist/index.js", - "lint": "eslint . --ext .ts", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [ - "plex", - "overseerr", - "homelab", - "selfhost" - ], - "author": "Jess Latimer", - "license": "MIT", - "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.59.5", - "@typescript-eslint/parser": "^5.59.5", - "eslint": "^8.40.0", - "typescript": "^5.0.4" - }, - "dependencies": { - "@types/lodash": "^4.14.194", - "@types/node": "^20.1.3", - "axios": "^1.4.0", - "dotenv": "^16.0.3", - "lodash": "^4.17.21" - } + "name": "plex-requester-collections", + "version": "1.0.0", + "description": "Node app to automatically create Plex Collections for content requested by users in Overseerr.", + "main": "dist/index.js", + "scripts": { + "start": "tsc && node dist/index.js", + "lint": "eslint . --ext .ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "plex", + "overseerr", + "homelab", + "selfhost" + ], + "author": "Jess Latimer", + "license": "MIT", + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.59.5", + "@typescript-eslint/parser": "^5.59.5", + "eslint": "^8.40.0", + "typescript": "^5.0.4" + }, + "dependencies": { + "@types/lodash": "^4.14.194", + "@types/node": "^20.1.3", + "axios": "^1.4.0", + "dotenv": "^16.0.3", + "lodash": "^4.17.21" + } } diff --git a/src/index.ts b/src/index.ts index 17fbbe0..3a01595 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,21 +22,81 @@ import _ from "lodash"; // ID of current library section. const sectionId = parseInt(plexSections[i]?.key); - // Get all the media items, existing collections, and existing labels for this library section. + // Get all the media items and existing collections for this library section. const mediaItems = await PlexAPI.getAllItems(sectionId); - const collections = await PlexAPI.getCollections(sectionId); - const labels = await PlexAPI.getLabels(sectionId); + let collections = await PlexAPI.getCollections(sectionId); // Cycle through each media item in the library section, tag it with the requester. for (let j = 0; j < mediaItems.length; j++) { const mediaItem = mediaItems[j]; - const request = getRequestForPlexMediaId( + + // Does a requester entry exist for this media item? + const request = _.find( requests, - parseInt(mediaItem?.ratingKey) + (item) => item?.media?.ratingKey === mediaItem?.ratingKey ); + + // Bingo, this media item has a requester... Do the stuff. if (mediaItem && request) { + // Init some values we're going to need. + const mediaId = parseInt(mediaItem.ratingKey); + const plexUsername = request?.requestedBy?.plexUsername; + + // Tag the media item. + const mediaLabelValue = "requester:" + plexUsername; + const result = await PlexAPI.addLabelToItem( + sectionId, + PlexAPI.getPlexTypeCode(sectionType), + mediaId, + mediaLabelValue + ); + + // This is what the smart collection should be called. + const collectionTitle = + sectionType == "movie" + ? "Movies Requested by " + plexUsername + : "TV Shows Requested by " + plexUsername; + + // Does the smart collection already exist? + const collection = _.find( + collections, + (item) => item?.title === collectionTitle + ); + // If collection exists with this title, assume it's set up correctly and we don't need to do anything else. + // If collection does not exist with this title, create it and tag is with owner label. + if (!collection) { + // Get the numberic ID of the label we're using right now. + const mediaLabelKey = await PlexAPI.getKeyForLabel( + sectionId, + mediaLabelValue + ); + + // Create the new smart collection + const createColResult = + await PlexAPI.createSmartCollection({ + sectionId: sectionId, + title: collectionTitle, + titleSort: "zzz_" + collectionTitle, // TO DO Move prefix to env option. + itemType: PlexAPI.getPlexTypeCode(sectionType), + sort: "addedAt%3Adesc", //date added descending + query: "label=" + mediaLabelKey + }); + // Only continue if creating the collection seems to have worked. + if (createColResult) { + const labelColResult = await PlexAPI.addLabelToItem( + sectionId, + PlexAPI.getPlexTypeCode(createColResult.type), + parseInt(createColResult.ratingKey), + "owner:" + plexUsername + ); + } + + // Update list of collections we're working with now that we've added one. + collections = await PlexAPI.getCollections(sectionId); + } + console.log( - `${mediaItem.title} requested by ${request?.requestedBy?.displayName}` + `${mediaItem.title} requested by ${plexUsername}` ); } } @@ -45,11 +105,3 @@ import _ from "lodash"; } } })(); - -const getRequestForPlexMediaId = function (requests, plexId) { - const result = _.find( - requests, - (element) => parseInt(element?.media?.ratingKey) === plexId - ); - return result; -}; diff --git a/src/plex.ts b/src/plex.ts index 115f5d4..f697a0f 100644 --- a/src/plex.ts +++ b/src/plex.ts @@ -163,7 +163,6 @@ type PlexCollectionOptions = { sectionId: number; title: string; titleSort?: string; - mediaIds?: Array; itemType: number; sort?: string; query: string; @@ -258,7 +257,17 @@ const PlexAPI = { // TO DO This one was kind of a finicky query. Just copying their exact string for now, will come back and simplify. // Looks like you can't set titleSort in this command const data = await this.callApi({ - url: `/library/collections?type=${options.itemType}&title=${options.title}&smart=1&uri=server%3A%2F%2F${machineId}%2Fcom.plexapp.plugins.library%2Flibrary%2Fsections%2F${options.sectionId}%2Fall%3Ftype%3D${options.itemType}%26sort%3D${options.sort}%26${options.query}§ionId=${options.sectionId}`, + url: `/library/collections?type=${ + options.itemType + }&title=${encodeURIComponent( + options.title + )}&smart=1&uri=server%3A%2F%2F${machineId}%2Fcom.plexapp.plugins.library%2Flibrary%2Fsections%2F${ + options.sectionId + }%2Fall%3Ftype%3D${options.itemType}%26sort%3D${ + options.sort + }%26${encodeURIComponent(options.query)}§ionId=${ + options.sectionId + }`, method: "post" }); @@ -269,6 +278,9 @@ const PlexAPI = { ? data.MediaContainer.Metadata[0].ratingKey : undefined; + // If key can't be found, get out of here cause next call might make wrong changes. + if (!collectionKey) return undefined; + // Update the collection's titleSort field. const result = await this.updateItemDetails( options.sectionId, @@ -282,8 +294,8 @@ const PlexAPI = { this.debug(result); } - this.debug(data?.MediaContainer?.Metadata); - return data?.MediaContainer?.Metadata; + this.debug(data?.MediaContainer?.Metadata[0]); + return data?.MediaContainer?.Metadata[0]; }, // The API command described here can return the information for each account setup on the Plex server. getAccounts: async function () { @@ -336,28 +348,17 @@ const PlexAPI = { this.debug(labelKey); return labelKey; }, - // Alias helper funtion to simplify adding labels to movies. - addLabelToMovie: async function ( - sectionId: number, - movieId: number, - label: string - ) { - return this.updateItemDetails(sectionId, movieId, { - "label[0].tag.tag": encodeURIComponent(label), - "label.locked": 1, - type: PlexTypes.MOVIE - }); - }, - // Alias helper funtion to simplify adding labels to collecitons. - addLabelToCollection: async function ( + // Alias helper funtion to simplify adding labels to plex items (Movies, Shows, Collections, etc.). + addLabelToItem: async function ( sectionId: number, - collectionId: number, + itemType: number, + itemId: number, label: string ) { - return this.updateItemDetails(sectionId, collectionId, { - "label[0].tag.tag": encodeURIComponent(label), + return this.updateItemDetails(sectionId, itemId, { + "label[0].tag.tag": label, "label.locked": 1, - type: PlexTypes.COLLECTION + type: itemType }); }, // Returns an array of all the top-level media items (Movies or TV Shows) in a given Section AKA Library.