Skip to content

Commit 8c215e1

Browse files
committed
auto tagging and collection creation works
1 parent 2c2cf04 commit 8c215e1

File tree

4 files changed

+119
-68
lines changed

4 files changed

+119
-68
lines changed

README.md

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
### ⚠️ UNDER DEVELOPMENT, INCOMPLETE ⚠️
2-
31
# Plex Requester Collections
42

53
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.

package.json

+30-30
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
{
2-
"name": "plex-requester-collections",
3-
"version": "0.0.1",
4-
"description": "Node app to automatically create Plex Collections for content requested by users in Overseerr.",
5-
"main": "dist/index.js",
6-
"scripts": {
7-
"start": "tsc && node dist/index.js",
8-
"lint": "eslint . --ext .ts",
9-
"test": "echo \"Error: no test specified\" && exit 1"
10-
},
11-
"keywords": [
12-
"plex",
13-
"overseerr",
14-
"homelab",
15-
"selfhost"
16-
],
17-
"author": "Jess Latimer",
18-
"license": "MIT",
19-
"devDependencies": {
20-
"@typescript-eslint/eslint-plugin": "^5.59.5",
21-
"@typescript-eslint/parser": "^5.59.5",
22-
"eslint": "^8.40.0",
23-
"typescript": "^5.0.4"
24-
},
25-
"dependencies": {
26-
"@types/lodash": "^4.14.194",
27-
"@types/node": "^20.1.3",
28-
"axios": "^1.4.0",
29-
"dotenv": "^16.0.3",
30-
"lodash": "^4.17.21"
31-
}
2+
"name": "plex-requester-collections",
3+
"version": "1.0.0",
4+
"description": "Node app to automatically create Plex Collections for content requested by users in Overseerr.",
5+
"main": "dist/index.js",
6+
"scripts": {
7+
"start": "tsc && node dist/index.js",
8+
"lint": "eslint . --ext .ts",
9+
"test": "echo \"Error: no test specified\" && exit 1"
10+
},
11+
"keywords": [
12+
"plex",
13+
"overseerr",
14+
"homelab",
15+
"selfhost"
16+
],
17+
"author": "Jess Latimer",
18+
"license": "MIT",
19+
"devDependencies": {
20+
"@typescript-eslint/eslint-plugin": "^5.59.5",
21+
"@typescript-eslint/parser": "^5.59.5",
22+
"eslint": "^8.40.0",
23+
"typescript": "^5.0.4"
24+
},
25+
"dependencies": {
26+
"@types/lodash": "^4.14.194",
27+
"@types/node": "^20.1.3",
28+
"axios": "^1.4.0",
29+
"dotenv": "^16.0.3",
30+
"lodash": "^4.17.21"
31+
}
3232
}

src/index.ts

+66-14
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,81 @@ import _ from "lodash";
2222
// ID of current library section.
2323
const sectionId = parseInt(plexSections[i]?.key);
2424

25-
// Get all the media items, existing collections, and existing labels for this library section.
25+
// Get all the media items and existing collections for this library section.
2626
const mediaItems = await PlexAPI.getAllItems(sectionId);
27-
const collections = await PlexAPI.getCollections(sectionId);
28-
const labels = await PlexAPI.getLabels(sectionId);
27+
let collections = await PlexAPI.getCollections(sectionId);
2928

3029
// Cycle through each media item in the library section, tag it with the requester.
3130
for (let j = 0; j < mediaItems.length; j++) {
3231
const mediaItem = mediaItems[j];
33-
const request = getRequestForPlexMediaId(
32+
33+
// Does a requester entry exist for this media item?
34+
const request = _.find(
3435
requests,
35-
parseInt(mediaItem?.ratingKey)
36+
(item) => item?.media?.ratingKey === mediaItem?.ratingKey
3637
);
38+
39+
// Bingo, this media item has a requester... Do the stuff.
3740
if (mediaItem && request) {
41+
// Init some values we're going to need.
42+
const mediaId = parseInt(mediaItem.ratingKey);
43+
const plexUsername = request?.requestedBy?.plexUsername;
44+
45+
// Tag the media item.
46+
const mediaLabelValue = "requester:" + plexUsername;
47+
const result = await PlexAPI.addLabelToItem(
48+
sectionId,
49+
PlexAPI.getPlexTypeCode(sectionType),
50+
mediaId,
51+
mediaLabelValue
52+
);
53+
54+
// This is what the smart collection should be called.
55+
const collectionTitle =
56+
sectionType == "movie"
57+
? "Movies Requested by " + plexUsername
58+
: "TV Shows Requested by " + plexUsername;
59+
60+
// Does the smart collection already exist?
61+
const collection = _.find(
62+
collections,
63+
(item) => item?.title === collectionTitle
64+
);
65+
// If collection exists with this title, assume it's set up correctly and we don't need to do anything else.
66+
// If collection does not exist with this title, create it and tag is with owner label.
67+
if (!collection) {
68+
// Get the numberic ID of the label we're using right now.
69+
const mediaLabelKey = await PlexAPI.getKeyForLabel(
70+
sectionId,
71+
mediaLabelValue
72+
);
73+
74+
// Create the new smart collection
75+
const createColResult =
76+
await PlexAPI.createSmartCollection({
77+
sectionId: sectionId,
78+
title: collectionTitle,
79+
titleSort: "zzz_" + collectionTitle, // TO DO Move prefix to env option.
80+
itemType: PlexAPI.getPlexTypeCode(sectionType),
81+
sort: "addedAt%3Adesc", //date added descending
82+
query: "label=" + mediaLabelKey
83+
});
84+
// Only continue if creating the collection seems to have worked.
85+
if (createColResult) {
86+
const labelColResult = await PlexAPI.addLabelToItem(
87+
sectionId,
88+
PlexAPI.getPlexTypeCode(createColResult.type),
89+
parseInt(createColResult.ratingKey),
90+
"owner:" + plexUsername
91+
);
92+
}
93+
94+
// Update list of collections we're working with now that we've added one.
95+
collections = await PlexAPI.getCollections(sectionId);
96+
}
97+
3898
console.log(
39-
`${mediaItem.title} requested by ${request?.requestedBy?.displayName}`
99+
`${mediaItem.title} requested by ${plexUsername}`
40100
);
41101
}
42102
}
@@ -45,11 +105,3 @@ import _ from "lodash";
45105
}
46106
}
47107
})();
48-
49-
const getRequestForPlexMediaId = function (requests, plexId) {
50-
const result = _.find(
51-
requests,
52-
(element) => parseInt(element?.media?.ratingKey) === plexId
53-
);
54-
return result;
55-
};

