@ctrl/plex 3.3.0 → 3.5.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 CHANGED
@@ -105,7 +105,8 @@ npm run test-cleanup
105
105
  get a claim token from https://www.plex.tv/claim/
106
106
  export PLEX_CLAIM_TOKEN=claim-token
107
107
 
108
- ```
108
+ start plex container for testing
109
+ ```console
109
110
  docker run -d \
110
111
  --name=plex \
111
112
  --net=host \
@@ -132,7 +133,12 @@ docker run -d \
132
133
  lscr.io/linuxserver/plex:latest
133
134
  ```
134
135
 
135
- bootstrap media
136
+ Pull latest plex container if needed
137
+ ```console
138
+ docker pull lscr.io/linuxserver/plex:latest
136
139
  ```
137
- NODE_OPTIONS="--loader ts-node/esm" node scripts/bootstraptest.ts --no-docker --server-name=orbstack`
140
+
141
+ bootstrap plex server with test media
142
+ ```console
143
+ NODE_OPTIONS="--loader ts-node/esm" node scripts/bootstraptest.ts --no-docker --server-name=orbstack --password=$PLEX_PASSWORD --username=$PLEX_USERNAME
138
144
  ```
@@ -1,6 +1,6 @@
1
1
  import WebSocket from 'ws';
