@ctrl/plex 3.9.2 → 3.11.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
@@ -84,31 +84,37 @@ export PLEX_USERNAME=email
84
84
  export PLEX_PASSWORD=password
85
85
  ```
86
86
 
87
- Claim server and setup test content (once)
87
+ Setup test content (once)
88
88
 
89
89
  ```sh
90
- npm run claim-server && npm run add-media
90
+ pnpm run add-media
91
91
  ```
92
92
 
93
93
  Run tests
94
94
 
95
95
  ```sh
96
- npm test
96
+ pnpm test
97
97
  ```
98
98
 
99
99
  Post testing, remove plex server from account. Warning this is destructive. Do not use this on a real plex account.
100
100
 
101
101
  ```sh
102
- npm run test-cleanup
102
+ pnpm run test-cleanup
103
103
  ```
104
104
 
105
- ### Running tests locally (mostly for myself)
105
+ ### Running tests locally
106
+
107
+ #### Step 1
106
108
 
107
109
  get a claim token from https://www.plex.tv/claim/
108
110
  export PLEX_CLAIM_TOKEN=claim-token
109
111
 
110
112
  Start plex container for testing. Replace `/Users/scooper/gh/plex` with the path to this repo's directory.
111
113
 
114
+ #### Step 2
115
+
116
+ Replace `/Users/scooper/gh/plex` with the path to this repo's directory.
117
+
112
118
  ```console
113
119
  docker run -d \
114
120
  --name=plex \
