@ctrl/plex 1.5.2 → 2.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.
- package/README.md +3 -2
- package/dist/src/alert.d.ts +12 -0
- package/dist/src/alert.js +29 -0
- package/dist/src/alert.types.d.ts +59 -0
- package/dist/src/alert.types.js +1 -0
- package/dist/{base → src/base}/partialPlexObject.d.ts +18 -12
- package/dist/{base → src/base}/partialPlexObject.js +32 -23
- package/dist/{base → src/base}/playable.d.ts +2 -2
- package/dist/src/base/playable.js +8 -0
- package/dist/{base → src/base}/plexObject.d.ts +8 -1
- package/dist/{base → src/base}/plexObject.js +21 -18
- package/dist/{baseFunctionality.d.ts → src/baseFunctionality.d.ts} +17 -1
- package/dist/{baseFunctionality.js → src/baseFunctionality.js} +7 -15
- package/dist/{client.d.ts → src/client.d.ts} +2 -2
- package/dist/{client.js → src/client.js} +12 -20
- package/dist/src/client.types.js +1 -0
- package/dist/src/config.js +35 -0
- package/dist/src/exceptions.d.ts +20 -0
- package/dist/src/exceptions.js +40 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +11 -0
- package/dist/{library.d.ts → src/library.d.ts} +207 -21
- package/dist/{library.js → src/library.js} +349 -133
- package/dist/{library.types.d.ts → src/library.types.d.ts} +71 -1
- package/dist/src/library.types.js +1 -0
- package/dist/{media.d.ts → src/media.d.ts} +16 -4
- package/dist/{media.js → src/media.js} +42 -49
- package/dist/src/media.types.d.ts +7 -0
- package/dist/src/media.types.js +1 -0
- package/dist/{myplex.d.ts → src/myplex.d.ts} +16 -6
- package/dist/{myplex.js → src/myplex.js} +71 -57
- package/dist/src/myplex.types.js +10 -0
- package/dist/src/playlist.d.ts +75 -0
- package/dist/src/playlist.js +142 -0
- package/dist/src/playlist.types.d.ts +17 -0
- package/dist/src/playlist.types.js +1 -0
- package/dist/{search.d.ts → src/search.d.ts} +5 -4
- package/dist/{search.js → src/search.js} +16 -20
- package/dist/src/search.types.js +1 -0
- package/dist/{server.d.ts → src/server.d.ts} +22 -10
- package/dist/{server.js → src/server.js} +65 -50
- package/dist/src/server.types.js +1 -0
- package/dist/src/settings.d.ts +79 -0
- package/dist/src/settings.js +160 -0
- package/dist/{util.d.ts → src/util.d.ts} +2 -1
- package/dist/{util.js → src/util.js} +8 -12
- package/dist/{video.d.ts → src/video.d.ts} +39 -61
- package/dist/{video.js → src/video.js} +110 -93
- package/dist/{video.types.d.ts → src/video.types.d.ts} +1 -1
- package/dist/src/video.types.js +6 -0
- package/package.json +46 -44
- package/dist/base/playable.js +0 -12
- package/dist/client.types.js +0 -2
- package/dist/config.js +0 -41
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -23
- package/dist/library.types.js +0 -2
- package/dist/myplex.types.js +0 -13
- package/dist/playlist.d.ts +0 -7
- package/dist/playlist.js +0 -19
- package/dist/search.types.js +0 -2
- package/dist/server.types.js +0 -2
- package/dist/video.types.js +0 -9
- /package/dist/{client.types.d.ts → src/client.types.d.ts} +0 -0
- /package/dist/{config.d.ts → src/config.d.ts} +0 -0
- /package/dist/{myplex.types.d.ts → src/myplex.types.d.ts} +0 -0
- /package/dist/{search.types.d.ts → src/search.types.d.ts} +0 -0
- /package/dist/{server.types.d.ts → src/server.types.d.ts} +0 -0
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const url_1 = require("url");
|
|
9
|
-
const p_any_1 = __importDefault(require("p-any"));
|
|
10
|
-
const config_1 = require("./config");
|
|
11
|
-
const server_1 = require("./server");
|
|
12
|
-
const plexObject_1 = require("./base/plexObject");
|
|
13
|
-
const xml2js_1 = require("xml2js");
|
|
1
|
+
import { URL, URLSearchParams } from 'url';
|
|
2
|
+
import got from 'got';
|
|
3
|
+
import pAny from 'p-any';
|
|
4
|
+
import { parseStringPromise } from 'xml2js';
|
|
5
|
+
import { PlexObject } from './base/plexObject.js';
|
|
6
|
+
import { BASE_HEADERS, TIMEOUT } from './config.js';
|
|
7
|
+
import { PlexServer } from './server.js';
|
|
14
8
|
/**
|
|
15
9
|
* MyPlex account and profile information. This object represents the data found Account on
|
|
16
10
|
* the myplex.tv servers at the url https://plex.tv/users/account. You may create this object
|
|
@@ -18,7 +12,8 @@ const xml2js_1 = require("xml2js");
|
|
|
18
12
|
* method provided at :class:`~plexapi.server.PlexServer.myPlexAccount()` which will create
|
|
19
13
|
* and return this object.
|
|
20
14
|
*/
|
|
21
|
-
class MyPlexAccount {
|
|
15
|
+
export class MyPlexAccount {
|
|
16
|
+
static { this.key = 'https://plex.tv/api/v2/user'; }
|
|
22
17
|
/**
|
|
23
18
|
*
|
|
24
19
|
* @param username Your MyPlex username
|
|
@@ -28,7 +23,7 @@ class MyPlexAccount {
|
|
|
28
23
|
* @param timeout timeout in seconds on initial connect to myplex
|
|
29
24
|
* @param server not often available
|
|
30
25
|
*/
|
|
31
|
-
constructor(baseUrl = null, username, password, token, timeout =
|
|
26
|
+
constructor(baseUrl = null, username, password, token, timeout = TIMEOUT, server) {
|
|
32
27
|
this.baseUrl = baseUrl;
|
|
33
28
|
this.username = username;
|
|
34
29
|
this.password = password;
|
|
@@ -94,6 +89,18 @@ class MyPlexAccount {
|
|
|
94
89
|
const data = await this.query(MyPlexResource.key);
|
|
95
90
|
return data.map(device => new MyPlexResource(this, device, this.baseUrl));
|
|
96
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* @param name Name to match against.
|
|
94
|
+
* @param clientId clientIdentifier to match against.
|
|
95
|
+
*/
|
|
96
|
+
async device(name = '', clientId) {
|
|
97
|
+
const devices = await this.devices();
|
|
98
|
+
const device = devices.find(device => device.name?.toLowerCase() === name.toLowerCase() || device.clientIdentifier === clientId);
|
|
99
|
+
if (!device) {
|
|
100
|
+
throw new Error('Unable to find device');
|
|
101
|
+
}
|
|
102
|
+
return device;
|
|
103
|
+
}
|
|
97
104
|
/**
|
|
98
105
|
* Returns a list of all :class:`~plexapi.myplex.MyPlexDevice` objects connected to the server.
|
|
99
106
|
*/
|
|
@@ -117,18 +124,18 @@ class MyPlexAccount {
|
|
|
117
124
|
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
|
118
125
|
requestHeaders.Authorization = `Basic ${credentials}`;
|
|
119
126
|
}
|
|
120
|
-
const promise =
|
|
127
|
+
const promise = got({
|
|
121
128
|
method,
|
|
122
|
-
url: new
|
|
129
|
+
url: new URL(url),
|
|
123
130
|
headers: requestHeaders,
|
|
124
|
-
timeout:
|
|
125
|
-
username,
|
|
126
|
-
password,
|
|
127
|
-
retry: 0,
|
|
131
|
+
timeout: { request: timeout ?? TIMEOUT },
|
|
132
|
+
...(username ? { username } : {}),
|
|
133
|
+
...(password ? { password } : {}),
|
|
134
|
+
retry: { limit: 0 },
|
|
128
135
|
});
|
|
129
136
|
if (url.includes('xml')) {
|
|
130
137
|
const res = await promise;
|
|
131
|
-
const xml = await
|
|
138
|
+
const xml = await parseStringPromise(res.body);
|
|
132
139
|
return xml;
|
|
133
140
|
}
|
|
134
141
|
const res = await promise.json();
|
|
@@ -141,29 +148,29 @@ class MyPlexAccount {
|
|
|
141
148
|
*/
|
|
142
149
|
async claimToken() {
|
|
143
150
|
const url = 'https://plex.tv/api/claim/token.json';
|
|
144
|
-
const response = await this.query(url, 'get', undefined,
|
|
151
|
+
const response = await this.query(url, 'get', undefined, TIMEOUT);
|
|
145
152
|
return response.token;
|
|
146
153
|
}
|
|
147
154
|
/**
|
|
148
155
|
* @param token pass token from claimToken
|
|
149
156
|
*/
|
|
150
157
|
async claimServer(token) {
|
|
151
|
-
const params = new
|
|
158
|
+
const params = new URLSearchParams({
|
|
152
159
|
token,
|
|
153
|
-
...
|
|
160
|
+
...BASE_HEADERS,
|
|
154
161
|
});
|
|
155
162
|
const url = `${this.baseUrl}/myplex/claim?${params.toString()}`;
|
|
156
|
-
return
|
|
163
|
+
return got({
|
|
157
164
|
method: 'POST',
|
|
158
165
|
url,
|
|
159
|
-
timeout:
|
|
166
|
+
timeout: { request: TIMEOUT },
|
|
160
167
|
headers: this._headers(),
|
|
161
|
-
retry: 0,
|
|
168
|
+
retry: { limit: 0 },
|
|
162
169
|
});
|
|
163
170
|
}
|
|
164
171
|
_headers() {
|
|
165
172
|
const headers = {
|
|
166
|
-
...
|
|
173
|
+
...BASE_HEADERS,
|
|
167
174
|
'Content-type': 'application/json',
|
|
168
175
|
};
|
|
169
176
|
if (this.token) {
|
|
@@ -176,7 +183,6 @@ class MyPlexAccount {
|
|
|
176
183
|
return data.user;
|
|
177
184
|
}
|
|
178
185
|
_loadData(user) {
|
|
179
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
180
186
|
// Attempt to prevent token from being logged accidentally
|
|
181
187
|
Object.defineProperty(this, 'token', {
|
|
182
188
|
enumerable: false,
|
|
@@ -194,21 +200,19 @@ class MyPlexAccount {
|
|
|
194
200
|
this.username = user.username;
|
|
195
201
|
this.title = user.title;
|
|
196
202
|
this.locale = user.locale;
|
|
197
|
-
this.mailingListStatus =
|
|
203
|
+
this.mailingListStatus = user.mailingListStatus ?? 'inactive';
|
|
198
204
|
this.mailingListActive = user.mailingListActive;
|
|
199
205
|
this.queueEmail = user.queueEmail;
|
|
200
206
|
this.thumb = user.thumb;
|
|
201
207
|
this.scrobbleTypes = user.scrobbleTypes;
|
|
202
208
|
this.restricted = user.restricted;
|
|
203
|
-
this.subscriptionActive =
|
|
204
|
-
this.subscriptionStatus =
|
|
205
|
-
this.subscriptionPlan =
|
|
206
|
-
this.subscriptionFeatures =
|
|
209
|
+
this.subscriptionActive = user.subscription?.active ?? null;
|
|
210
|
+
this.subscriptionStatus = user.subscription?.status?.toLowerCase() ?? 'inactive';
|
|
211
|
+
this.subscriptionPlan = user.subscription?.plan ?? null;
|
|
212
|
+
this.subscriptionFeatures = user.subscription?.features ?? [];
|
|
207
213
|
this.entitlements = user.entitlements;
|
|
208
214
|
}
|
|
209
215
|
}
|
|
210
|
-
exports.MyPlexAccount = MyPlexAccount;
|
|
211
|
-
MyPlexAccount.key = 'https://plex.tv/api/v2/user';
|
|
212
216
|
/**
|
|
213
217
|
* Connects to the specified cls with url and token
|
|
214
218
|
*/
|
|
@@ -221,7 +225,8 @@ async function connect(cls, url, token, timeout) {
|
|
|
221
225
|
* This object represents resources connected to your Plex server that can provide
|
|
222
226
|
* content such as Plex Media Servers, iPhone or Android clients, etc.
|
|
223
227
|
*/
|
|
224
|
-
class MyPlexResource {
|
|
228
|
+
export class MyPlexResource {
|
|
229
|
+
static { this.key = 'https://plex.tv/api/v2/resources?includeHttps=1&includeRelay=1'; }
|
|
225
230
|
constructor(account, data, baseUrl = null) {
|
|
226
231
|
this.account = account;
|
|
227
232
|
this.baseUrl = baseUrl;
|
|
@@ -255,19 +260,18 @@ class MyPlexResource {
|
|
|
255
260
|
// TODO: switch between PlexServer and PlexClient
|
|
256
261
|
// Try connecting to all known resource connections in parellel, but
|
|
257
262
|
// only return the first server (in order) that provides a response.
|
|
258
|
-
const promises = attemptUrls.map(async (url) => connect((...args) => new
|
|
259
|
-
const result = await
|
|
263
|
+
const promises = attemptUrls.map(async (url) => connect((...args) => new PlexServer(...args), url, this.accessToken, timeout));
|
|
264
|
+
const result = await pAny(promises);
|
|
260
265
|
return result;
|
|
261
266
|
}
|
|
262
267
|
/** Remove this device from your account */
|
|
263
268
|
async delete() {
|
|
264
|
-
const key = `https://plex.tv/api/servers/${this.clientIdentifier}?X-Plex-Client-Identifier=${
|
|
265
|
-
await
|
|
269
|
+
const key = `https://plex.tv/api/servers/${this.clientIdentifier}?X-Plex-Client-Identifier=${BASE_HEADERS['X-Plex-Client-Identifier']}&X-Plex-Token=${this.accessToken}`;
|
|
270
|
+
await got.delete(key, { retry: { limit: 0 } });
|
|
266
271
|
}
|
|
267
272
|
_loadData(data) {
|
|
268
|
-
var _a, _b, _c;
|
|
269
273
|
this.name = data.name;
|
|
270
|
-
this.accessToken =
|
|
274
|
+
this.accessToken = data.accessToken ?? '';
|
|
271
275
|
this.owned = data.owned;
|
|
272
276
|
this.clientIdentifier = data.clientIdentifier;
|
|
273
277
|
this.createdAt = new Date(data.createdAt);
|
|
@@ -282,12 +286,10 @@ class MyPlexResource {
|
|
|
282
286
|
this.product = data.product;
|
|
283
287
|
this.productVersion = data.productVersion;
|
|
284
288
|
this.connections =
|
|
285
|
-
|
|
289
|
+
data.connections?.map(connection => new ResourceConnection(connection)) ?? [];
|
|
286
290
|
}
|
|
287
291
|
}
|
|
288
|
-
|
|
289
|
-
MyPlexResource.key = 'https://plex.tv/api/v2/resources?includeHttps=1&includeRelay=1';
|
|
290
|
-
class ResourceConnection {
|
|
292
|
+
export class ResourceConnection {
|
|
291
293
|
constructor(data) {
|
|
292
294
|
this.TAG = 'Connection';
|
|
293
295
|
this._loadData(data);
|
|
@@ -301,16 +303,31 @@ class ResourceConnection {
|
|
|
301
303
|
this.httpuri = `http://${data.address}:${data.port}`;
|
|
302
304
|
}
|
|
303
305
|
}
|
|
304
|
-
exports.ResourceConnection = ResourceConnection;
|
|
305
306
|
/**
|
|
306
307
|
* This object represents resources connected to your Plex server that provide
|
|
307
308
|
* playback ability from your Plex Server, iPhone or Android clients, Plex Web,
|
|
308
309
|
* this API, etc. The raw xml for the data presented here can be found at:
|
|
309
310
|
* https://plex.tv/devices.xml
|
|
310
311
|
*/
|
|
311
|
-
class MyPlexDevice extends
|
|
312
|
+
export class MyPlexDevice extends PlexObject {
|
|
313
|
+
static { this.TAG = 'Device'; }
|
|
314
|
+
static { this.key = 'https://plex.tv/devices.xml'; }
|
|
315
|
+
async connect() {
|
|
316
|
+
// TODO: switch between PlexServer and PlexClient
|
|
317
|
+
// Try connecting to all known resource connections in parellel, but
|
|
318
|
+
// only return the first server (in order) that provides a response.
|
|
319
|
+
const promises = (this.connections ?? []).map(async (url) => connect((...args) => new PlexServer(...args), url, this.token));
|
|
320
|
+
const result = await pAny(promises);
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Remove this device from your account
|
|
325
|
+
*/
|
|
326
|
+
async delete() {
|
|
327
|
+
const key = `https://plex.tv/devices/${this.id}.xml`;
|
|
328
|
+
await this.server.query(key, 'delete');
|
|
329
|
+
}
|
|
312
330
|
_loadData(data) {
|
|
313
|
-
var _a;
|
|
314
331
|
this.name = data.$.name;
|
|
315
332
|
this.publicAddress = data.$.publicAddress;
|
|
316
333
|
this.product = data.$.product;
|
|
@@ -332,11 +349,8 @@ class MyPlexDevice extends plexObject_1.PlexObject {
|
|
|
332
349
|
});
|
|
333
350
|
this.screenResolution = data.$.screenResolution;
|
|
334
351
|
this.screenDensity = data.$.screenDensity;
|
|
335
|
-
this.createdAt = new Date(data.$.createdAt);
|
|
336
|
-
this.lastSeenAt = new Date(data.$.lastSeenAt);
|
|
337
|
-
this.connections =
|
|
352
|
+
this.createdAt = new Date(parseInt(data.$.createdAt, 10));
|
|
353
|
+
this.lastSeenAt = new Date(parseInt(data.$.lastSeenAt, 10));
|
|
354
|
+
this.connections = data.Connection?.map(connection => connection.$.uri);
|
|
338
355
|
}
|
|
339
356
|
}
|
|
340
|
-
exports.MyPlexDevice = MyPlexDevice;
|
|
341
|
-
MyPlexDevice.TAG = 'Device';
|
|
342
|
-
MyPlexDevice.key = 'https://plex.tv/devices.xml';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export var Status;
|
|
2
|
+
(function (Status) {
|
|
3
|
+
Status["Online"] = "online";
|
|
4
|
+
Status["Offline"] = "offline";
|
|
5
|
+
})(Status || (Status = {}));
|
|
6
|
+
export var Protocol;
|
|
7
|
+
(function (Protocol) {
|
|
8
|
+
Protocol["HTTP"] = "http";
|
|
9
|
+
Protocol["HTTPS"] = "https";
|
|
10
|
+
})(Protocol || (Protocol = {}));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Playable } from './base/playable.js';
|
|
2
|
+
import type { Section } from './library.js';
|
|
3
|
+
import { PlaylistResponse } from './playlist.types.js';
|
|
4
|
+
import type { PlexServer } from './server.js';
|
|
5
|
+
import { Episode, Movie, VideoType } from './video.js';
|
|
6
|
+
interface CreateRegularPlaylistOptions {
|
|
7
|
+
/** True to create a smart playlist */
|
|
8
|
+
smart?: false;
|
|
9
|
+
/** Regular playlists only */
|
|
10
|
+
items?: VideoType[];
|
|
11
|
+
}
|
|
12
|
+
interface CreateSmartPlaylistOptions {
|
|
13
|
+
/** True to create a smart playlist */
|
|
14
|
+
smart: true;
|
|
15
|
+
/** Smart playlists only, the library section to create the playlist in. */
|
|
16
|
+
section?: Section;
|
|
17
|
+
/** Smart playlists only, limit the number of items in the playlist. */
|
|
18
|
+
limit?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Smart playlists only, a string of comma separated sort fields
|
|
21
|
+
* or a list of sort fields in the format ``column:dir``.
|
|
22
|
+
* See {@link Section.search} for more info.
|
|
23
|
+
*/
|
|
24
|
+
sort?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Smart playlists only, a dictionary of advanced filters.
|
|
27
|
+
* See {@link Section.search} for more info.
|
|
28
|
+
*/
|
|
29
|
+
filters?: Record<string, any>;
|
|
30
|
+
}
|
|
31
|
+
type CreatePlaylistOptions = CreateRegularPlaylistOptions | CreateSmartPlaylistOptions;
|
|
32
|
+
type PlaylistContent = Episode | Movie;
|
|
33
|
+
export declare class Playlist extends Playable {
|
|
34
|
+
static TAG: string;
|
|
35
|
+
static create(server: PlexServer, title: string, options: CreatePlaylistOptions): Promise<Playlist>;
|
|
36
|
+
/** Create a smart playlist. */
|
|
37
|
+
private static _create;
|
|
38
|
+
TYPE: string;
|
|
39
|
+
addedAt: Date;
|
|
40
|
+
updatedAt: Date;
|
|
41
|
+
composite: string;
|
|
42
|
+
guid: string;
|
|
43
|
+
leafCount: number;
|
|
44
|
+
playlistType: string;
|
|
45
|
+
smart: boolean;
|
|
46
|
+
summary: string;
|
|
47
|
+
allowSync?: boolean;
|
|
48
|
+
duration?: number;
|
|
49
|
+
durationInSeconds?: number;
|
|
50
|
+
/** Cache of playlist items */
|
|
51
|
+
private _items;
|
|
52
|
+
_edit(args: {
|
|
53
|
+
title?: string;
|
|
54
|
+
summary?: string;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
edit(changeObj: {
|
|
57
|
+
title?: string;
|
|
58
|
+
summary?: string;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* @returns the item in the playlist that matches the specified title.
|
|
62
|
+
*/
|
|
63
|
+
item(title: string): Promise<PlaylistContent | null>;
|
|
64
|
+
items(): Promise<PlaylistContent[]>;
|
|
65
|
+
/** Add items to a playlist. */
|
|
66
|
+
addItems(items: PlaylistContent[]): Promise<void>;
|
|
67
|
+
/** Remove an item from a playlist. */
|
|
68
|
+
removeItems(items: PlaylistContent[]): Promise<void>;
|
|
69
|
+
/** Delete the playlist. */
|
|
70
|
+
delete(): Promise<void>;
|
|
71
|
+
protected _loadData(data: PlaylistResponse): void;
|
|
72
|
+
protected _loadFullData(data: any): void;
|
|
73
|
+
private _getPlaylistItemID;
|
|
74
|
+
}
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { URLSearchParams } from 'url';
|
|
2
|
+
import { Playable } from './base/playable.js';
|
|
3
|
+
import { fetchItems } from './baseFunctionality.js';
|
|
4
|
+
import { BadRequest, NotFound } from './exceptions.js';
|
|
5
|
+
import { Episode, Movie } from './video.js';
|
|
6
|
+
/**
|
|
7
|
+
* Map media types to their respective class
|
|
8
|
+
*/
|
|
9
|
+
function contentClass(data) {
|
|
10
|
+
switch (data.type) {
|
|
11
|
+
case 'episode':
|
|
12
|
+
return Episode;
|
|
13
|
+
case 'movie':
|
|
14
|
+
return Movie;
|
|
15
|
+
default:
|
|
16
|
+
throw new Error('Media type not implemented');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class Playlist extends Playable {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
this.TYPE = 'playlist';
|
|
23
|
+
/** Cache of playlist items */
|
|
24
|
+
this._items = null;
|
|
25
|
+
}
|
|
26
|
+
static { this.TAG = 'Playlist'; }
|
|
27
|
+
static async create(server, title, options) {
|
|
28
|
+
if (options.smart) {
|
|
29
|
+
throw new Error('not yet supported');
|
|
30
|
+
// return this._createSmart(server, title, options);
|
|
31
|
+
}
|
|
32
|
+
return this._create(server, title, options.items);
|
|
33
|
+
}
|
|
34
|
+
/** Create a smart playlist. */
|
|
35
|
+
// private static _createSmart(server: PlexServer, title: string, options: CreatePlaylistOptions) {}
|
|
36
|
+
static async _create(server, title, items) {
|
|
37
|
+
if (!items || items.length === 0) {
|
|
38
|
+
throw new BadRequest('Must include items to add when creating new playlist.');
|
|
39
|
+
}
|
|
40
|
+
const { listType } = items[0];
|
|
41
|
+
const ratingKeys = items ? items.map(x => x.ratingKey) : [];
|
|
42
|
+
const uri = `${server._uriRoot()}/library/metadata/${ratingKeys.join(',')}`;
|
|
43
|
+
const params = new URLSearchParams({
|
|
44
|
+
uri,
|
|
45
|
+
type: listType,
|
|
46
|
+
title,
|
|
47
|
+
smart: '0',
|
|
48
|
+
});
|
|
49
|
+
const key = `/playlists?${params.toString()}`;
|
|
50
|
+
const data = await server.query(key, 'post');
|
|
51
|
+
return new Playlist(server, data.MediaContainer.Metadata[0], key);
|
|
52
|
+
}
|
|
53
|
+
async _edit(args) {
|
|
54
|
+
const searchparams = new URLSearchParams(args);
|
|
55
|
+
const key = `${this.key}?${searchparams.toString()}`;
|
|
56
|
+
await this.server.query(key, 'put');
|
|
57
|
+
}
|
|
58
|
+
async edit(changeObj) {
|
|
59
|
+
await this._edit(changeObj);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @returns the item in the playlist that matches the specified title.
|
|
63
|
+
*/
|
|
64
|
+
async item(title) {
|
|
65
|
+
const items = await this.items();
|
|
66
|
+
const matched = items.find(item => item.title.toLowerCase() === title.toLowerCase());
|
|
67
|
+
return matched ?? null;
|
|
68
|
+
}
|
|
69
|
+
async items() {
|
|
70
|
+
if (this._items === null) {
|
|
71
|
+
const key = `/playlists/${this.ratingKey}/items`;
|
|
72
|
+
const items = await fetchItems(this.server, key);
|
|
73
|
+
this._items = items.map(data => {
|
|
74
|
+
const Cls = contentClass(data);
|
|
75
|
+
return new Cls(this.server, data, key, this);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return this._items;
|
|
79
|
+
}
|
|
80
|
+
/** Add items to a playlist. */
|
|
81
|
+
async addItems(items) {
|
|
82
|
+
if (this.smart) {
|
|
83
|
+
throw new BadRequest('Cannot add items to a smart playlist.');
|
|
84
|
+
}
|
|
85
|
+
const isInvalidType = items.some(x => x.listType !== this.playlistType);
|
|
86
|
+
if (isInvalidType) {
|
|
87
|
+
throw new BadRequest('Can not mix media types when building a playlist');
|
|
88
|
+
}
|
|
89
|
+
const ratingKeys = items.map(x => x.ratingKey);
|
|
90
|
+
const params = new URLSearchParams({
|
|
91
|
+
uri: `${this.server._uriRoot()}/library/metadata/${ratingKeys.join(',')}`,
|
|
92
|
+
});
|
|
93
|
+
const key = `${this.key}/items?${params.toString()}`;
|
|
94
|
+
await this.server.query(key, 'put');
|
|
95
|
+
}
|
|
96
|
+
/** Remove an item from a playlist. */
|
|
97
|
+
async removeItems(items) {
|
|
98
|
+
if (this.smart) {
|
|
99
|
+
throw new BadRequest('Cannot remove items to a smart playlist.');
|
|
100
|
+
}
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
// eslint-disable-next-line no-await-in-loop
|
|
103
|
+
const playlistItemId = await this._getPlaylistItemID(item);
|
|
104
|
+
const key = `${this.key}/items/${playlistItemId}`;
|
|
105
|
+
// eslint-disable-next-line no-await-in-loop
|
|
106
|
+
await this.server.query(key, 'delete');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Delete the playlist. */
|
|
110
|
+
async delete() {
|
|
111
|
+
await this.server.query(this.key, 'delete');
|
|
112
|
+
}
|
|
113
|
+
_loadData(data) {
|
|
114
|
+
this.key = data.key.replace('/items', '');
|
|
115
|
+
this.ratingKey = data.ratingKey;
|
|
116
|
+
this.title = data.title;
|
|
117
|
+
this.type = data.type;
|
|
118
|
+
this.addedAt = new Date(data.addedAt);
|
|
119
|
+
this.updatedAt = new Date(data.updatedAt);
|
|
120
|
+
this.composite = data.composite;
|
|
121
|
+
this.guid = data.guid;
|
|
122
|
+
this.playlistType = data.playlistType;
|
|
123
|
+
this.summary = data.summary;
|
|
124
|
+
this.smart = data.smart;
|
|
125
|
+
this.leafCount = data.leafCount;
|
|
126
|
+
// TODO: verify these. Possibly audio playlist related
|
|
127
|
+
this.allowSync = data.allowSync;
|
|
128
|
+
this.duration = data.duration;
|
|
129
|
+
this.durationInSeconds = data.durationInSeconds;
|
|
130
|
+
}
|
|
131
|
+
_loadFullData(data) {
|
|
132
|
+
this._loadData(data.Metadata[0]);
|
|
133
|
+
}
|
|
134
|
+
async _getPlaylistItemID(item) {
|
|
135
|
+
const items = await this.items();
|
|
136
|
+
const playlistItem = items.find(i => i.ratingKey === item.ratingKey);
|
|
137
|
+
if (!playlistItem) {
|
|
138
|
+
throw new NotFound(`Item with title "${item.title}" not found in the playlist`);
|
|
139
|
+
}
|
|
140
|
+
return playlistItem.playlistItemID;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PlaylistResponse {
|
|
2
|
+
ratingKey: string;
|
|
3
|
+
key: string;
|
|
4
|
+
guid: string;
|
|
5
|
+
type: string;
|
|
6
|
+
title: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
smart: boolean;
|
|
9
|
+
playlistType: string;
|
|
10
|
+
composite: string;
|
|
11
|
+
leafCount: number;
|
|
12
|
+
addedAt: number;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
allowSync?: boolean;
|
|
15
|
+
duration?: number;
|
|
16
|
+
durationInSeconds?: number;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ValueOf } from 'type-fest';
|
|
2
|
-
import { PlexObject } from './base/plexObject';
|
|
3
|
-
import { MatchSearchResult } from './search.types';
|
|
2
|
+
import { PlexObject } from './base/plexObject.js';
|
|
3
|
+
import { MatchSearchResult } from './search.types.js';
|
|
4
4
|
export declare class SearchResult extends PlexObject {
|
|
5
5
|
static TAG: string;
|
|
6
6
|
guid: string;
|
|
@@ -41,12 +41,13 @@ export declare const SEARCHTYPES: {
|
|
|
41
41
|
readonly playlist: 15;
|
|
42
42
|
readonly playlistFolder: 16;
|
|
43
43
|
readonly collection: 18;
|
|
44
|
+
readonly optimizedVersion: 42;
|
|
44
45
|
readonly userPlaylistItem: 1001;
|
|
45
46
|
};
|
|
46
|
-
|
|
47
|
+
type SearchTypesValues = ValueOf<typeof SEARCHTYPES>;
|
|
47
48
|
/**
|
|
48
49
|
* Returns the integer value of the library string type.
|
|
49
50
|
* @param libtype to lookup (movie, show, season, episode, artist, album, track, collection)
|
|
50
51
|
*/
|
|
51
|
-
export declare function searchType(libtype
|
|
52
|
+
export declare function searchType(libtype?: string | number | keyof typeof SEARCHTYPES | SearchTypesValues): SearchTypesValues;
|
|
52
53
|
export {};
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const util_1 = require("./util");
|
|
6
|
-
class SearchResult extends plexObject_1.PlexObject {
|
|
1
|
+
import { PlexObject } from './base/plexObject.js';
|
|
2
|
+
import { rsplit } from './util.js';
|
|
3
|
+
export class SearchResult extends PlexObject {
|
|
4
|
+
static { this.TAG = 'SearchResult'; }
|
|
7
5
|
_loadData(data) {
|
|
8
6
|
this.guid = data.guid;
|
|
9
7
|
this.lifespanEnded = data.lifespanEnded;
|
|
@@ -12,19 +10,18 @@ class SearchResult extends plexObject_1.PlexObject {
|
|
|
12
10
|
this.year = data.year;
|
|
13
11
|
}
|
|
14
12
|
}
|
|
15
|
-
exports.SearchResult = SearchResult;
|
|
16
|
-
SearchResult.TAG = 'SearchResult';
|
|
17
13
|
/**
|
|
18
14
|
* Represents a single Agent
|
|
19
15
|
*/
|
|
20
|
-
class Agent extends
|
|
16
|
+
export class Agent extends PlexObject {
|
|
17
|
+
static { this.TAG = 'Agent'; }
|
|
21
18
|
// languageCode: any[] = [];
|
|
22
19
|
_loadData(data) {
|
|
23
20
|
this.hasAttribution = data.hasAttribution;
|
|
24
21
|
this.hasPrefs = data.hasPrefs;
|
|
25
22
|
this.identifier = data.identifier;
|
|
26
23
|
this.primary = data.primary;
|
|
27
|
-
this.shortIdentifier =
|
|
24
|
+
this.shortIdentifier = rsplit(this.identifier, '.', 1)[1];
|
|
28
25
|
if (this.initpath.includes('mediaType')) {
|
|
29
26
|
this.name = data.name;
|
|
30
27
|
// this.languageCode = [];
|
|
@@ -32,9 +29,7 @@ class Agent extends plexObject_1.PlexObject {
|
|
|
32
29
|
}
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
|
-
|
|
36
|
-
Agent.TAG = 'Agent';
|
|
37
|
-
exports.SEARCHTYPES = {
|
|
32
|
+
export const SEARCHTYPES = {
|
|
38
33
|
movie: 1,
|
|
39
34
|
show: 2,
|
|
40
35
|
season: 3,
|
|
@@ -52,21 +47,22 @@ exports.SEARCHTYPES = {
|
|
|
52
47
|
playlist: 15,
|
|
53
48
|
playlistFolder: 16,
|
|
54
49
|
collection: 18,
|
|
50
|
+
optimizedVersion: 42,
|
|
55
51
|
userPlaylistItem: 1001,
|
|
56
52
|
};
|
|
57
53
|
/**
|
|
58
54
|
* Returns the integer value of the library string type.
|
|
59
55
|
* @param libtype to lookup (movie, show, season, episode, artist, album, track, collection)
|
|
60
56
|
*/
|
|
61
|
-
function searchType(libtype) {
|
|
62
|
-
if (
|
|
63
|
-
.
|
|
64
|
-
|
|
57
|
+
export function searchType(libtype) {
|
|
58
|
+
if (libtype &&
|
|
59
|
+
Object.values(SEARCHTYPES)
|
|
60
|
+
.map(num => num.toString())
|
|
61
|
+
.includes(`${libtype}`)) {
|
|
65
62
|
return Number(libtype);
|
|
66
63
|
}
|
|
67
|
-
if (
|
|
68
|
-
return
|
|
64
|
+
if (libtype && SEARCHTYPES[libtype] !== undefined) {
|
|
65
|
+
return SEARCHTYPES[libtype];
|
|
69
66
|
}
|
|
70
67
|
throw new Error(`Unknown libtype: ${libtype}`);
|
|
71
68
|
}
|
|
72
|
-
exports.searchType = searchType;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|