@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.
@@ -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();
@@ -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 path
107
- * @param method
108
- * @param headers
107
+ * @param options
109
108
  */
110
- async query(path, method = 'get', options = {}, username, password) {
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: options.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 maxresults option to that
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 maxresults Only return the specified number of results (optional).
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(maxresults = 9_999_999, mindate, ratingKey, accountId, librarySectionId) {
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 (mindate !== undefined) {
153
- args['viewedAt>'] = Math.floor(mindate.getTime() / 1000).toString();
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, maxresults).toString();
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(url.pathname + url.search);
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
- maxresults > results.length) {
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(url.pathname + url.search);
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(Settings.key);
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(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
+ });
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('/clients');
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 base The base URL before the fragment (``#!``).
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
+ }
@@ -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.1",
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": {