@ctrl/plex 3.7.0 → 3.9.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.
@@ -0,0 +1,268 @@
1
+ import { PlexObject } from './base/plexObject.js';
2
+ import { findItems } from './baseFunctionality.js';
3
+ import { BadRequest } from './exceptions.js';
4
+ /**
5
+ * Control a PlayQueue.
6
+ *
7
+ * A PlayQueue is a linear list of media items that can be played in sequence.
8
+ * It represents the current playback queue and supports operations like adding,
9
+ * removing, and reordering items.
10
+ */
11
+ export class PlayQueue extends PlexObject {
12
+ constructor() {
13
+ super(...arguments);
14
+ /** Cache of PlayQueue items */
15
+ this._items = null;
16
+ }
17
+ static { this.TAG = 'PlayQueue'; }
18
+ static { this.TYPE = 'playqueue'; }
19
+ /**
20
+ * Retrieve an existing PlayQueue by identifier.
21
+ */
22
+ static async get(server, playQueueID, options = {}) {
23
+ const { own = false, center, window = 50, includeBefore = true, includeAfter = true } = options;
24
+ const args = {
25
+ own: own ? 1 : 0,
26
+ window,
27
+ includeBefore: includeBefore ? 1 : 0,
28
+ includeAfter: includeAfter ? 1 : 0,
29
+ };
30
+ if (center !== undefined) {
31
+ args.center = center;
32
+ }
33
+ const params = new URLSearchParams();
34
+ for (const [key, value] of Object.entries(args)) {
35
+ params.set(key, value.toString());
36
+ }
37
+ const path = `/playQueues/${playQueueID}?${params.toString()}`;
38
+ const data = await server.query(path, 'get');
39
+ const playQueue = new PlayQueue(server, data.MediaContainer, path);
40
+ return playQueue;
41
+ }
42
+ /**
43
+ * Create and return a new PlayQueue.
44
+ */
45
+ static async create(server, items, options = {}) {
46
+ const { startItem, shuffle = false, repeat = false, includeChapters = true, includeRelated = true, continuous = false, } = options;
47
+ const args = {
48
+ includeChapters: includeChapters ? 1 : 0,
49
+ includeRelated: includeRelated ? 1 : 0,
50
+ repeat: repeat ? 1 : 0,
51
+ shuffle: shuffle ? 1 : 0,
52
+ continuous: continuous ? 1 : 0,
53
+ };
54
+ if (Array.isArray(items)) {
55
+ const itemKeys = items.map(x => x.ratingKey).join(',');
56
+ const uriArgs = encodeURIComponent(`/library/metadata/${itemKeys}`);
57
+ args.uri = `library:///directory/${uriArgs}`;
58
+ args.type = items[0].listType;
59
+ }
60
+ else if (items.type === 'playlist') {
61
+ const playlist = items;
62
+ args.type = playlist.playlistType;
63
+ args.playlistID = playlist.ratingKey;
64
+ }
65
+ else {
66
+ const item = items;
67
+ args.type = item.listType;
68
+ const library = await server.library();
69
+ args.uri = `server://${server.machineIdentifier}/${library.identifier}${item.key}`;
70
+ }
71
+ if (startItem) {
72
+ args.key = startItem.key;
73
+ }
74
+ const params = new URLSearchParams();
75
+ for (const [key, value] of Object.entries(args)) {
76
+ params.set(key, value.toString());
77
+ }
78
+ const path = `/playQueues?${params.toString()}`;
79
+ const data = await server.query(path, 'post');
80
+ const playQueue = new PlayQueue(server, data.MediaContainer, path);
81
+ return playQueue;
82
+ }
83
+ /**
84
+ * Create and return a new PlayQueue from a station key.
85
+ * This is a convenience method for radio stations.
86
+ */
87
+ static async fromStationKey(server, key) {
88
+ const library = await server.library();
89
+ const args = {
90
+ type: 'audio',
91
+ uri: `server://${server.machineIdentifier}/${library.identifier}${key}`,
92
+ };
93
+ const params = new URLSearchParams();
94
+ for (const [key, value] of Object.entries(args)) {
95
+ params.set(key, value.toString());
96
+ }
97
+ const path = `/playQueues?${params.toString()}`;
98
+ const data = await server.query(path, 'post');
99
+ const playQueue = new PlayQueue(server, data.MediaContainer, path);
100
+ return playQueue;
101
+ }
102
+ /**
103
+ * Get items in the PlayQueue.
104
+ */
105
+ get items() {
106
+ if (this._items === null) {
107
+ this._items = findItems(this._data?.Metadata ?? [], {}, undefined, this.server, this);
108
+ // Set selectedItem based on the offset now that items are loaded
109
+ if (this.playQueueSelectedItemOffset >= 0 &&
110
+ this._items.length > this.playQueueSelectedItemOffset &&
111
+ !this.selectedItem) {
112
+ this.selectedItem = this._items[this.playQueueSelectedItemOffset];
113
+ }
114
+ }
115
+ return this._items;
116
+ }
117
+ /**
118
+ * Get item at specific index.
119
+ */
120
+ getItem(index) {
121
+ return this.items[index] || null;
122
+ }
123
+ /**
124
+ * Get the length of the PlayQueue.
125
+ */
126
+ get length() {
127
+ return this.playQueueTotalCount;
128
+ }
129
+ /**
130
+ * Check if the PlayQueue contains the provided media item.
131
+ */
132
+ contains(media) {
133
+ return this.items.some(x => x.playQueueItemID === media.playQueueItemID);
134
+ }
135
+ /**
136
+ * Get a similar object from this PlayQueue.
137
+ * Useful for looking up playQueueItemIDs using items from the Library.
138
+ */
139
+ getQueueItem(item) {
140
+ const matches = this.items.filter(x => x.ratingKey === item.ratingKey);
141
+ if (matches.length === 1) {
142
+ return matches[0];
143
+ }
144
+ else if (matches.length > 1) {
145
+ throw new BadRequest(`${item.title} occurs multiple times in this PlayQueue, provide exact item`);
146
+ }
147
+ else {
148
+ throw new BadRequest(`${item.title} not valid for this PlayQueue`);
149
+ }
150
+ }
151
+ /**
152
+ * Append an item to the "Up Next" section of the PlayQueue.
153
+ */
154
+ async addItem(item, options = {}) {
155
+ const { playNext = false, refresh = true } = options;
156
+ if (refresh) {
157
+ await this.refresh();
158
+ }
159
+ const args = {};
160
+ if (item.type === 'playlist') {
161
+ const playlist = item;
162
+ args.playlistID = playlist.ratingKey;
163
+ }
164
+ else {
165
+ const playableItem = item;
166
+ const section = await playableItem.section();
167
+ args.uri = `library://${section.uuid}/item${playableItem.key}`;
168
+ }
169
+ if (playNext) {
170
+ args.next = 1;
171
+ }
172
+ const params = new URLSearchParams();
173
+ for (const [key, value] of Object.entries(args)) {
174
+ params.set(key, value.toString());
175
+ }
176
+ const path = `/playQueues/${this.playQueueID}?${params.toString()}`;
177
+ const data = await this.server.query(path, 'put');
178
+ this._invalidateCacheAndLoadData(data.MediaContainer);
179
+ return this;
180
+ }
181
+ /**
182
+ * Move an item to the beginning of the PlayQueue.
183
+ * If 'after' is provided, the item will be placed immediately after the specified item.
184
+ */
185
+ async moveItem(item, options = {}) {
186
+ const { after, refresh = true } = options;
187
+ if (refresh) {
188
+ await this.refresh();
189
+ }
190
+ const args = {};
191
+ let queueItem = item;
192
+ if (!this.contains(item)) {
193
+ queueItem = this.getQueueItem(item);
194
+ }
195
+ if (after) {
196
+ let afterItem = after;
197
+ if (!this.contains(after)) {
198
+ afterItem = this.getQueueItem(after);
199
+ }
200
+ args.after = afterItem.playQueueItemID;
201
+ }
202
+ let path = `/playQueues/${this.playQueueID}/items/${queueItem.playQueueItemID}/move`;
203
+ if (Object.keys(args).length > 0) {
204
+ const params = new URLSearchParams();
205
+ for (const [key, value] of Object.entries(args)) {
206
+ params.set(key, value.toString());
207
+ }
208
+ path += `?${params.toString()}`;
209
+ }
210
+ const data = await this.server.query(path, 'put');
211
+ this._invalidateCacheAndLoadData(data.MediaContainer);
212
+ return this;
213
+ }
214
+ /**
215
+ * Remove an item from the PlayQueue.
216
+ */
217
+ async removeItem(item, refresh = true) {
218
+ if (refresh) {
219
+ await this.refresh();
220
+ }
221
+ let queueItem = item;
222
+ if (!this.contains(item)) {
223
+ queueItem = this.getQueueItem(item);
224
+ }
225
+ const path = `/playQueues/${this.playQueueID}/items/${queueItem.playQueueItemID}`;
226
+ const data = await this.server.query(path, 'delete');
227
+ this._invalidateCacheAndLoadData(data.MediaContainer);
228
+ return this;
229
+ }
230
+ /**
231
+ * Remove all items from the PlayQueue.
232
+ */
233
+ async clear() {
234
+ const path = `/playQueues/${this.playQueueID}/items`;
235
+ const data = await this.server.query(path, 'delete');
236
+ this._invalidateCacheAndLoadData(data.MediaContainer);
237
+ return this;
238
+ }
239
+ /**
240
+ * Refresh the PlayQueue from the Plex server.
241
+ */
242
+ async refresh() {
243
+ const path = `/playQueues/${this.playQueueID}`;
244
+ const data = await this.server.query(path, 'get');
245
+ this._invalidateCacheAndLoadData(data.MediaContainer);
246
+ }
247
+ _loadData(data) {
248
+ this._data = data; // Store raw data for items access
249
+ this.identifier = data.identifier;
250
+ this.mediaTagPrefix = data.mediaTagPrefix;
251
+ this.mediaTagVersion = data.mediaTagVersion;
252
+ this.playQueueID = data.playQueueID;
253
+ this.playQueueLastAddedItemID = data.playQueueLastAddedItemID;
254
+ this.playQueueSelectedItemID = data.playQueueSelectedItemID;
255
+ this.playQueueSelectedItemOffset = data.playQueueSelectedItemOffset;
256
+ this.playQueueSelectedMetadataItemID = data.playQueueSelectedMetadataItemID;
257
+ this.playQueueShuffled = data.playQueueShuffled;
258
+ this.playQueueSourceURI = data.playQueueSourceURI;
259
+ this.playQueueTotalCount = data.playQueueTotalCount;
260
+ this.playQueueVersion = data.playQueueVersion;
261
+ this.size = data.size || this.playQueueTotalCount;
262
+ // selectedItem will be set lazily when accessing items
263
+ }
264
+ _invalidateCacheAndLoadData(data) {
265
+ this._items = null; // Clear cached items
266
+ this._loadData(data);
267
+ }
268
+ }
@@ -0,0 +1,67 @@
1
+ import type { Playable } from './base/playable.js';
2
+ export interface PlayQueueResponse {
3
+ /** PlayQueue identifier */
4
+ identifier: string;
5
+ /** Media tag prefix path */
6
+ mediaTagPrefix: string;
7
+ /** Media tag version number */
8
+ mediaTagVersion: number;
9
+ /** Unique ID of the PlayQueue */
10
+ playQueueID: number;
11
+ /** ID of the last added item, defines where "Up Next" region starts */
12
+ playQueueLastAddedItemID?: number;
13
+ /** The queue item ID of the currently selected item */
14
+ playQueueSelectedItemID: number;
15
+ /** The offset of the selected item in the PlayQueue */
16
+ playQueueSelectedItemOffset: number;
17
+ /** ID of the currently selected item, matches ratingKey */
18
+ playQueueSelectedMetadataItemID: number;
19
+ /** True if the PlayQueue is shuffled */
20
+ playQueueShuffled: boolean;
21
+ /** Original URI used to create the PlayQueue */
22
+ playQueueSourceURI: string;
23
+ /** Total number of items in the PlayQueue */
24
+ playQueueTotalCount: number;
25
+ /** Version of the PlayQueue, increments on changes */
26
+ playQueueVersion: number;
27
+ /** Total size of the PlayQueue (alias for playQueueTotalCount) */
28
+ size: number;
29
+ }
30
+ export interface CreatePlayQueueOptions {
31
+ /** Media item in the PlayQueue where playback should begin */
32
+ startItem?: Playable;
33
+ /** Start the playqueue shuffled */
34
+ shuffle?: boolean;
35
+ /** Start the playqueue with repeat enabled */
36
+ repeat?: boolean;
37
+ /** Include chapters */
38
+ includeChapters?: boolean;
39
+ /** Include related content */
40
+ includeRelated?: boolean;
41
+ /** Include additional items after the initial item */
42
+ continuous?: boolean;
43
+ }
44
+ export interface GetPlayQueueOptions {
45
+ /** If server should transfer ownership */
46
+ own?: boolean;
47
+ /** The playQueueItemID of the center of the window */
48
+ center?: number;
49
+ /** Number of items to return from each side of the center item */
50
+ window?: number;
51
+ /** Include items before the center */
52
+ includeBefore?: boolean;
53
+ /** Include items after the center */
54
+ includeAfter?: boolean;
55
+ }
56
+ export interface AddPlayQueueItemOptions {
57
+ /** If true, add item to front of "Up Next" section */
58
+ playNext?: boolean;
59
+ /** Refresh the PlayQueue from server before updating */
60
+ refresh?: boolean;
61
+ }
62
+ export interface MovePlayQueueItemOptions {
63
+ /** Item to place the moved item after */
64
+ after?: Playable;
65
+ /** Refresh the PlayQueue from server before updating */
66
+ refresh?: boolean;
67
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,12 @@
1
1
  import { URL, URLSearchParams } from 'url';
2
+ import { Playable } from './base/playable.js';
2
3
  import { PlexClient } from './client.js';
3
4
  import { Hub, Library } from './library.js';
4
5
  import { Optimized } from './media.js';
5
6
  import { MyPlexAccount } from './myplex.js';
7
+ import type { Playlist } from './playlist.js';
8
+ import { PlayQueue } from './playqueue.js';
9
+ import type { CreatePlayQueueOptions } from './playqueue.types.js';
6
10
  import { Agent, SEARCHTYPES } from './search.js';
7
11
  import type { HistoryMetadatum } from './server.types.js';
8
12
  import { Settings } from './settings.js';
@@ -188,6 +192,14 @@ export declare class PlexServer {
188
192
  */
189
193
  history(maxresults?: number, mindate?: Date, ratingKey?: number | string, accountId?: number | string, librarySectionId?: number | string): Promise<HistoryMetadatum[]>;
190
194
  settings(): Promise<Settings>;
195
+ /**
196
+ * Creates and returns a new PlayQueue.
197
+ *
198
+ * @param item Media item or playlist to add to PlayQueue
199
+ * @param options Creation options for the PlayQueue
200
+ * @returns New PlayQueue instance
201
+ */
202
+ createPlayQueue(item: Playable | Playable[] | Playlist, options?: CreatePlayQueueOptions): Promise<PlayQueue>;
191
203
  /**
192
204
  * Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
193
205
  * token to access this server. If you are not the owner of this PlexServer
@@ -6,6 +6,7 @@ import { BASE_HEADERS, TIMEOUT, X_PLEX_CONTAINER_SIZE } from './config.js';
6
6
  import { Hub, Library } from './library.js';
7
7
  import { Optimized } from './media.js';
8
8
  import { MyPlexAccount } from './myplex.js';
9
+ import { PlayQueue } from './playqueue.js';
9
10
  import { Agent, SEARCHTYPES } from './search.js';
10
11
  import { Settings } from './settings.js';
11
12
  /**
@@ -178,6 +179,16 @@ export class PlexServer {
178
179
  // const data = this.query('/playlists');
179
180
  // console.log(JSON.stringify(data));
180
181
  // }
182
+ /**
183
+ * Creates and returns a new PlayQueue.
184
+ *
185
+ * @param item Media item or playlist to add to PlayQueue
186
+ * @param options Creation options for the PlayQueue
187
+ * @returns New PlayQueue instance
188
+ */
189
+ async createPlayQueue(item, options = {}) {
190
+ return PlayQueue.create(this, item, options);
191
+ }
181
192
  /**
182
193
  * Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
183
194
  * token to access this server. If you are not the owner of this PlexServer
@@ -3,7 +3,6 @@ import { Playable } from './base/playable.js';
3
3
  import type { ExtrasData, FullShowData, MovieData, ShowData } from './library.types.js';
4
4
  import { Chapter, Collection, Country, Director, Genre, Guid, Marker, Media, Poster, Producer, Rating, Role, Similar, Writer } from './media.js';
5
5
  import type { ChapterSource, EpisodeMetadata, FullMovieResponse } from './video.types.js';
6
- export type VideoType = Movie | Show;
7
6
  declare abstract class Video extends Playable {
8
7
  /** Datetime this item was added to the library. */
9
8
  addedAt: Date;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ctrl/plex",
3
- "version": "3.7.0",
4
- "description": "plex api client in typescript",
3
+ "version": "3.9.0",
4
+ "description": "plex api client in typescript using ofetch",
5
5
  "author": "Scott Cooper <scttcper@gmail.com>",
6
6
  "publishConfig": {
7
7
  "access": "public"