@ctrl/plex 3.9.1 → 3.10.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 +11 -5
- package/dist/src/audio.d.ts +2 -2
- package/dist/src/audio.js +4 -3
- package/dist/src/base/partialPlexObject.d.ts +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/library.d.ts +5 -5
- package/dist/src/library.js +0 -1
- package/dist/src/playlist.d.ts +25 -2
- package/dist/src/playlist.js +38 -1
- package/dist/src/search.types.d.ts +3 -3
- package/dist/src/server.d.ts +2 -2
- package/dist/src/server.js +7 -3
- package/dist/src/server.types.d.ts +5 -2
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -84,31 +84,37 @@ export PLEX_USERNAME=email
|
|
|
84
84
|
export PLEX_PASSWORD=password
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Setup test content (once)
|
|
88
88
|
|
|
89
89
|
```sh
|
|
90
|
-
|
|
90
|
+
pnpm run add-media
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Run tests
|
|
94
94
|
|
|
95
95
|
```sh
|
|
96
|
-
|
|
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
|
-
|
|
102
|
+
pnpm run test-cleanup
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
### Running tests locally
|
|
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/audio.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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,11 +1,11 @@
|
|
|
1
1
|
import { URLSearchParams } from 'url';
|
|
2
|
-
import {
|
|
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
|
|
8
|
+
export class Audio extends Playable {
|
|
9
9
|
/** Default metadata type for audio sync items. */
|
|
10
10
|
static { this.METADATA_TYPE = 'track'; }
|
|
11
11
|
/** Hardcoded list type for filtering. */
|
|
@@ -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;
|
|
@@ -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
|
}
|
|
@@ -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").
|
|
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.
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/src/library.d.ts
CHANGED
|
@@ -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 {
|
|
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<
|
|
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:
|
|
472
|
-
Metadata:
|
|
473
|
-
protected _loadData(data:
|
|
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.
|
package/dist/src/library.js
CHANGED
|
@@ -26,7 +26,6 @@ export class Library {
|
|
|
26
26
|
for (const elem of elems.MediaContainer.Directory) {
|
|
27
27
|
for (const cls of [MovieSection, ShowSection, MusicSection]) {
|
|
28
28
|
if (cls.TYPE === elem.type) {
|
|
29
|
-
// eslint-disable-next-line new-cap
|
|
30
29
|
const instance = new cls(this.server, elem, key);
|
|
31
30
|
sections.push(instance);
|
|
32
31
|
}
|
package/dist/src/playlist.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Playable } from './base/playable.js';
|
|
2
|
+
import { Album, Artist, Track } from './audio.js';
|
|
2
3
|
import type { Section, SectionType } from './library.js';
|
|
3
4
|
import type { PlaylistResponse } from './playlist.types.js';
|
|
4
5
|
import type { PlexServer } from './server.js';
|
|
@@ -29,10 +30,32 @@ interface CreateSmartPlaylistOptions {
|
|
|
29
30
|
filters?: Record<string, any>;
|
|
30
31
|
}
|
|
31
32
|
type CreatePlaylistOptions = CreateRegularPlaylistOptions | CreateSmartPlaylistOptions;
|
|
32
|
-
type PlaylistContent = Episode | Movie;
|
|
33
|
+
type PlaylistContent = Episode | Movie | Track | Album | Artist;
|
|
34
|
+
export interface UpdatePlaylistOptions {
|
|
35
|
+
/** New title for the playlist */
|
|
36
|
+
title?: string;
|
|
37
|
+
/** New summary/description for the playlist */
|
|
38
|
+
summary?: string;
|
|
39
|
+
}
|
|
33
40
|
export declare class Playlist extends Playable {
|
|
34
41
|
static TAG: string;
|
|
35
42
|
static create(server: PlexServer, title: string, options: CreatePlaylistOptions): Promise<Playlist>;
|
|
43
|
+
/**
|
|
44
|
+
* Update a playlist's metadata by ratingKey without fetching the full playlist first.
|
|
45
|
+
*
|
|
46
|
+
* @param server - The Plex server instance
|
|
47
|
+
* @param ratingKey - The playlist's ratingKey
|
|
48
|
+
* @param options - Fields to update (title and/or summary)
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* await Playlist.update(server, '12345', {
|
|
53
|
+
* title: 'My Updated Playlist',
|
|
54
|
+
* summary: 'A new description'
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
static update(server: PlexServer, ratingKey: string, options: UpdatePlaylistOptions): Promise<void>;
|
|
36
59
|
/** Create a smart playlist. */
|
|
37
60
|
private static _create;
|
|
38
61
|
TYPE: string;
|
|
@@ -61,7 +84,7 @@ export declare class Playlist extends Playable {
|
|
|
61
84
|
* @returns the item in the playlist that matches the specified title.
|
|
62
85
|
*/
|
|
63
86
|
item(title: string): Promise<PlaylistContent | null>;
|
|
64
|
-
items(): Promise<
|
|
87
|
+
items<T extends PlaylistContent>(): Promise<T[]>;
|
|
65
88
|
/** Add items to a playlist. */
|
|
66
89
|
addItems(items: PlaylistContent[]): Promise<void>;
|
|
67
90
|
/** Remove an item from a playlist. */
|
package/dist/src/playlist.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { URLSearchParams } from 'url';
|
|
2
2
|
import { Playable } from './base/playable.js';
|
|
3
|
+
import { Album, Artist, Track } from './audio.js';
|
|
3
4
|
import { fetchItems } from './baseFunctionality.js';
|
|
4
5
|
import { BadRequest, NotFound } from './exceptions.js';
|
|
5
6
|
import { Episode, Movie } from './video.js';
|
|
@@ -12,8 +13,14 @@ function contentClass(data) {
|
|
|
12
13
|
return Episode;
|
|
13
14
|
case 'movie':
|
|
14
15
|
return Movie;
|
|
16
|
+
case 'track':
|
|
17
|
+
return Track;
|
|
18
|
+
case 'album':
|
|
19
|
+
return Album;
|
|
20
|
+
case 'artist':
|
|
21
|
+
return Artist;
|
|
15
22
|
default:
|
|
16
|
-
throw new Error(
|
|
23
|
+
throw new Error(`Media type '${data.type}' not implemented`);
|
|
17
24
|
}
|
|
18
25
|
}
|
|
19
26
|
export class Playlist extends Playable {
|
|
@@ -31,6 +38,35 @@ export class Playlist extends Playable {
|
|
|
31
38
|
}
|
|
32
39
|
return this._create(server, title, options.items);
|
|
33
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Update a playlist's metadata by ratingKey without fetching the full playlist first.
|
|
43
|
+
*
|
|
44
|
+
* @param server - The Plex server instance
|
|
45
|
+
* @param ratingKey - The playlist's ratingKey
|
|
46
|
+
* @param options - Fields to update (title and/or summary)
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* await Playlist.update(server, '12345', {
|
|
51
|
+
* title: 'My Updated Playlist',
|
|
52
|
+
* summary: 'A new description'
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
static async update(server, ratingKey, options) {
|
|
57
|
+
if (!options.title && !options.summary) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const params = new URLSearchParams();
|
|
61
|
+
if (options.title) {
|
|
62
|
+
params.set('title', options.title);
|
|
63
|
+
}
|
|
64
|
+
if (options.summary) {
|
|
65
|
+
params.set('summary', options.summary);
|
|
66
|
+
}
|
|
67
|
+
const key = `/playlists/${ratingKey}?${params.toString()}`;
|
|
68
|
+
await server.query(key, 'put');
|
|
69
|
+
}
|
|
34
70
|
/** Create a smart playlist. */
|
|
35
71
|
// private static _createSmart(server: PlexServer, title: string, options: CreatePlaylistOptions) {}
|
|
36
72
|
static async _create(server, title, items) {
|
|
@@ -135,6 +171,7 @@ export class Playlist extends Playable {
|
|
|
135
171
|
if (!playlistItem) {
|
|
136
172
|
throw new NotFound(`Item with title "${item.title}" not found in the playlist`);
|
|
137
173
|
}
|
|
174
|
+
// playlistItemID is added dynamically by Plex API for playlist items
|
|
138
175
|
return playlistItem.playlistItemID;
|
|
139
176
|
}
|
|
140
177
|
}
|
|
@@ -6,7 +6,7 @@ export interface MatchSearchResult {
|
|
|
6
6
|
year: number;
|
|
7
7
|
lifespanEnded: boolean;
|
|
8
8
|
}
|
|
9
|
-
export interface
|
|
9
|
+
export interface SearchResultContainer {
|
|
10
10
|
title: string;
|
|
11
11
|
type: string;
|
|
12
12
|
hubIdentifier: string;
|
|
@@ -15,7 +15,7 @@ export interface SearchResult {
|
|
|
15
15
|
more: boolean;
|
|
16
16
|
style: string;
|
|
17
17
|
Directory?: Directory[];
|
|
18
|
-
Metadata?:
|
|
18
|
+
Metadata?: SearchResult[];
|
|
19
19
|
}
|
|
20
20
|
export interface Directory {
|
|
21
21
|
key: string;
|
|
@@ -34,7 +34,7 @@ export interface Directory {
|
|
|
34
34
|
count: number;
|
|
35
35
|
thumb?: string;
|
|
36
36
|
}
|
|
37
|
-
interface
|
|
37
|
+
interface SearchResult {
|
|
38
38
|
librarySectionTitle: string;
|
|
39
39
|
ratingKey: string;
|
|
40
40
|
key: string;
|
package/dist/src/server.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Playlist } from './playlist.js';
|
|
|
8
8
|
import { PlayQueue } from './playqueue.js';
|
|
9
9
|
import type { CreatePlayQueueOptions } from './playqueue.types.js';
|
|
10
10
|
import { Agent, SEARCHTYPES } from './search.js';
|
|
11
|
-
import type {
|
|
11
|
+
import type { HistoryResult } from './server.types.js';
|
|
12
12
|
import { Settings } from './settings.js';
|
|
13
13
|
/**
|
|
14
14
|
* This is the main entry point to interacting with a Plex server. It allows you to
|
|
@@ -190,7 +190,7 @@ export declare class PlexServer {
|
|
|
190
190
|
* @param accountId request history for a specific account ID.
|
|
191
191
|
* @param librarySectionId request history for a specific library section ID.
|
|
192
192
|
*/
|
|
193
|
-
history(maxresults?: number, mindate?: Date, ratingKey?: number | string, accountId?: number | string, librarySectionId?: number | string): Promise<
|
|
193
|
+
history(maxresults?: number, mindate?: Date, ratingKey?: number | string, accountId?: number | string, librarySectionId?: number | string): Promise<HistoryResult[]>;
|
|
194
194
|
settings(): Promise<Settings>;
|
|
195
195
|
/**
|
|
196
196
|
* Creates and returns a new PlayQueue.
|
package/dist/src/server.js
CHANGED
|
@@ -143,7 +143,7 @@ export class PlexServer {
|
|
|
143
143
|
args.librarySectionID = librarySectionId.toString();
|
|
144
144
|
}
|
|
145
145
|
if (mindate !== undefined) {
|
|
146
|
-
args['viewedAt>'] = mindate.getTime().toString();
|
|
146
|
+
args['viewedAt>'] = Math.floor(mindate.getTime() / 1000).toString();
|
|
147
147
|
}
|
|
148
148
|
args['X-Plex-Container-Start'] = '0';
|
|
149
149
|
args['X-Plex-Container-Size'] = Math.min(X_PLEX_CONTAINER_SIZE, maxresults).toString();
|
|
@@ -151,14 +151,18 @@ export class PlexServer {
|
|
|
151
151
|
let key = '/status/sessions/history/all?' + new URLSearchParams(args).toString();
|
|
152
152
|
let raw = await this.query(key);
|
|
153
153
|
const totalResults = raw.MediaContainer.totalSize;
|
|
154
|
-
|
|
154
|
+
// Filter out null/undefined items from the metadata
|
|
155
|
+
const validMetadata = raw.MediaContainer.Metadata?.filter(Boolean) ?? [];
|
|
156
|
+
results = results.concat(validMetadata);
|
|
155
157
|
while (results.length <= totalResults &&
|
|
156
158
|
X_PLEX_CONTAINER_SIZE === raw.MediaContainer.size &&
|
|
157
159
|
maxresults > results.length) {
|
|
158
160
|
args['X-Plex-Container-Start'] = (Number(args['X-Plex-Container-Start']) + Number(args['X-Plex-Container-Size'])).toString();
|
|
159
161
|
key = '/status/sessions/history/all?' + new URLSearchParams(args).toString();
|
|
160
162
|
raw = await this.query(key);
|
|
161
|
-
|
|
163
|
+
// Filter out null/undefined items from the metadata
|
|
164
|
+
const validMetadata = raw.MediaContainer.Metadata?.filter(item => item != null) ?? [];
|
|
165
|
+
results = results.concat(validMetadata);
|
|
162
166
|
}
|
|
163
167
|
return results;
|
|
164
168
|
}
|
|
@@ -65,10 +65,12 @@ export interface HistoryMediaContainer {
|
|
|
65
65
|
size: number;
|
|
66
66
|
totalSize: number;
|
|
67
67
|
offset: number;
|
|
68
|
-
Metadata
|
|
68
|
+
Metadata?: Array<HistoryResult | null | undefined>;
|
|
69
69
|
}
|
|
70
|
-
export interface
|
|
70
|
+
export interface HistoryResult {
|
|
71
71
|
key: string;
|
|
72
|
+
ratingKey: string;
|
|
73
|
+
historyKey: string;
|
|
72
74
|
parentKey?: string;
|
|
73
75
|
grandparentKey?: string;
|
|
74
76
|
title: string;
|
|
@@ -84,6 +86,7 @@ export interface HistoryMetadatum {
|
|
|
84
86
|
viewedAt: number;
|
|
85
87
|
accountID: number;
|
|
86
88
|
deviceID: number;
|
|
89
|
+
librarySectionID: string;
|
|
87
90
|
}
|
|
88
91
|
export interface PlaylistMediaContainer {
|
|
89
92
|
size: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctrl/plex",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "plex api client in typescript using ofetch",
|
|
5
5
|
"author": "Scott Cooper <scttcper@gmail.com>",
|
|
6
6
|
"publishConfig": {
|
|
@@ -41,25 +41,25 @@
|
|
|
41
41
|
"xml2js": "^0.6.2"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@ctrl/oxlint-config": "1.2.
|
|
44
|
+
"@ctrl/oxlint-config": "1.2.8",
|
|
45
45
|
"@ctrl/video-filename-parser": "5.4.1",
|
|
46
46
|
"@sindresorhus/tsconfig": "8.0.1",
|
|
47
47
|
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
|
48
|
-
"@types/node": "24.
|
|
48
|
+
"@types/node": "24.7.2",
|
|
49
49
|
"@types/ws": "8.18.1",
|
|
50
50
|
"@types/xml2js": "0.4.14",
|
|
51
51
|
"@types/yargs": "17.0.33",
|
|
52
52
|
"@vitest/coverage-v8": "3.2.4",
|
|
53
53
|
"execa": "9.6.0",
|
|
54
|
-
"globby": "
|
|
54
|
+
"globby": "15.0.0",
|
|
55
55
|
"make-dir": "5.1.0",
|
|
56
56
|
"ora": "9.0.0",
|
|
57
|
-
"oxlint": "1.
|
|
58
|
-
"p-retry": "7.
|
|
57
|
+
"oxlint": "1.22.0",
|
|
58
|
+
"p-retry": "7.1.0",
|
|
59
59
|
"prettier": "3.6.2",
|
|
60
60
|
"tsx": "4.20.6",
|
|
61
|
-
"typedoc": "0.28.
|
|
62
|
-
"typescript": "5.9.
|
|
61
|
+
"typedoc": "0.28.14",
|
|
62
|
+
"typescript": "5.9.3",
|
|
63
63
|
"vitest": "3.2.4",
|
|
64
64
|
"yargs": "18.0.0"
|
|
65
65
|
},
|
|
@@ -91,5 +91,5 @@
|
|
|
91
91
|
"engines": {
|
|
92
92
|
"node": ">=18"
|
|
93
93
|
},
|
|
94
|
-
"packageManager": "pnpm@10.
|
|
94
|
+
"packageManager": "pnpm@10.18.2"
|
|
95
95
|
}
|