@ctrl/plex 3.12.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/src/alert.js +1 -1
- package/dist/src/audio.d.ts +3 -1
- package/dist/src/audio.js +8 -6
- package/dist/src/base/partialPlexObject.d.ts +26 -22
- package/dist/src/base/partialPlexObject.js +36 -39
- package/dist/src/base/plexObject.js +5 -4
- package/dist/src/baseFunctionality.js +2 -2
- package/dist/src/client.d.ts +4 -2
- package/dist/src/client.js +2 -2
- package/dist/src/library.d.ts +23 -6
- package/dist/src/library.js +25 -25
- package/dist/src/media.js +7 -7
- package/dist/src/myplex.d.ts +26 -19
- package/dist/src/myplex.js +28 -26
- package/dist/src/playlist.js +6 -6
- package/dist/src/playqueue.js +8 -8
- package/dist/src/server.d.ts +32 -23
- package/dist/src/server.js +55 -40
- package/dist/src/server.types.d.ts +12 -0
- package/dist/src/settings.js +1 -1
- package/dist/src/util.d.ts +5 -4
- package/dist/src/util.js +2 -4
- package/dist/src/video.d.ts +4 -1
- package/dist/src/video.js +11 -9
- package/package.json +1 -1
package/dist/src/server.js
CHANGED
|
@@ -42,7 +42,7 @@ export class PlexServer {
|
|
|
42
42
|
return fetchItems(this, key, undefined, Agent, this);
|
|
43
43
|
}
|
|
44
44
|
async connect() {
|
|
45
|
-
const data = await this.query(this.key);
|
|
45
|
+
const data = await this.query({ path: this.key });
|
|
46
46
|
this._loadData(data.MediaContainer);
|
|
47
47
|
// Attempt to prevent token from being logged accidentally
|
|
48
48
|
if (this.token) {
|
|
@@ -60,12 +60,14 @@ export class PlexServer {
|
|
|
60
60
|
return this._library;
|
|
61
61
|
}
|
|
62
62
|
try {
|
|
63
|
-
const data = await this.query(Library.key);
|
|
63
|
+
const data = await this.query({ path: Library.key });
|
|
64
64
|
this._library = new Library(this, data.MediaContainer);
|
|
65
65
|
}
|
|
66
66
|
catch {
|
|
67
67
|
// TODO: validate error type, also TODO figure out how this is used
|
|
68
|
-
const data = await this.query(
|
|
68
|
+
const data = await this.query({
|
|
69
|
+
path: '/library/sections/',
|
|
70
|
+
});
|
|
69
71
|
this._library = new Library(this, data.MediaContainer);
|
|
70
72
|
}
|
|
71
73
|
return this._library;
|
|
@@ -82,10 +84,9 @@ export class PlexServer {
|
|
|
82
84
|
* 'Arnold' you’ll get a result for the actor, but also the most recently added
|
|
83
85
|
* movies he’s in.
|
|
84
86
|
* @param query Query to use when searching your library.
|
|
85
|
-
* @param
|
|
86
|
-
* @param limit Optionally limit to the specified number of results per Hub.
|
|
87
|
+
* @param options Search options.
|
|
87
88
|
*/
|
|
88
|
-
async search(query, mediatype, limit) {
|
|
89
|
+
async search(query, { mediatype, limit, } = {}) {
|
|
89
90
|
const params = { query };
|
|
90
91
|
if (mediatype) {
|
|
91
92
|
params.section = SEARCHTYPES[mediatype].toString();
|
|
@@ -93,8 +94,9 @@ export class PlexServer {
|
|
|
93
94
|
if (limit) {
|
|
94
95
|
params.limit = limit.toString();
|
|
95
96
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
97
|
+
const url = new URL('/hubs/search', this.baseurl);
|
|
98
|
+
url.search = new URLSearchParams(params).toString();
|
|
99
|
+
const hubs = await fetchItems(this, url.pathname + url.search, undefined, Hub, this);
|
|
98
100
|
return hubs;
|
|
99
101
|
}
|
|
100
102
|
/**
|
|
@@ -102,12 +104,10 @@ export class PlexServer {
|
|
|
102
104
|
* by encoding the response to utf-8 and parsing the returned XML into and
|
|
103
105
|
* ElementTree object. Returns None if no data exists in the response.
|
|
104
106
|
* TODO: use headers
|
|
105
|
-
* @param
|
|
106
|
-
* @param method
|
|
107
|
-
* @param headers
|
|
107
|
+
* @param options
|
|
108
108
|
*/
|
|
109
|
-
async query(path, method = 'get',
|
|
110
|
-
const requestHeaders = this._headers();
|
|
109
|
+
async query({ path, method = 'get', headers, body, username, password, }) {
|
|
110
|
+
const requestHeaders = { ...this._headers(), ...headers };
|
|
111
111
|
if (username && password) {
|
|
112
112
|
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
113
113
|
requestHeaders.Authorization = `Basic ${credentials}`;
|
|
@@ -120,7 +120,7 @@ export class PlexServer {
|
|
|
120
120
|
method,
|
|
121
121
|
headers: requestHeaders,
|
|
122
122
|
timeout: this.timeout ?? TIMEOUT,
|
|
123
|
-
body
|
|
123
|
+
body,
|
|
124
124
|
retry: 0,
|
|
125
125
|
responseType: 'json',
|
|
126
126
|
});
|
|
@@ -129,15 +129,11 @@ export class PlexServer {
|
|
|
129
129
|
/**
|
|
130
130
|
* Returns a list of media items from watched history. If there are many results, they will
|
|
131
131
|
* be fetched from the server in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only
|
|
132
|
-
* looking for the first <num> results, it would be wise to set the
|
|
132
|
+
* looking for the first <num> results, it would be wise to set the maxResults option to that
|
|
133
133
|
* amount so this functions doesn't iterate over all results on the server.
|
|
134
|
-
* @param
|
|
135
|
-
* @param mindate Min datetime to return results from. This really helps speed up the result listing. For example: datetime.now() - timedelta(days=7)
|
|
136
|
-
* @param ratingKey request history for a specific ratingKey item.
|
|
137
|
-
* @param accountId request history for a specific account ID.
|
|
138
|
-
* @param librarySectionId request history for a specific library section ID.
|
|
134
|
+
* @param options Filter and paging options.
|
|
139
135
|
*/
|
|
140
|
-
async history(
|
|
136
|
+
async history({ maxResults = 9_999_999, minDate, ratingKey, accountId, librarySectionId, } = {}) {
|
|
141
137
|
const args = { sort: 'viewedAt:desc' };
|
|
142
138
|
if (ratingKey !== undefined) {
|
|
143
139
|
args.metadataItemID = ratingKey.toString();
|
|
@@ -148,24 +144,29 @@ export class PlexServer {
|
|
|
148
144
|
if (librarySectionId !== undefined) {
|
|
149
145
|
args.librarySectionID = librarySectionId.toString();
|
|
150
146
|
}
|
|
151
|
-
if (
|
|
152
|
-
args['viewedAt>'] = Math.floor(
|
|
147
|
+
if (minDate !== undefined) {
|
|
148
|
+
args['viewedAt>'] = Math.floor(minDate.getTime() / 1000).toString();
|
|
153
149
|
}
|
|
154
150
|
args['X-Plex-Container-Start'] = '0';
|
|
155
|
-
args['X-Plex-Container-Size'] = Math.min(X_PLEX_CONTAINER_SIZE,
|
|
151
|
+
args['X-Plex-Container-Size'] = Math.min(X_PLEX_CONTAINER_SIZE, maxResults).toString();
|
|
156
152
|
let results = [];
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
const url = new URL('/status/sessions/history/all', this.baseurl);
|
|
154
|
+
url.search = new URLSearchParams(args).toString();
|
|
155
|
+
let raw = await this.query({
|
|
156
|
+
path: url.pathname + url.search,
|
|
157
|
+
});
|
|
159
158
|
const totalResults = raw.MediaContainer.totalSize;
|
|
160
159
|
// Filter out null/undefined items from the metadata
|
|
161
160
|
const validMetadata = raw.MediaContainer.Metadata?.filter(Boolean) ?? [];
|
|
162
161
|
results.push(...validMetadata);
|
|
163
162
|
while (results.length <= totalResults &&
|
|
164
163
|
X_PLEX_CONTAINER_SIZE === raw.MediaContainer.size &&
|
|
165
|
-
|
|
164
|
+
maxResults > results.length) {
|
|
166
165
|
args['X-Plex-Container-Start'] = (Number(args['X-Plex-Container-Start']) + Number(args['X-Plex-Container-Size'])).toString();
|
|
167
|
-
|
|
168
|
-
raw = await this.query(
|
|
166
|
+
url.search = new URLSearchParams(args).toString();
|
|
167
|
+
raw = await this.query({
|
|
168
|
+
path: url.pathname + url.search,
|
|
169
|
+
});
|
|
169
170
|
// Filter out null/undefined items from the metadata
|
|
170
171
|
const validMetadata = raw.MediaContainer.Metadata?.filter(item => item != null) ?? [];
|
|
171
172
|
results.push(...validMetadata);
|
|
@@ -174,7 +175,9 @@ export class PlexServer {
|
|
|
174
175
|
}
|
|
175
176
|
async settings() {
|
|
176
177
|
if (!this._settings) {
|
|
177
|
-
const data = await this.query(
|
|
178
|
+
const data = await this.query({
|
|
179
|
+
path: Settings.key,
|
|
180
|
+
});
|
|
178
181
|
this._settings = new Settings(this, data.MediaContainer.Setting);
|
|
179
182
|
}
|
|
180
183
|
return this._settings;
|
|
@@ -206,14 +209,21 @@ export class PlexServer {
|
|
|
206
209
|
*/
|
|
207
210
|
myPlexAccount() {
|
|
208
211
|
if (!this._myPlexAccount) {
|
|
209
|
-
this._myPlexAccount = new MyPlexAccount(
|
|
212
|
+
this._myPlexAccount = new MyPlexAccount({
|
|
213
|
+
baseUrl: this.baseurl,
|
|
214
|
+
token: this.token,
|
|
215
|
+
timeout: this.timeout,
|
|
216
|
+
server: this,
|
|
217
|
+
});
|
|
210
218
|
}
|
|
211
219
|
return this._myPlexAccount;
|
|
212
220
|
}
|
|
213
221
|
// Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server.
|
|
214
222
|
async clients() {
|
|
215
223
|
const items = [];
|
|
216
|
-
const response = await this.query(
|
|
224
|
+
const response = await this.query({
|
|
225
|
+
path: '/clients',
|
|
226
|
+
});
|
|
217
227
|
if (response.MediaContainer?.Server === undefined) {
|
|
218
228
|
return [];
|
|
219
229
|
}
|
|
@@ -243,7 +253,7 @@ export class PlexServer {
|
|
|
243
253
|
* Build a URL string with proper token argument. Token will be appended to the URL
|
|
244
254
|
* if either includeToken is True or TODO: CONFIG.log.show_secrets is 'true'.
|
|
245
255
|
*/
|
|
246
|
-
url(key, includeToken = false, params) {
|
|
256
|
+
url(key, { includeToken = false, params, } = {}) {
|
|
247
257
|
if (!this.baseurl) {
|
|
248
258
|
throw new Error('PlexClient object missing baseurl.');
|
|
249
259
|
}
|
|
@@ -254,20 +264,25 @@ export class PlexServer {
|
|
|
254
264
|
url.search = searchParams.toString();
|
|
255
265
|
return url;
|
|
256
266
|
}
|
|
267
|
+
if (params) {
|
|
268
|
+
url.search = params.toString();
|
|
269
|
+
}
|
|
257
270
|
return url;
|
|
258
271
|
}
|
|
259
272
|
/**
|
|
260
273
|
* Build the Plex Web URL for the object.
|
|
261
|
-
* @param
|
|
262
|
-
* Default is https://app.plex.tv/desktop.
|
|
263
|
-
* @param endpoint The Plex Web URL endpoint.
|
|
264
|
-
* None for server, 'playlist' for playlists, 'details' for all other media types.
|
|
274
|
+
* @param options Options for the URL.
|
|
265
275
|
*/
|
|
266
|
-
_buildWebURL(base = 'https://app.plex.tv/desktop/', endpoint, params) {
|
|
276
|
+
_buildWebURL({ base = 'https://app.plex.tv/desktop/', endpoint, params, } = {}) {
|
|
277
|
+
const url = new URL(base);
|
|
278
|
+
const queryString = params?.toString() ? `?${params.toString()}` : '';
|
|
267
279
|
if (endpoint) {
|
|
268
|
-
|
|
280
|
+
url.hash = `!/server/${this.machineIdentifier}/${endpoint}${queryString}`;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
url.hash = `!/media/${this.machineIdentifier}/com.plexapp.plugins.library${queryString}`;
|
|
269
284
|
}
|
|
270
|
-
return
|
|
285
|
+
return url.toString();
|
|
271
286
|
}
|
|
272
287
|
_uriRoot() {
|
|
273
288
|
return `server://${this.machineIdentifier}/com.plexapp.plugins.library`;
|
|
@@ -119,3 +119,15 @@ export interface ServerConnectionInfo {
|
|
|
119
119
|
protocolVersion: string;
|
|
120
120
|
protocolCapabilities: string;
|
|
121
121
|
}
|
|
122
|
+
export interface HistoryOptions {
|
|
123
|
+
/** Only return the specified number of results. */
|
|
124
|
+
maxResults?: number;
|
|
125
|
+
/** Min datetime to return results from. */
|
|
126
|
+
minDate?: Date;
|
|
127
|
+
/** Request history for a specific ratingKey item. */
|
|
128
|
+
ratingKey?: number | string;
|
|
129
|
+
/** Request history for a specific account ID. */
|
|
130
|
+
accountId?: number | string;
|
|
131
|
+
/** Request history for a specific library section ID. */
|
|
132
|
+
librarySectionId?: number | string;
|
|
133
|
+
}
|
package/dist/src/settings.js
CHANGED
package/dist/src/util.d.ts
CHANGED
|
@@ -12,9 +12,10 @@ export declare function rsplit(str: string, sep: string, maxsplit: number): stri
|
|
|
12
12
|
* Return the full agent identifier from a short identifier, name, or confirm full identifier.
|
|
13
13
|
*/
|
|
14
14
|
export declare function getAgentIdentifier(section: Section, agent: string): Promise<string>;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
/** Simple tag helper for editing a object. */
|
|
16
|
+
export declare function tagHelper(tag: string, items: string[], { locked, remove }?: {
|
|
17
|
+
locked?: boolean;
|
|
18
|
+
remove?: boolean;
|
|
19
|
+
}): Record<string, string | number>;
|
|
19
20
|
export declare function ltrim(x: string, characters: string[]): string;
|
|
20
21
|
export declare function lowerFirst(str: string): string;
|
package/dist/src/util.js
CHANGED
|
@@ -19,10 +19,8 @@ export async function getAgentIdentifier(section, agent) {
|
|
|
19
19
|
}
|
|
20
20
|
throw new Error(`Couldnt find "${agent}" in agents list (${agents.join(', ')})`);
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
*/
|
|
25
|
-
export function tagHelper(tag, items, locked = true, remove = false) {
|
|
22
|
+
/** Simple tag helper for editing a object. */
|
|
23
|
+
export function tagHelper(tag, items, { locked = true, remove = false } = {}) {
|
|
26
24
|
const data = {};
|
|
27
25
|
if (remove) {
|
|
28
26
|
const tagname = `${tag}[].tag.tag-`;
|
package/dist/src/video.d.ts
CHANGED
|
@@ -62,7 +62,10 @@ declare abstract class Video extends Playable {
|
|
|
62
62
|
/**
|
|
63
63
|
* I haven't tested this yet. It may not work.
|
|
64
64
|
*/
|
|
65
|
-
uploadPoster(url
|
|
65
|
+
uploadPoster({ url, file, }?: {
|
|
66
|
+
url?: string;
|
|
67
|
+
file?: Uint8Array;
|
|
68
|
+
}): Promise<void>;
|
|
66
69
|
protected _loadData(data: MovieData | ShowData | EpisodeMetadata): void;
|
|
67
70
|
}
|
|
68
71
|
/**
|
package/dist/src/video.js
CHANGED
|
@@ -19,18 +19,18 @@ class Video extends Playable {
|
|
|
19
19
|
*/
|
|
20
20
|
get thumbUrl() {
|
|
21
21
|
const thumb = this.thumb ?? this.parentThumb ?? this.granparentThumb;
|
|
22
|
-
return this.server.url(thumb, true);
|
|
22
|
+
return this.server.url(thumb, { includeToken: true });
|
|
23
23
|
}
|
|
24
24
|
get artUrl() {
|
|
25
25
|
const art = this.art ?? this.grandparentArt;
|
|
26
|
-
return this.server.url(art, true);
|
|
26
|
+
return this.server.url(art, { includeToken: true });
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Mark video as watched.
|
|
30
30
|
*/
|
|
31
31
|
async markWatched() {
|
|
32
32
|
const key = `/:/scrobble?key=${this.ratingKey}&identifier=com.plexapp.plugins.library`;
|
|
33
|
-
await this.server.query(key);
|
|
33
|
+
await this.server.query({ path: key });
|
|
34
34
|
await this.reload();
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
@@ -38,16 +38,16 @@ class Video extends Playable {
|
|
|
38
38
|
*/
|
|
39
39
|
async markUnwatched() {
|
|
40
40
|
const key = `/:/unscrobble?key=${this.ratingKey}&identifier=com.plexapp.plugins.library`;
|
|
41
|
-
await this.server.query(key);
|
|
41
|
+
await this.server.query({ path: key });
|
|
42
42
|
await this.reload();
|
|
43
43
|
}
|
|
44
44
|
async rate(rate) {
|
|
45
45
|
const key = `/:/rate?key=${this.ratingKey}&identifier=com.plexapp.plugins.library&rating=${rate}`;
|
|
46
|
-
await this.server.query(key);
|
|
46
|
+
await this.server.query({ path: key });
|
|
47
47
|
await this.reload();
|
|
48
48
|
}
|
|
49
49
|
async extras() {
|
|
50
|
-
const data = await this.server.query(this._detailsKey);
|
|
50
|
+
const data = await this.server.query({ path: this._detailsKey });
|
|
51
51
|
return findItems(data.MediaContainer.Metadata[0].Extras?.Metadata, undefined, Extra, this.server, this);
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
@@ -67,14 +67,16 @@ class Video extends Playable {
|
|
|
67
67
|
/**
|
|
68
68
|
* I haven't tested this yet. It may not work.
|
|
69
69
|
*/
|
|
70
|
-
async uploadPoster(url, file) {
|
|
70
|
+
async uploadPoster({ url, file, } = {}) {
|
|
71
71
|
if (url) {
|
|
72
72
|
const key = `/library/metadata/${this.ratingKey}/posters?url=${encodeURIComponent(url)}`;
|
|
73
|
-
await this.server.query(key, 'post');
|
|
73
|
+
await this.server.query({ path: key, method: 'post' });
|
|
74
74
|
}
|
|
75
75
|
else if (file) {
|
|
76
76
|
const key = `/library/metadata/${this.ratingKey}/posters`;
|
|
77
|
-
await this.server.query(
|
|
77
|
+
await this.server.query({
|
|
78
|
+
path: key,
|
|
79
|
+
method: 'post',
|
|
78
80
|
body: file,
|
|
79
81
|
});
|
|
80
82
|
}
|