@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.
@@ -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('/library/sections/');
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 mediatype Optionally limit your search to the specified media type.
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 key = `/hubs/search?${new URLSearchParams(params).toString()}`;
97
- const hubs = await fetchItems(this, key, undefined, Hub, this);
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 path
106
- * @param method
107
- * @param headers
107
+ * @param options
108
108
  */
109
- async query(path, method = 'get', options = {}, username, password) {
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: options.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 maxresults option to that
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 maxresults Only return the specified number of results (optional).
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(maxresults = 9_999_999, mindate, ratingKey, accountId, librarySectionId) {
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 (mindate !== undefined) {
152
- args['viewedAt>'] = Math.floor(mindate.getTime() / 1000).toString();
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, maxresults).toString();
151
+ args['X-Plex-Container-Size'] = Math.min(X_PLEX_CONTAINER_SIZE, maxResults).toString();
156
152
  let results = [];
157
- let key = `/status/sessions/history/all?${new URLSearchParams(args).toString()}`;
158
- let raw = await this.query(key);
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
- maxresults > results.length) {
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
- key = `/status/sessions/history/all?${new URLSearchParams(args).toString()}`;
168
- raw = await this.query(key);
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(Settings.key);
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(this.baseurl, undefined, undefined, this.token, this.timeout, this);
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('/clients');
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 base The base URL before the fragment (``#!``).
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
- return `${base}#!/server/${this.machineIdentifier}/${endpoint}?${params?.toString()}`;
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 `${base}#!/media/${this.machineIdentifier}/com.plexapp.plugins.library?${params?.toString()}`;
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
+ }
@@ -48,7 +48,7 @@ export class Settings extends PlexObject {
48
48
  }
49
49
  }
50
50
  const url = `${this.key}?${params.toString()}`;
51
- await this.server.query(url, 'put');
51
+ await this.server.query({ path: url, method: 'put' });
52
52
  }
53
53
  _loadData(data) {
54
54
  this._data = data;
@@ -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
- * Simple tag helper for editing a object.
17
- */
18
- export declare function tagHelper(tag: string, items: string[], locked?: boolean, remove?: boolean): Record<string, string | number>;
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
- * Simple tag helper for editing a object.
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-`;
@@ -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?: string, file?: Uint8Array): Promise<void>;
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(key, 'post', {
77
+ await this.server.query({
78
+ path: key,
79
+ method: 'post',
78
80
  body: file,
79
81
  });
80
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl/plex",
3
- "version": "3.12.0",
3
+ "version": "4.0.0",
4
4
  "description": "plex api client in typescript using ofetch",
5
5
  "author": "Scott Cooper <scttcper@gmail.com>",
6
6
  "publishConfig": {