src/plex.ts

+23-22
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ type PlexCollectionOptions = {
163163
sectionId: number;
164164
title: string;
165165
titleSort?: string;
166-
mediaIds?: Array<number>;
167166
itemType: number;
168167
sort?: string;
169168
query: string;
@@ -258,7 +257,17 @@ const PlexAPI = {
258257
// TO DO This one was kind of a finicky query. Just copying their exact string for now, will come back and simplify.
259258
// Looks like you can't set titleSort in this command
260259
const data = await this.callApi({
261-
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}&sectionId=${options.sectionId}`,
260+
url: `/library/collections?type=${
261+
options.itemType
262+
}&title=${encodeURIComponent(
263+
options.title
264+
)}&smart=1&uri=server%3A%2F%2F${machineId}%2Fcom.plexapp.plugins.library%2Flibrary%2Fsections%2F${
265+
options.sectionId
266+
}%2Fall%3Ftype%3D${options.itemType}%26sort%3D${
267+
options.sort
268+
}%26${encodeURIComponent(options.query)}&sectionId=${
269+
options.sectionId
270+
}`,
262271
method: "post"
263272
});
264273

@@ -269,6 +278,9 @@ const PlexAPI = {
269278
? data.MediaContainer.Metadata[0].ratingKey
270279
: undefined;
271280

281+
// If key can't be found, get out of here cause next call might make wrong changes.
282+
if (!collectionKey) return undefined;
283+
272284
// Update the collection's titleSort field.
273285
const result = await this.updateItemDetails(
274286
options.sectionId,
@@ -282,8 +294,8 @@ const PlexAPI = {
282294
this.debug(result);
283295
}
284296

285-
this.debug(data?.MediaContainer?.Metadata);
286-
return data?.MediaContainer?.Metadata;
297+
this.debug(data?.MediaContainer?.Metadata[0]);
298+
return data?.MediaContainer?.Metadata[0];
287299
},
288300
// The API command described here can return the information for each account setup on the Plex server.
289301
getAccounts: async function () {
@@ -336,28 +348,17 @@ const PlexAPI = {
336348
this.debug(labelKey);
337349
return labelKey;
338350
},
339-
// Alias helper funtion to simplify adding labels to movies.
340-
addLabelToMovie: async function (
341-
sectionId: number,
342-
movieId: number,
343-
label: string
344-
) {
345-
return this.updateItemDetails(sectionId, movieId, {
346-
"label[0].tag.tag": encodeURIComponent(label),
347-
"label.locked": 1,
348-
type: PlexTypes.MOVIE
349-
});
350-
},
351-
// Alias helper funtion to simplify adding labels to collecitons.
352-
addLabelToCollection: async function (
351+
// Alias helper funtion to simplify adding labels to plex items (Movies, Shows, Collections, etc.).
352+
addLabelToItem: async function (
353353
sectionId: number,
354-
collectionId: number,
354+
itemType: number,
355+
itemId: number,
355356
label: string
356357
) {
357-
return this.updateItemDetails(sectionId, collectionId, {
358-
"label[0].tag.tag": encodeURIComponent(label),
358+
return this.updateItemDetails(sectionId, itemId, {
359+
"label[0].tag.tag": label,
359360
"label.locked": 1,
360-
type: PlexTypes.COLLECTION
361+
type: itemType
361362
});
362363
},
363364
// Returns an array of all the top-level media items (Movies or TV Shows) in a given Section AKA Library.

0 commit comments

Comments
 (0)