2
- import { AlertTypes } from './alert.types.js';
3
- import { PlexServer } from './server.js';
2
+ import type { AlertTypes } from './alert.types.js';
3
+ import type { PlexServer } from './server.js';
4
4
  export declare class AlertListener {
5
5
  private readonly server;
6
6
  callback: (data: AlertTypes) => void;
package/dist/src/alert.js CHANGED
@@ -8,7 +8,6 @@ export class AlertListener {
8
8
  async run() {
9
9
  const url = this.server.url(this.key, true).toString().replace('http', 'ws');
10
10
  this._ws = new WebSocket(url);
11
- // eslint-disable-next-line @typescript-eslint/ban-types
12
11
  this._ws.on('message', (buffer) => {
13
12
  try {
14
13
  const data = JSON.parse(buffer.toString());
@@ -1,6 +1,5 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import { URL } from 'url';
3
- import { Player } from './client.types.js';
2
+ import type { Player } from './client.types.js';
4
3
  export interface PlexOptions {
5
4
  /** (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional). */
6
5
  server?: any;
@@ -1,12 +1,12 @@
1
- import { Class } from 'type-fest';
1
+ import type { Class } from 'type-fest';
2
2
  import { PartialPlexObject } from './base/partialPlexObject.js';
3
3
  import { PlexObject } from './base/plexObject.js';
4
- import { CollectionData, LibraryRootResponse, Location, SectionsDirectory } from './library.types.js';
4
+ import type { CollectionData, LibraryRootResponse, Location, SectionsDirectory } from './library.types.js';
5
5
  import { Playlist } from './playlist.js';
6
- import { Agent, SEARCHTYPES } from './search.js';
7
- import { SearchResult } from './search.types.js';
6
+ import { type Agent, type SEARCHTYPES } from './search.js';
7
+ import type { SearchResult } from './search.types.js';
8
8
  import type { PlexServer } from './server.js';
9
- import { Movie, Show, VideoType } from './video.js';
9
+ import { Movie, Show, type VideoType } from './video.js';
10
10
  export type Section = MovieSection | ShowSection;
11
11
  export declare class Library {
12
12
  private readonly server;
@@ -364,7 +364,7 @@ export declare abstract class LibrarySection<SectionVideoType = VideoType> exten
364
364
  folders(): Promise<Folder[]>;
365
365
  genres(): Promise<FilterChoice[]>;
366
366
  /**
367
- * Returns a list of available {@link FilteringFields} for a specified libtype.
367
+ * Returns a list of available {@link FilteringField} for a specified libtype.
368
368
  * This is the list of options in the custom filter dropdown menu
369
369
  */
370
370
  listFields(libtype?: Libtype): Promise<FilteringField[]>;
@@ -213,7 +213,6 @@ export class Library {
213
213
  const results = [];
214
214
  const sections = await this.sections();
215
215
  for (const section of sections) {
216
- // eslint-disable-next-line no-await-in-loop
217
216
  const items = await section.all();
218
217
  for (const item of items) {
219
218
  results.push(item);
@@ -227,7 +226,6 @@ export class Library {
227
226
  async emptyTrash() {
228
227
  const sections = await this.sections();
229
228
  for (const section of sections) {
230
- // eslint-disable-next-line no-await-in-loop
231
229
  await section.emptyTrash();
232
230
  }
233
231
  }
@@ -485,7 +483,7 @@ export class LibrarySection extends PlexObject {
485
483
  return fetchItems(this.server, key, undefined, FilterChoice);
486
484
  }
487
485
  /**
488
- * Returns a list of available {@link FilteringFields} for a specified libtype.
486
+ * Returns a list of available {@link FilteringField} for a specified libtype.
489
487
  * This is the list of options in the custom filter dropdown menu
490
488
  */
491
489
  async listFields(libtype = this.type) {
@@ -496,7 +494,7 @@ export class LibrarySection extends PlexObject {
496
494
  const filter = filterTypes.find(f => f.type === libtype);
497
495
  if (!filter) {
498
496
  throw new NotFound(`Unknown libtype "${libtype}" for this library.
499
- Available libtypes: ${filterTypes.join(', ')}`);
497
+ Available libtypes: ${filterTypes.map(f => f.type).join(', ')}`);
500
498
  }
501
499
  return filter;
502
500
  }
@@ -1,4 +1,4 @@
1
- import { ChapterSource, MarkerData, MediaTagData } from './video.types.js';
1
+ import type { ChapterSource, MarkerData, MediaTagData } from './video.types.js';
2
2
  export interface LibraryRootResponse {
3
3
  size: number;
4
4
  allowSync: boolean;
@@ -1,5 +1,5 @@
1
1
  import { PlexObject } from './base/plexObject.js';
2
- import { ChapterData, MarkerData, MediaData, MediaPartData, MediaPartStreamData } from './video.types.js';
2
+ import type { ChapterData, MarkerData, MediaData, MediaPartData, MediaPartStreamData } from './video.types.js';
3
3
  /**
4
4
  * Base class for media tags used for filtering and searching your library
5
5
  * items or navigating the metadata of media items in your library. Tags are
@@ -1,5 +1,5 @@
1
1
  import { PlexObject } from './base/plexObject.js';
2
- import { Connection, Device, ResourcesResponse } from './myplex.types.js';
2
+ import type { Connection, Device, ResourcesResponse, WebLogin } from './myplex.types.js';
3
3
  import { PlexServer } from './server.js';
4
4
  /**
5
5
  * MyPlex account and profile information. This object represents the data found Account on
@@ -16,6 +16,19 @@ export declare class MyPlexAccount {
16
16
  timeout: number;
17
17
  server?: PlexServer;
18
18
  static key: string;
19
+ /**
20
+ * This follows the outline described in https://forums.plex.tv/t/authenticating-with-plex/609370
21
+ * to fetch a token and potentially compromise username and password. To use first call `getWebLogin()`
22
+ * and present the returned uri to a user to go to, then await `webLoginCheck()`. If you pass in a
23
+ * `forwardUrl`, then send the user to the returned uri, and when a request comes in on the passed in
24
+ * url, then await `webLoginCheck()`.
25
+ */
26
+ static getWebLogin(forwardUrl?: string | null): Promise<WebLogin>;
27
+ /**
28
+ * Pass in the `webLogin` object obtained from `getWebLogin()` and this will poll Plex to see if
29
+ * the user agreed. It returns a connected `MyPlexAccount` or throws an error.
30
+ */
31
+ static webLoginCheck(webLogin: WebLogin, timeoutSeconds?: number): Promise<MyPlexAccount>;
19
32
  FRIENDINVITE: string;
20
33
  HOMEUSERCREATE: string;
21
34
  EXISTINGUSER: string;
@@ -136,7 +149,7 @@ export declare class MyPlexAccount {
136
149
  */
137
150
  export declare class MyPlexResource {
138
151
  readonly account: MyPlexAccount;
139
- private baseUrl;
152
+ private readonly baseUrl;
140
153
  static key: string;
141
154
  TAG: string;
142
155
  /** Descriptive name of this resource */
@@ -1,4 +1,4 @@
1
- import { URLSearchParams } from 'url';
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
2
  import { ofetch } from 'ofetch';
3
3
  import pAny from 'p-any';
4
4
  import { parseStringPromise } from 'xml2js';
@@ -14,6 +14,71 @@ import { PlexServer } from './server.js';
14
14
  */
15
15
  export class MyPlexAccount {
16
16
  static { this.key = 'https://plex.tv/api/v2/user'; }
17
+ /**
18
+ * This follows the outline described in https://forums.plex.tv/t/authenticating-with-plex/609370
19
+ * to fetch a token and potentially compromise username and password. To use first call `getWebLogin()`
20
+ * and present the returned uri to a user to go to, then await `webLoginCheck()`. If you pass in a
21
+ * `forwardUrl`, then send the user to the returned uri, and when a request comes in on the passed in
22
+ * url, then await `webLoginCheck()`.
23
+ */
24
+ static async getWebLogin(forwardUrl = null) {
25
+ const appName = BASE_HEADERS['X-Plex-Product'];
26
+ const clientIdentifier = BASE_HEADERS['X-Plex-Client-Identifier'];
27
+ const pin = await ofetch('https://plex.tv/api/v2/pins', {
28
+ method: 'post',
29
+ headers: {
30
+ Accept: 'application/json',
31
+ },
32
+ query: {
33
+ strong: 'true',
34
+ 'X-Plex-Product': appName,
35
+ 'X-Plex-Client-Identifier': clientIdentifier,
36
+ },
37
+ });
38
+ return {
39
+ ...pin,
40
+ uri: `https://app.plex.tv/auth#?clientID=${encodeURIComponent(clientIdentifier)}&code=${encodeURIComponent(pin.code)}&context%5Bdevice%5D%5Bproduct%5D=${encodeURIComponent(appName)}${forwardUrl ? '&forwardUrl=' + encodeURIComponent(forwardUrl) : ''}`,
41
+ };
42
+ }
43
+ /**
44
+ * Pass in the `webLogin` object obtained from `getWebLogin()` and this will poll Plex to see if
45
+ * the user agreed. It returns a connected `MyPlexAccount` or throws an error.
46
+ */
47
+ static async webLoginCheck(webLogin, timeoutSeconds = 60) {
48
+ const recheckMs = 3000;
49
+ const clientIdentifier = BASE_HEADERS['X-Plex-Client-Identifier'];
50
+ const uri = `https://plex.tv/api/v2/pins/${webLogin.id}`;
51
+ const startTime = Date.now();
52
+ while (Date.now() < startTime + timeoutSeconds * 1000) {
53
+ try {
54
+ const tokenResponse = await ofetch(uri, {
55
+ method: 'GET',
56
+ headers: {
57
+ Accept: 'application/json',
58
+ },
59
+ query: {
60
+ code: webLogin.code,
61
+ 'X-Plex-Client-Identifier': clientIdentifier,
62
+ },
63
+ timeout: recheckMs,
64
+ retry: 5,
65
+ retryDelay: recheckMs,
66
+ });
67
+ if (tokenResponse.authToken) {
68
+ const myPlexAccount = new MyPlexAccount(null, '', '', tokenResponse.authToken);
69
+ return await myPlexAccount.connect();
70
+ }
71
+ await sleep(recheckMs);
72
+ }
73
+ catch (err) {
74
+ if (err.message.includes('aborted')) {
75
+ continue;
76
+ }
77
+ throw err;
78
+ }
79
+ }
80
+ throw new Error('Failed to authenticate before timeout');
81
+ }
17
82
  /**
18
83
  *
19
84
  * @param username Your MyPlex username
@@ -68,7 +133,6 @@ export class MyPlexAccount {
68
133
  this._loadData(data);
69
134
  return this;
70
135
  }
71
- // log('Logging in with token');
72
136
  const data = await this.query(MyPlexAccount.key);
73
137
  this._loadData(data);
74
138
  return this;
@@ -130,3 +130,8 @@ export interface Device {
130
130
  };
131
131
  }>;
132
132
  }
133
+ export interface WebLogin {
134
+ id: number;
135
+ code: string;
136
+ uri: string;
137
+ }
@@ -1,8 +1,8 @@
1
1
  import { Playable } from './base/playable.js';
2
2
  import type { Section } from './library.js';
3
- import { PlaylistResponse } from './playlist.types.js';
3
+ import type { PlaylistResponse } from './playlist.types.js';
4
4
  import type { PlexServer } from './server.js';
5
- import { Episode, Movie, VideoType } from './video.js';
5
+ import { Episode, Movie, type VideoType } from './video.js';
6
6
  interface CreateRegularPlaylistOptions {
7
7
  /** True to create a smart playlist */
8
8
  smart?: false;
@@ -99,10 +99,8 @@ export class Playlist extends Playable {
99
99
  throw new BadRequest('Cannot remove items to a smart playlist.');
100
100
  }
101
101
  for (const item of items) {
102
- // eslint-disable-next-line no-await-in-loop
103
102
  const playlistItemId = await this._getPlaylistItemID(item);
104
103
  const key = `${this.key}/items/${playlistItemId}`;
105
- // eslint-disable-next-line no-await-in-loop
106
104
  await this.server.query(key, 'delete');
107
105
  }
108
106
  }
@@ -1,6 +1,6 @@
1
- import { ValueOf } from 'type-fest';
1
+ import type { ValueOf } from 'type-fest';
2
2
  import { PlexObject } from './base/plexObject.js';
3
- import { MatchSearchResult } from './search.types.js';
3
+ import type { MatchSearchResult } from './search.types.js';
4
4
  export declare class SearchResult extends PlexObject {
5
5
  static TAG: string;
6
6
  guid: string;
@@ -1,11 +1,10 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import { URL, URLSearchParams } from 'url';
3
2
  import { PlexClient } from './client.js';
4
3
  import { Hub, Library } from './library.js';
5
4
  import { Optimized } from './media.js';
6
5
  import { MyPlexAccount } from './myplex.js';
7
6
  import { Agent, SEARCHTYPES } from './search.js';
8
- import { HistoryMetadatum } from './server.types.js';
7
+ import type { HistoryMetadatum } from './server.types.js';
9
8
  import { Settings } from './settings.js';
10
9
  /**
11
10
  * This is the main entry point to interacting with a Plex server. It allows you to
@@ -156,7 +156,6 @@ export class PlexServer {
156
156
  maxresults > results.length) {
157
157
  args['X-Plex-Container-Start'] = (Number(args['X-Plex-Container-Start']) + Number(args['X-Plex-Container-Size'])).toString();
158
158
  key = '/status/sessions/history/all?' + new URLSearchParams(args).toString();
159
- // eslint-disable-next-line no-await-in-loop
160
159
  raw = await this.query(key);
161
160
  results = results.concat(raw.MediaContainer.Metadata);
162
161
  }
@@ -185,7 +184,6 @@ export class PlexServer {
185
184
  * you're likley to recieve an authentication error calling this.
186
185
  */
187
186
  myPlexAccount() {
188
- // eslint-disable-next-line logical-assignment-operators
189
187
  if (!this._myPlexAccount) {
190
188
  this._myPlexAccount = new MyPlexAccount(this.baseurl, undefined, undefined, this.token, this.timeout, this);
191
189
  }
@@ -205,7 +203,6 @@ export class PlexServer {
205
203
  }
206
204
  for (const server of response.MediaContainer.Server) {
207
205
  let { port } = server;
208
- // eslint-disable-next-line logical-assignment-operators
209
206
  if (!port) {
210
207
  // TODO: print warning about doing weird port stuff
211
208
  port = Number(ports?.[server.machineIdentifier]);
@@ -55,7 +55,6 @@ export class Settings extends PlexObject {
55
55
  }
56
56
  _loadData(data) {
57
57
  this._data = data;
58
- // eslint-disable-next-line logical-assignment-operators
59
58
  this._settings = this._settings ?? {};
60
59
  for (const elem of data) {
61
60
  const id = lowerFirst(elem.id);
@@ -1,9 +1,8 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- import { URL } from 'url';
1
+ import type { URL } from 'url';
3
2
  import { Playable } from './base/playable.js';
4
- import { ExtrasData, FullShowData, MovieData, ShowData } from './library.types.js';
3
+ import type { ExtrasData, FullShowData, MovieData, ShowData } from './library.types.js';
5
4
  import { Chapter, Collection, Country, Director, Genre, Guid, Marker, Media, Poster, Producer, Rating, Role, Similar, Writer } from './media.js';
6
- import { ChapterSource, EpisodeMetadata, FullMovieResponse } from './video.types.js';
5
+ import type { ChapterSource, EpisodeMetadata, FullMovieResponse } from './video.types.js';
7
6
  export type VideoType = Movie | Show;
8
7
  declare abstract class Video extends Playable {
9
8
  /** Datetime this item was added to the library. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl/plex",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "plex api client in typescript",
5
5
  "author": "Scott Cooper <scttcper@gmail.com>",
6
6
  "publishConfig": {
@@ -20,12 +20,12 @@
20
20
  "typescript"
21
21
  ],
22
22
  "scripts": {
23
- "lint": "npm run lint:biome && npm run lint:eslint",
23
+ "lint": "pnpm run '/^(lint:biome|lint:eslint)$/'",
24
24
  "lint:biome": "biome check .",
25
- "lint:eslint": "eslint --ext .ts,.tsx .",
26
- "lint:fix": "npm run lint:biome:fix && npm run lint:eslint:fix",
27
- "lint:eslint:fix": "eslint --ext .ts,.tsx . --fix",
28
- "lint:biome:fix": "biome check . --apply",
25
+ "lint:eslint": "eslint .",
26
+ "lint:fix": "pnpm run '/^(lint:biome|lint:eslint):fix$/'",
27
+ "lint:eslint:fix": "eslint . --fix",
28
+ "lint:biome:fix": "biome check . --write",
29
29
  "prepare": "npm run build",
30
30
  "build": "tsc",
31
31
  "build:docs": "typedoc",
@@ -37,37 +37,32 @@
37
37
  "test-cleanup": "node --loader ts-node/esm scripts/test-cleanup.ts"
38
38
  },
39
39
  "dependencies": {
40
- "@ctrl/mac-address": "^3.0.3",
41
- "ofetch": "^1.3.4",
40
+ "@ctrl/mac-address": "^3.1.0",
41
+ "ofetch": "^1.4.1",
42
42
  "p-any": "^4.0.0",
43
- "type-fest": "^4.18.1",
44
- "ws": "^8.17.0",
43
+ "type-fest": "^4.40.1",
44
+ "ws": "^8.18.2",
45
45
  "xml2js": "^0.6.2"
46
46
  },
47
47
  "devDependencies": {
48
- "@biomejs/biome": "1.7.2",
49
- "@ctrl/eslint-config-biome": "2.6.7",
50
- "@sindresorhus/tsconfig": "5.0.0",
51
- "@types/lodash": "4.17.1",
52
- "@types/micromatch": "4.0.7",
53
- "@types/node": "20.12.8",
54
- "@types/ws": "8.5.10",
48
+ "@biomejs/biome": "1.9.4",
49
+ "@ctrl/eslint-config-biome": "4.4.0",
50
+ "@sindresorhus/tsconfig": "7.0.0",
51
+ "@types/node": "22.15.3",
52
+ "@types/ws": "8.18.1",
55
53
  "@types/xml2js": "0.4.14",
56
- "@types/yargs": "17.0.32",
57
- "@vitest/coverage-v8": "1.6.0",
58
- "delay": "6.0.0",
59
- "eslint": "8.57.0",
60
- "execa": "8.0.1",
61
- "glob": "10.3.12",
62
- "globby": "14.0.1",
63
- "lodash": "4.17.21",
54
+ "@types/yargs": "17.0.33",
55
+ "@vitest/coverage-v8": "3.1.2",
56
+ "eslint": "9.26.0",
57
+ "execa": "9.5.2",
58
+ "globby": "14.1.0",
64
59
  "make-dir": "5.0.0",
65
- "ora": "8.0.1",
66
- "p-retry": "6.2.0",
60
+ "ora": "8.2.0",
61
+ "p-retry": "6.2.1",
67
62
  "ts-node": "10.9.2",
68
- "typedoc": "0.25.13",
69
- "typescript": "5.4.5",
70
- "vitest": "1.6.0",
63
+ "typedoc": "0.28.3",
64
+ "typescript": "5.8.3",
65
+ "vitest": "3.1.2",
71
66
  "yargs": "17.7.2"
72
67
  },
73
68
  "release": {
@@ -77,5 +72,6 @@
77
72
  },
78
73
  "engines": {
79
74
  "node": ">=18"
80
- }
75
+ },
76
+ "packageManager": "pnpm@10.10.0"
81
77
  }