package/dist/src/alert.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import WebSocket from 'ws';
2
2
  export class AlertListener {
3
+ server;
4
+ callback;
5
+ key = '/:/websockets/notifications';
3
6
  constructor(server, callback) {
4
7
  this.server = server;
5
8
  this.callback = callback;
6
- this.key = '/:/websockets/notifications';
7
9
  }
8
10
  async run() {
9
11
  const url = this.server.url(this.key, true).toString().replace('http', 'ws');
@@ -1,4 +1,4 @@
1
- import { PartialPlexObject } from './base/partialPlexObject.js';
1
+ import { Playable } from './base/playable.js';
2
2
  import { PlexObject } from './base/plexObject.js';
3
3
  import type { AlbumData, ArtistData, TrackData } from './audio.types.js';
4
4
  import { Chapter, Collection, Country, Field, Format, Genre, Guid, Image, Label, Media, Mood, Similar, Style, Subformat } from './media.js';
@@ -6,7 +6,7 @@ import type { PlexServer } from './server.js';
6
6
  /**
7
7
  * Base class for all audio objects including Artist, Album, and Track.
8
8
  */
9
- export declare class Audio extends PartialPlexObject {
9
+ export declare class Audio extends Playable {
10
10
  /** Default metadata type for audio sync items. */
11
11
  static METADATA_TYPE: string;
12
12
  /** Hardcoded list type for filtering. */
package/dist/src/audio.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { URLSearchParams } from 'url';
2
- import { PartialPlexObject } from './base/partialPlexObject.js';
2
+ import { Playable } from './base/playable.js';
3
3
  import { fetchItem, fetchItems } from './baseFunctionality.js';
4
4
  import { Chapter, Collection, Country, Field, Format, Genre, Guid, Image, Label, Media, Mood, Similar, Style, Subformat, } from './media.js';
5
5
  /**
6
6
  * Base class for all audio objects including Artist, Album, and Track.
7
7
  */
8
- export class Audio extends PartialPlexObject {
8
+ export class Audio extends Playable {
9
9
  /** Default metadata type for audio sync items. */
10
- static { this.METADATA_TYPE = 'track'; }
10
+ static METADATA_TYPE = 'track';
11
11
  /** Hardcoded list type for filtering. */
12
12
  get listType() {
13
13
  return 'audio';
@@ -114,6 +114,8 @@ export class Audio extends PartialPlexObject {
114
114
  this.musicAnalysisVersion = isNaN(musicAnalysisVersionInt)
115
115
  ? undefined
116
116
  : musicAnalysisVersionInt;
117
+ this.playlistItemID = data.playlistItemID;
118
+ this.ratingKey = data.ratingKey;
117
119
  const ratingKeyInt = data.ratingKey ? parseInt(data.ratingKey, 10) : NaN;
118
120
  this.ratingKey = isNaN(ratingKeyInt) ? this.ratingKey : ratingKeyInt.toString();
119
121
  this.summary = data.summary;
@@ -151,8 +153,8 @@ export class Audio extends PartialPlexObject {
151
153
  * Represents a single Track.
152
154
  */
153
155
  export class Track extends Audio {
154
- static { this.TAG = 'Track'; }
155
- static { this.TYPE = 'track'; }
156
+ static TAG = 'Track';
157
+ static TYPE = 'track';
156
158
  // Properties from Mixins (assuming, some might overlap with Audio)
157
159
  // userRating inherited from Audio
158
160
  // art inherited from Audio
@@ -307,8 +309,8 @@ export class Track extends Audio {
307
309
  */
308
310
  export class Artist extends Audio {
309
311
  /* implements AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, ArtMixin, PosterMixin, ThemeMixin, ArtistEditMixins */
310
- static { this.TAG = 'Directory'; }
311
- static { this.TYPE = 'artist'; }
312
+ static TAG = 'Directory';
313
+ static TYPE = 'artist';
312
314
  get locations() {
313
315
  // Replicate listAttrs logic (assuming Location tag with path attribute)
314
316
  return this._data?.Location?.map((loc) => loc.path).filter(Boolean) ?? [];
@@ -383,7 +385,6 @@ export class Artist extends Audio {
383
385
  if (e.constructor.name === 'NotFound') {
384
386
  return undefined;
385
387
  }
386
- // eslint-disable-next-line @typescript-eslint/only-throw-error
387
388
  throw e;
388
389
  }
389
390
  }
@@ -479,8 +480,8 @@ export class Artist extends Audio {
479
480
  * Represents a single Album.
480
481
  */
481
482
  export class Album extends Audio {
482
- static { this.TAG = 'Directory'; }
483
- static { this.TYPE = 'album'; }
483
+ static TAG = 'Directory';
484
+ static TYPE = 'album';
484
485
  constructor(server, data, initpath, parent) {
485
486
  super(server, data, initpath, parent);
486
487
  this._loadData(data);
@@ -100,7 +100,7 @@ export declare abstract class PartialPlexObject extends PlexObject {
100
100
  * @param maxresults Only return the specified number of results (optional).
101
101
  * @param mindate Min datetime to return results from.
102
102
  */
103
- history(maxresults?: number, mindate?: Date): Promise<import("../server.types.js").HistoryMetadatum[]>;
103
+ history(maxresults?: number, mindate?: Date): Promise<import("../server.types.js").HistoryResult[]>;
104
104
  section(): Promise<Section>;
105
105
  /**
106
106
  * Delete a media element. This has to be enabled under settings > server > library in plex webui.
@@ -3,31 +3,28 @@ import { SearchResult, searchType } from '../search.js';
3
3
  import { getAgentIdentifier, ltrim, tagHelper } from '../util.js';
4
4
  import { PlexObject } from './plexObject.js';
5
5
  export class PartialPlexObject extends PlexObject {
6
- constructor() {
7
- super(...arguments);
8
- this._INCLUDES = {
9
- checkFiles: 0,
10
- includeAllConcerts: 0,
11
- includeBandwidths: 1,
12
- includeChapters: 1,
13
- includeChildren: 0,
14
- includeConcerts: 0,
15
- includeExternalMedia: 0,
16
- includeExtras: 0,
17
- includeFields: 'thumbBlurHash,artBlurHash',
18
- includeGeolocation: 1,
19
- includeLoudnessRamps: 1,
20
- includeMarkers: 1,
21
- includeOnDeck: 0,
22
- includePopularLeaves: 0,
23
- includePreferences: 0,
24
- includeRelated: 0,
25
- includeRelatedCount: 0,
26
- includeReviews: 0,
27
- includeStations: 0,
28
- };
29
- this._detailsKey = this._buildDetailsKey();
30
- }
6
+ _INCLUDES = {
7
+ checkFiles: 0,
8
+ includeAllConcerts: 0,
9
+ includeBandwidths: 1,
10
+ includeChapters: 1,
11
+ includeChildren: 0,
12
+ includeConcerts: 0,
13
+ includeExternalMedia: 0,
14
+ includeExtras: 0,
15
+ includeFields: 'thumbBlurHash,artBlurHash',
16
+ includeGeolocation: 1,
17
+ includeLoudnessRamps: 1,
18
+ includeMarkers: 1,
19
+ includeOnDeck: 0,
20
+ includePopularLeaves: 0,
21
+ includePreferences: 0,
22
+ includeRelated: 0,
23
+ includeRelatedCount: 0,
24
+ includeReviews: 0,
25
+ includeStations: 0,
26
+ };
27
+ _detailsKey = this._buildDetailsKey();
31
28
  /**
32
29
  * Tell Plex Media Server to performs analysis on it this item to gather
33
30
  * information. Analysis includes:
@@ -3,10 +3,18 @@ import { URLSearchParams } from 'url';
3
3
  * Base class for all? Plex objects
4
4
  */
5
5
  export class PlexObject {
6
+ server;
6
7
  /** xml element tag */
7
- static { this.TAG = null; }
8
+ static TAG = null;
8
9
  /** xml element type */
9
- static { this.TYPE = null; }
10
+ static TYPE = null;
11
+ /**
12
+ * WeakRef to the parent object that this object is built from.
13
+ */
14
+ parent;
15
+ _detailsKey;
16
+ initpath;
17
+ _INCLUDES;
10
18
  constructor(server, data, initpath, parent) {
11
19
  this.server = server;
12
20
  this.initpath = initpath ?? this.key;
@@ -57,6 +57,7 @@ export declare class PlexClient {
57
57
  _token: string | null;
58
58
  TAG: string;
59
59
  key: string;
60
+ timeout: number;
60
61
  deviceClass?: string;
61
62
  machineIdentifier?: string;
62
63
  product?: string;
@@ -66,7 +67,6 @@ export declare class PlexClient {
66
67
  platform?: string;
67
68
  platformVersion?: string;
68
69
  title?: string;
69
- timeout: number;
70
70
  constructor(options?: PlexOptions);
71
71
  /**
72
72
  * Alias of reload as any subsequent requests to this client will be
@@ -32,17 +32,18 @@ import { BASE_HEADERS, TIMEOUT } from './config.js';
32
32
  * :func:`~plexapi.client.PlexClient.proxyThroughServer()` (default False).
33
33
  */
34
34
  export class PlexClient {
35
+ /**
36
+ * HTTP address of the client
37
+ */
38
+ _baseurl = null;
39
+ /**
40
+ * Token used to access this client
41
+ */
42
+ _token = null;
43
+ TAG = 'Player';
44
+ key = '/resources';
45
+ timeout;
35
46
  constructor(options = {}) {
36
- /**
37
- * HTTP address of the client
38
- */
39
- this._baseurl = null;
40
- /**
41
- * Token used to access this client
42
- */
43
- this._token = null;
44
- this.TAG = 'Player';
45
- this.key = '/resources';
46
47
  if (options.baseurl) {
47
48
  if (options.baseurl.endsWith('/')) {
48
49
  this._baseurl = options.baseurl.slice(0, -1);
@@ -1,39 +1,39 @@
1
1
  export class BadRequest extends Error {
2
+ message = 'An invalid request, generally a user error.';
2
3
  constructor(message) {
3
4
  super(message);
4
- this.message = 'An invalid request, generally a user error.';
5
5
  this.name = 'BadRequest';
6
6
  this.message = message;
7
7
  }
8
8
  }
9
9
  export class NotFound extends Error {
10
+ message = 'Request media item or device is not found.';
10
11
  constructor(message) {
11
12
  super(message);
12
- this.message = 'Request media item or device is not found.';
13
13
  this.name = 'NotFound';
14
14
  this.message = message;
15
15
  }
16
16
  }
17
17
  export class UnknownType extends Error {
18
+ message = 'Unknown library type.';
18
19
  constructor(message) {
19
20
  super(message);
20
- this.message = 'Unknown library type.';
21
21
  this.name = 'UnknownType';
22
22
  this.message = message;
23
23
  }
24
24
  }
25
25
  export class Unsupported extends Error {
26
+ message = 'Unsupported client request.';
26
27
  constructor(message) {
27
28
  super(message);
28
- this.message = 'Unsupported client request.';
29
29
  this.name = 'Unsupported';
30
30
  this.message = message;
31
31
  }
32
32
  }
33
33
  export class Unauthorized extends Error {
34
+ message = 'Invalid username or password.';
34
35
  constructor(message) {
35
36
  super(message);
36
- this.message = 'Invalid username or password.';
37
37
  this.name = 'Unauthorized';
38
38
  this.message = message;
39
39
  }
@@ -8,6 +8,7 @@ export * from './myplex.js';
8
8
  export * from './playlist.js';
9
9
  export * from './playqueue.js';
10
10
  export * from './server.js';
11
+ export type { HistoryResult } from './server.types.js';
11
12
  export * from './video.js';
12
13
  export * from './audio.js';
13
14
  export { X_PLEX_IDENTIFIER } from './config.js';
@@ -5,7 +5,7 @@ import { Album, Artist, Track } from './audio.js';
5
5
  import type { CollectionData, LibraryRootResponse, Location, SectionsDirectory } from './library.types.js';
6
6
  import { Playlist } from './playlist.js';
7
7
  import { type Agent, type SEARCHTYPES } from './search.js';
8
- import type { SearchResult } from './search.types.js';
8
+ import type { SearchResultContainer } from './search.types.js';
9
9
  import type { PlexServer } from './server.js';
10
10
  import { Movie, Show } from './video.js';
11
11
  export type Section = MovieSection | ShowSection | MusicSection;
@@ -26,7 +26,7 @@ export declare class Library {
26
26
  */
27
27
  sections(): Promise<Section[]>;
28
28
  section<T extends Section = Section>(title: string): Promise<T>;
29
- sectionByID(sectionId: string | number): Promise<Section>;
29
+ sectionByID<T extends Section = Section>(sectionId: string | number): Promise<T>;
30
30
  /**
31
31
  * Simplified add for the most common options.
32
32
  *
@@ -468,9 +468,9 @@ export declare class Hub extends PlexObject {
468
468
  size: number;
469
469
  title: string;
470
470
  type: string;
471
- Directory: SearchResult['Directory'];
472
- Metadata: SearchResult['Metadata'];
473
- protected _loadData(data: SearchResult): void;
471
+ Directory: SearchResultContainer['Directory'];
472
+ Metadata: SearchResultContainer['Metadata'];
473
+ protected _loadData(data: SearchResultContainer): void;
474
474
  }
475
475
  /**
476
476
  * Represents a Folder inside a library.
@@ -8,7 +8,8 @@ import { Playlist } from './playlist.js';
8
8
  import { searchType } from './search.js';
9
9
  import { Movie, Show } from './video.js';
10
10
  export class Library {
11
- static { this.key = '/library'; }
11
+ server;
12
+ static key = '/library';
12
13
  constructor(server, data) {
13
14
  this.server = server;
14
15
  this._loadData(data);
@@ -26,7 +27,6 @@ export class Library {
26
27
  for (const elem of elems.MediaContainer.Directory) {
27
28
  for (const cls of [MovieSection, ShowSection, MusicSection]) {
28
29
  if (cls.TYPE === elem.type) {
29
- // eslint-disable-next-line new-cap
30
30
  const instance = new cls(this.server, elem, key);
31
31
  sections.push(instance);
32
32
  }
@@ -36,9 +36,9 @@ export class Library {
36
36
  }
37
37
  async section(title) {
38
38
  const sections = await this.sections();
39
- const section = sections.find(s => s.title.toLowerCase() === title.toLowerCase());
39
+ const section = sections.find(s => s.title?.toLowerCase() === title.toLowerCase());
40
40
  if (!section) {
41
- const avilableSections = sections.map(s => s.title).join(', ');
41
+ const avilableSections = sections.map(s => s.title || 'Unknown').join(', ');
42
42
  throw new Error(`Invalid library section: ${title}. Available: ${avilableSections}`);
43
43
  }
44
44
  return section;
@@ -276,9 +276,10 @@ export class Library {
276
276
  * Base class for a single library section.
277
277
  */
278
278
  export class LibrarySection extends PlexObject {
279
- static { this.ALLOWED_FILTERS = []; }
280
- static { this.ALLOWED_SORT = []; }
281
- static { this.BOOLEAN_FILTERS = ['unwatched', 'duplicate']; }
279
+ static ALLOWED_FILTERS = [];
280
+ static ALLOWED_SORT = [];
281
+ static BOOLEAN_FILTERS = ['unwatched', 'duplicate'];
282
+ SECTION_TYPE;
282
283
  async all(sort = '') {
283
284
  let sortStr = '';
284
285
  if (sort) {
@@ -574,14 +575,8 @@ export class LibrarySection extends PlexObject {
574
575
  }
575
576
  }
576
577
  export class MovieSection extends LibrarySection {
577
- constructor() {
578
- super(...arguments);
579
- this.METADATA_TYPE = 'movie';
580
- this.CONTENT_TYPE = 'video';
581
- this.SECTION_TYPE = Movie;
582
- }
583
- static { this.TYPE = 'movie'; }
584
- static { this.ALLOWED_FILTERS = [
578
+ static TYPE = 'movie';
579
+ static ALLOWED_FILTERS = [
585
580
  'unwatched',
586
581
  'duplicate',
587
582
  'year',
@@ -603,8 +598,8 @@ export class MovieSection extends LibrarySection {
603
598
  'lastViewedAt',
604
599
  'viewCount',
605
600
  'addedAt',
606
- ]; }
607
- static { this.ALLOWED_SORT = [
601
+ ];
602
+ static ALLOWED_SORT = [
608
603
  'addedAt',
609
604
  'originallyAvailableAt',
610
605
  'lastViewedAt',
@@ -612,18 +607,15 @@ export class MovieSection extends LibrarySection {
612
607
  'rating',
613
608
  'mediaHeight',
614
609
  'duration',
615
- ]; }
616
- static { this.TAG = 'Directory'; }
610
+ ];
611
+ static TAG = 'Directory';
612
+ METADATA_TYPE = 'movie';
613
+ CONTENT_TYPE = 'video';
614
+ SECTION_TYPE = Movie;
617
615
  }
618
616
  export class ShowSection extends LibrarySection {
619
- constructor() {
620
- super(...arguments);
621
- this.METADATA_TYPE = 'episode';
622
- this.CONTENT_TYPE = 'video';
623
- this.SECTION_TYPE = Show;
624
- }
625
- static { this.TYPE = 'show'; }
626
- static { this.ALLOWED_FILTERS = [
617
+ static TYPE = 'show';
618
+ static ALLOWED_FILTERS = [
627
619
  'unwatched',
628
620
  'year',
629
621
  'genre',
@@ -649,16 +641,19 @@ export class ShowSection extends LibrarySection {
649
641
  'episode.userRating',
650
642
  'episode.viewCount',
651
643
  'episode.lastViewedAt',
652
- ]; }
653
- static { this.ALLOWED_SORT = [
644
+ ];
645
+ static ALLOWED_SORT = [
654
646
  'addedAt',
655
647
  'lastViewedAt',
656
648
  'originallyAvailableAt',
657
649
  'titleSort',
658
650
  'rating',
659
651
  'unwatched',
660
- ]; }
661
- static { this.TAG = 'Directory'; }
652
+ ];
653
+ static TAG = 'Directory';
654
+ METADATA_TYPE = 'episode';
655
+ CONTENT_TYPE = 'video';
656
+ SECTION_TYPE = Show;
662
657
  // TODO: figure out how to return episode objects
663
658
  // /**
664
659
  // * Search for an episode. See :func:`~plexapi.library.LibrarySection.search` for usage.
@@ -676,14 +671,11 @@ export class ShowSection extends LibrarySection {
676
671
  }
677
672
  }
678
673
  export class MusicSection extends LibrarySection {
679
- constructor() {
680
- super(...arguments);
681
- this.METADATA_TYPE = 'track';
682
- this.CONTENT_TYPE = 'audio';
683
- this.SECTION_TYPE = Track;
684
- }
685
- static { this.TYPE = 'artist'; }
686
- static { this.TAG = 'Directory'; }
674
+ static TYPE = 'artist';
675
+ static TAG = 'Directory';
676
+ METADATA_TYPE = 'track';
677
+ CONTENT_TYPE = 'audio';
678
+ SECTION_TYPE = Track;
687
679
  /** Returns a list of Album objects in this section. */
688
680
  async albums() {
689
681
  const key = `/library/sections/${this.key}/albums`;
@@ -737,7 +729,7 @@ export class MusicSection extends LibrarySection {
737
729
  }
738
730
  /** Represents a single Hub (or category) in the PlexServer search */
739
731
  export class Hub extends PlexObject {
740
- static { this.TAG = 'Hub'; }
732
+ static TAG = 'Hub';
741
733
  _loadData(data) {
742
734
  this.hubIdentifier = data.hubIdentifier;
743
735
  this.size = data.size;
@@ -767,11 +759,8 @@ export class Folder extends PlexObject {
767
759
  }
768
760
  }
769
761
  export class Collections extends PartialPlexObject {
770
- constructor() {
771
- super(...arguments);
772
- this.TYPE = 'collection';
773
- }
774
- static { this.TAG = 'Directory'; }
762
+ static TAG = 'Directory';
763
+ TYPE = 'collection';
775
764
  // Alias for childCount
776
765
  get size() {
777
766
  return this.childCount;
@@ -810,7 +799,7 @@ export class Collections extends PartialPlexObject {
810
799
  }
811
800
  }
812
801
  export class FilterChoice extends PlexObject {
813
- static { this.TAG = 'Directory'; }
802
+ static TAG = 'Directory';
814
803
  _loadData(data) {
815
804
  this.key = data.key;
816
805
  this.title = data.title;
@@ -819,7 +808,7 @@ export class FilterChoice extends PlexObject {
819
808
  }
820
809
  }
821
810
  export class FilteringType extends PlexObject {
822
- static { this.TAG = 'Type'; }
811
+ static TAG = 'Type';
823
812
  _loadData(data) {
824
813
  this.active = data.active;
825
814
  this.fields = findItems(data, undefined, FilteringField);
@@ -834,7 +823,7 @@ export class FilteringType extends PlexObject {
834
823
  * Represents a single Filter object for a {@link FilteringType}
835
824
  */
836
825
  export class FilteringFilter extends PlexObject {
837
- static { this.TAG = 'Filter'; }
826
+ static TAG = 'Filter';
838
827
  _loadData(data) {
839
828
  this.filter = data.filter;
840
829
  this.filterType = data.filterType;
@@ -847,7 +836,7 @@ export class FilteringFilter extends PlexObject {
847
836
  * Represents a single Sort object for a {@link FilteringType}
848
837
  */
849
838
  export class FilteringSort extends PlexObject {
850
- static { this.TAG = 'Sort'; }
839
+ static TAG = 'Sort';
851
840
  _loadData(data) {
852
841
  this.active = data.active;
853
842
  this.activeDirection = data.activeDirection;
@@ -863,7 +852,7 @@ export class FilteringSort extends PlexObject {
863
852
  * Represents a single Field object for a {@link FilteringType}
864
853
  */
865
854
  export class FilteringField extends PlexObject {
866
- static { this.TAG = 'Field'; }
855
+ static TAG = 'Field';
867
856
  _loadData(data) {
868
857
  this.key = data.key;
869
858
  this.title = data.title;
@@ -875,7 +864,7 @@ export class FilteringField extends PlexObject {
875
864
  * Represents a single FieldType for library filtering.
876
865
  */
877
866
  export class FilteringFieldType extends PlexObject {
878
- static { this.TAG = 'FieldType'; }
867
+ static TAG = 'FieldType';
879
868
  _loadData(data) {
880
869
  this.type = data.type;
881
870
  this.operators = findItems(data, undefined, FilteringOperator);
@@ -885,7 +874,7 @@ export class FilteringFieldType extends PlexObject {
885
874
  * Represents a single FilterChoice for library filtering.
886
875
  */
887
876
  export class FilteringOperator extends PlexObject {
888
- static { this.TAG = 'Operator'; }
877
+ static TAG = 'Operator';
889
878
  _loadData(data) {
890
879
  this.key = data.key;
891
880
  this.type = data.type;