@ctrl/plex 3.12.1 → 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 +2 -2
- 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 +42 -34
- 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();
|
|
@@ -103,12 +104,10 @@ export class PlexServer {
|
|
|
103
104
|
* by encoding the response to utf-8 and parsing the returned XML into and
|
|
104
105
|
* ElementTree object. Returns None if no data exists in the response.
|
|
105
106
|
* TODO: use headers
|
|
106
|
-
* @param
|
|
107
|
-
* @param method
|
|
108
|
-
* @param headers
|
|
107
|
+
* @param options
|
|
109
108
|
*/
|
|
110
|
-
async query(path, method = 'get',
|
|
111
|
-
const requestHeaders = this._headers();
|
|
109
|
+
async query({ path, method = 'get', headers, body, username, password, }) {
|
|
110
|
+
const requestHeaders = { ...this._headers(), ...headers };
|
|
112
111
|
if (username && password) {
|
|
113
112
|
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
114
113
|
requestHeaders.Authorization = `Basic ${credentials}`;
|
|
@@ -121,7 +120,7 @@ export class PlexServer {
|
|
|
121
120
|
method,
|
|
122
121
|
headers: requestHeaders,
|
|
123
122
|
timeout: this.timeout ?? TIMEOUT,
|
|
124
|
-
body
|
|
123
|
+
body,
|
|
125
124
|
retry: 0,
|
|
126
125
|
responseType: 'json',
|
|
127
126
|
});
|
|
@@ -130,15 +129,11 @@ export class PlexServer {
|
|
|
130
129
|
/**
|
|
131
130
|
* Returns a list of media items from watched history. If there are many results, they will
|
|
132
131
|
* be fetched from the server in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only
|
|
133
|
-
* 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
|
|
134
133
|
* amount so this functions doesn't iterate over all results on the server.
|
|
135
|
-
* @param
|
|
136
|
-
* @param mindate Min datetime to return results from. This really helps speed up the result listing. For example: datetime.now() - timedelta(days=7)
|
|
137
|
-
* @param ratingKey request history for a specific ratingKey item.
|
|
138
|
-
* @param accountId request history for a specific account ID.
|
|
139
|
-
* @param librarySectionId request history for a specific library section ID.
|
|
134
|
+
* @param options Filter and paging options.
|
|
140
135
|
*/
|
|
141
|
-
async history(
|
|
136
|
+
async history({ maxResults = 9_999_999, minDate, ratingKey, accountId, librarySectionId, } = {}) {
|
|
142
137
|
const args = { sort: 'viewedAt:desc' };
|
|
143
138
|
if (ratingKey !== undefined) {
|
|
144
139
|
args.metadataItemID = ratingKey.toString();
|
|
@@ -149,25 +144,29 @@ export class PlexServer {
|
|
|
149
144
|
if (librarySectionId !== undefined) {
|
|
150
145
|
args.librarySectionID = librarySectionId.toString();
|
|
151
146
|
}
|
|
152
|
-
if (
|
|
153
|
-
args['viewedAt>'] = Math.floor(
|
|
147
|
+
if (minDate !== undefined) {
|
|
148
|
+
args['viewedAt>'] = Math.floor(minDate.getTime() / 1000).toString();
|
|
154
149
|
}
|
|
155
150
|
args['X-Plex-Container-Start'] = '0';
|
|
156
|
-
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();
|
|
157
152
|
let results = [];
|
|
158
153
|
const url = new URL('/status/sessions/history/all', this.baseurl);
|
|
159
154
|
url.search = new URLSearchParams(args).toString();
|
|
160
|
-
let raw = await this.query(
|
|
155
|
+
let raw = await this.query({
|
|
156
|
+
path: url.pathname + url.search,
|
|
157
|
+
});
|
|
161
158
|
const totalResults = raw.MediaContainer.totalSize;
|
|
162
159
|
// Filter out null/undefined items from the metadata
|
|
163
160
|
const validMetadata = raw.MediaContainer.Metadata?.filter(Boolean) ?? [];
|
|
164
161
|
results.push(...validMetadata);
|
|
165
162
|
while (results.length <= totalResults &&
|
|
166
163
|
X_PLEX_CONTAINER_SIZE === raw.MediaContainer.size &&
|
|
167
|
-
|
|
164
|
+
maxResults > results.length) {
|
|
168
165
|
args['X-Plex-Container-Start'] = (Number(args['X-Plex-Container-Start']) + Number(args['X-Plex-Container-Size'])).toString();
|
|
169
166
|
url.search = new URLSearchParams(args).toString();
|
|
170
|
-
raw = await this.query(
|
|
167
|
+
raw = await this.query({
|
|
168
|
+
path: url.pathname + url.search,
|
|
169
|
+
});
|
|
171
170
|
// Filter out null/undefined items from the metadata
|
|
172
171
|
const validMetadata = raw.MediaContainer.Metadata?.filter(item => item != null) ?? [];
|
|
173
172
|
results.push(...validMetadata);
|
|
@@ -176,7 +175,9 @@ export class PlexServer {
|
|
|
176
175
|
}
|
|
177
176
|
async settings() {
|
|
178
177
|
if (!this._settings) {
|
|
179
|
-
const data = await this.query(
|
|
178
|
+
const data = await this.query({
|
|
179
|
+
path: Settings.key,
|
|
180
|
+
});
|
|
180
181
|
this._settings = new Settings(this, data.MediaContainer.Setting);
|
|
181
182
|
}
|
|
182
183
|
return this._settings;
|
|
@@ -208,14 +209,21 @@ export class PlexServer {
|
|
|
208
209
|
*/
|
|
209
210
|
myPlexAccount() {
|
|
210
211
|
if (!this._myPlexAccount) {
|
|
211
|
-
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
|
+
});
|
|
212
218
|
}
|
|
213
219
|
return this._myPlexAccount;
|
|
214
220
|
}
|
|
215
221
|
// Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server.
|
|
216
222
|
async clients() {
|
|
217
223
|
const items = [];
|
|
218
|
-
const response = await this.query(
|
|
224
|
+
const response = await this.query({
|
|
225
|
+
path: '/clients',
|
|
226
|
+
});
|
|
219
227
|
if (response.MediaContainer?.Server === undefined) {
|
|
220
228
|
return [];
|
|
221
229
|
}
|
|
@@ -245,7 +253,7 @@ export class PlexServer {
|
|
|
245
253
|
* Build a URL string with proper token argument. Token will be appended to the URL
|
|
246
254
|
* if either includeToken is True or TODO: CONFIG.log.show_secrets is 'true'.
|
|
247
255
|
*/
|
|
248
|
-
url(key, includeToken = false, params) {
|
|
256
|
+
url(key, { includeToken = false, params, } = {}) {
|
|
249
257
|
if (!this.baseurl) {
|
|
250
258
|
throw new Error('PlexClient object missing baseurl.');
|
|
251
259
|
}
|
|
@@ -256,16 +264,16 @@ export class PlexServer {
|
|
|
256
264
|
url.search = searchParams.toString();
|
|
257
265
|
return url;
|
|
258
266
|
}
|
|
267
|
+
if (params) {
|
|
268
|
+
url.search = params.toString();
|
|
269
|
+
}
|
|
259
270
|
return url;
|
|
260
271
|
}
|
|
261
272
|
/**
|
|
262
273
|
* Build the Plex Web URL for the object.
|
|
263
|
-
* @param
|
|
264
|
-
* Default is https://app.plex.tv/desktop.
|
|
265
|
-
* @param endpoint The Plex Web URL endpoint.
|
|
266
|
-
* None for server, 'playlist' for playlists, 'details' for all other media types.
|
|
274
|
+
* @param options Options for the URL.
|
|
267
275
|
*/
|
|
268
|
-
_buildWebURL(base = 'https://app.plex.tv/desktop/', endpoint, params) {
|
|
276
|
+
_buildWebURL({ base = 'https://app.plex.tv/desktop/', endpoint, params, } = {}) {
|
|
269
277
|
const url = new URL(base);
|
|
270
278
|
const queryString = params?.toString() ? `?${params.toString()}` : '';
|
|
271
279
|
if (endpoint) {
|
|
@@ -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
|
}
|