@ctrl/plex 3.6.1 → 3.8.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,578 @@
1
+ import { URLSearchParams } from 'url';
2
+ import { PartialPlexObject } from './base/partialPlexObject.js';
3
+ import { fetchItem, fetchItems } from './baseFunctionality.js';
4
+ import { Chapter, Collection, Country, Field, Format, Genre, Guid, Image, Label, Media, Mood, Similar, Style, Subformat, } from './media.js';
5
+ /**
6
+ * Base class for all audio objects including Artist, Album, and Track.
7
+ */
8
+ export class Audio extends PartialPlexObject {
9
+ /** Default metadata type for audio sync items. */
10
+ static { this.METADATA_TYPE = 'track'; }
11
+ /** Hardcoded list type for filtering. */
12
+ get listType() {
13
+ return 'audio';
14
+ }
15
+ /**
16
+ * @protected Should not be called directly. Use `server.fetchItem()`.
17
+ * Initializes a new instance of the Audio class.
18
+ * @param server The PlexServer instance used for communication.
19
+ * @param data The raw data object received from the Plex API.
20
+ * @param initpath The path used to fetch this item initially.
21
+ */
22
+ constructor(server, data, initpath, parent) {
23
+ super(server, data, initpath, parent);
24
+ this._loadData(data);
25
+ }
26
+ /**
27
+ * Returns the full URL for a given part (like a media stream) relative to the item's key.
28
+ * Includes the authentication token.
29
+ * @param part The relative path or resource identifier.
30
+ * @returns The full URL string including the server address and token, or undefined if part is empty.
31
+ */
32
+ url(part) {
33
+ // This typically refers to sub-resources like media parts, not the main item URL (which is `key`)
34
+ // The python version returns server.url(part), let's keep it consistent,
35
+ // assuming `part` is something like `/transcode/universal/start?protocol=...` relative to server root,
36
+ // or it might be used for things like `/tree`? The python `url` method seems rarely used directly.
37
+ // For now, mirroring the python seems safest. If `part` is meant to be appended to `this.key`,
38
+ // the logic would need changing.
39
+ return part ? this.server.url(part, true)?.toString() : undefined;
40
+ }
41
+ /** Indicates if the audio item has undergone sonic analysis. */
42
+ get hasSonicAnalysis() {
43
+ // Version 1 indicates sonic analysis is complete
44
+ return this.musicAnalysisVersion === 1;
45
+ }
46
+ /**
47
+ * Fetches a list of sonically similar audio items from the Plex server.
48
+ * The returned items will be instances of the same class as the current item
49
+ * (e.g., calling `sonicallySimilar` on a `Track` instance returns `Track[]`).
50
+ * @param limit Maximum number of similar items to return. Server default is 50.
51
+ * @param maxDistance Maximum sonic distance (0.0 - 1.0) between items. Server default is 0.25.
52
+ * @param options Optional additional filters to apply to the results.
53
+ * @returns A promise resolving to an array of sonically similar Audio items.
54
+ */
55
+ async sonicallySimilar(limit, maxDistance, options) {
56
+ if (!this.key) {
57
+ throw new Error('Cannot fetch similar items for an object without a key.');
58
+ }
59
+ const baseKey = `${this.key}/nearest`;
60
+ const params = new URLSearchParams();
61
+ if (limit !== undefined) {
62
+ params.append('limit', limit.toString());
63
+ }
64
+ if (maxDistance !== undefined) {
65
+ params.append('maxDistance', maxDistance.toString());
66
+ }
67
+ const finalKey = params.toString() ? `${baseKey}?${params.toString()}` : baseKey;
68
+ return fetchItems(this.server, finalKey, options, this.constructor, this);
69
+ }
70
+ /**
71
+ * Provides a default title for Sync operations based on the item's title.
72
+ * @returns The item's title, or undefined if the title is not set.
73
+ * @protected
74
+ */
75
+ _defaultSyncTitle() {
76
+ return this.title;
77
+ }
78
+ /**
79
+ * Populates the object's properties from the provided Plex API data.
80
+ * This method is called by the constructor and _loadFullData.
81
+ * @param data The raw data object from the Plex API.
82
+ * @protected
83
+ */
84
+ _loadData(data) {
85
+ this._data = data;
86
+ const addedAtTimestamp = data.addedAt ? parseInt(data.addedAt, 10) : NaN;
87
+ this.addedAt = isNaN(addedAtTimestamp) ? undefined : new Date(addedAtTimestamp * 1000);
88
+ this.art = data.art ? this.server.url(data.art, true)?.toString() : undefined;
89
+ this.artBlurHash = data.artBlurHash;
90
+ const distanceFloat = data.distance ? parseFloat(data.distance) : NaN;
91
+ this.distance = isNaN(distanceFloat) ? undefined : distanceFloat;
92
+ this.guid = data.guid;
93
+ const indexInt = data.index ? parseInt(data.index, 10) : NaN;
94
+ this.index = isNaN(indexInt) ? undefined : indexInt;
95
+ this.key = data.key ?? this.key ?? '';
96
+ const lastRatedAtTimestamp = data.lastRatedAt ? parseInt(data.lastRatedAt, 10) : NaN;
97
+ this.lastRatedAt = isNaN(lastRatedAtTimestamp)
98
+ ? undefined
99
+ : new Date(lastRatedAtTimestamp * 1000);
100
+ const lastViewedAtTimestamp = data.lastViewedAt ? parseInt(data.lastViewedAt, 10) : NaN;
101
+ this.lastViewedAt = isNaN(lastViewedAtTimestamp)
102
+ ? undefined
103
+ : new Date(lastViewedAtTimestamp * 1000);
104
+ const librarySectionIDInt = data.librarySectionID ? parseInt(data.librarySectionID, 10) : NaN;
105
+ this.librarySectionID = isNaN(librarySectionIDInt)
106
+ ? this.librarySectionID
107
+ : librarySectionIDInt;
108
+ this.librarySectionKey = data.librarySectionKey;
109
+ this.librarySectionTitle = data.librarySectionTitle;
110
+ // listType is handled by the getter
111
+ const musicAnalysisVersionInt = data.musicAnalysisVersion
112
+ ? parseInt(data.musicAnalysisVersion, 10)
113
+ : NaN;
114
+ this.musicAnalysisVersion = isNaN(musicAnalysisVersionInt)
115
+ ? undefined
116
+ : musicAnalysisVersionInt;
117
+ const ratingKeyInt = data.ratingKey ? parseInt(data.ratingKey, 10) : NaN;
118
+ this.ratingKey = isNaN(ratingKeyInt) ? this.ratingKey : ratingKeyInt.toString();
119
+ this.summary = data.summary;
120
+ this.thumb = data.thumb ? this.server.url(data.thumb, true)?.toString() : undefined;
121
+ this.thumbBlurHash = data.thumbBlurHash;
122
+ this.title = data.title ?? this.title;
123
+ this.titleSort = data.titleSort ?? this.title;
124
+ this.type = data.type ?? this.type;
125
+ const updatedAtTimestamp = data.updatedAt ? parseInt(data.updatedAt, 10) : NaN;
126
+ this.updatedAt = isNaN(updatedAtTimestamp) ? undefined : new Date(updatedAtTimestamp * 1000);
127
+ const userRatingFloat = data.userRating ? parseFloat(data.userRating) : NaN;
128
+ this.userRating = isNaN(userRatingFloat) ? undefined : userRatingFloat;
129
+ const viewCountInt = data.viewCount !== undefined ? parseInt(data.viewCount, 10) : NaN;
130
+ this.viewCount = isNaN(viewCountInt) ? 0 : viewCountInt;
131
+ // Map tag arrays like video.ts does
132
+ this.fields = data.Field?.map((d) => new Field(this.server, d, undefined, this)) ?? [];
133
+ this.images = data.Image?.map((d) => new Image(this.server, d, undefined, this)) ?? [];
134
+ this.moods = data.Mood?.map((d) => new Mood(this.server, d, undefined, this)) ?? [];
135
+ }
136
+ /**
137
+ * Overrides `PartialPlexObject._loadFullData` to apply Audio-specific attributes
138
+ * after fetching the full item data.
139
+ * @param data The raw data object representing the full item from the Plex API.
140
+ * @protected
141
+ */
142
+ _loadFullData(data) {
143
+ // Data is typically nested under 'Metadata' for single item fetches
144
+ const metadataItem = Array.isArray(data.Metadata) ? data.Metadata[0] : data;
145
+ if (metadataItem) {
146
+ this._loadData(metadataItem);
147
+ }
148
+ }
149
+ }
150
+ /**
151
+ * Represents a single Track.
152
+ */
153
+ export class Track extends Audio {
154
+ static { this.TAG = 'Track'; }
155
+ static { this.TYPE = 'track'; }
156
+ // Properties from Mixins (assuming, some might overlap with Audio)
157
+ // userRating inherited from Audio
158
+ // art inherited from Audio
159
+ // thumb inherited from Audio (used as poster?)
160
+ // theme inherited from Audio
161
+ constructor(server, data, initpath, parent) {
162
+ super(server, data, initpath, parent);
163
+ this._loadData(data);
164
+ }
165
+ /**
166
+ * @returns List of file paths where the track is found on disk.
167
+ */
168
+ get locations() {
169
+ const parts = this.media?.flatMap(m => m.parts ?? []) ?? [];
170
+ return parts.map(part => part.file).filter(Boolean);
171
+ }
172
+ /**
173
+ * @returns The track number.
174
+ */
175
+ get trackNumber() {
176
+ return this.index;
177
+ }
178
+ // /**
179
+ // * @returns File paths for all parts of this media item.
180
+ // */
181
+ // iterParts(): Part[] {
182
+ // return this.media?.flatMap(media => media.Part ?? []) ?? [];
183
+ // }
184
+ /**
185
+ * @returns A filename for use in download.
186
+ */
187
+ prettyfilename() {
188
+ const trackNum = String(this.trackNumber ?? '00').padStart(2, '0');
189
+ // Use optional chaining for potentially undefined properties
190
+ return `${this.grandparentTitle ?? 'Unknown Artist'} - ${this.parentTitle ?? 'Unknown Album'} - ${trackNum} - ${this.title ?? 'Unknown Track'}`;
191
+ }
192
+ /**
193
+ * Return the track's Album.
194
+ */
195
+ async album() {
196
+ if (!this.parentKey) {
197
+ throw new Error('Missing parentKey to fetch album');
198
+ }
199
+ const data = await fetchItem(this.server, this.parentKey);
200
+ return new Album(this.server, data, this.parentKey, this);
201
+ }
202
+ /**
203
+ * Return the track's Artist.
204
+ */
205
+ async artist() {
206
+ if (!this.grandparentKey) {
207
+ throw new Error('Missing grandparentKey to fetch artist');
208
+ }
209
+ const data = await fetchItem(this.server, this.grandparentKey);
210
+ return new Artist(this.server, data, this.grandparentKey, this);
211
+ }
212
+ /**
213
+ * @returns Default title for a new syncItem.
214
+ */
215
+ _defaultSyncTitle() {
216
+ // Use optional chaining for potentially undefined properties
217
+ return `${this.grandparentTitle ?? 'Unknown Artist'} - ${this.parentTitle ?? 'Unknown Album'} - ${this.title ?? 'Unknown Track'}`;
218
+ }
219
+ /**
220
+ * Returns the Plex Web URL pointing to the album details page for this track.
221
+ */
222
+ getWebURL(base) {
223
+ if (this.parentKey) {
224
+ const params = new URLSearchParams();
225
+ params.append('key', this.parentKey);
226
+ return this.server._buildWebURL(base, 'details', params);
227
+ }
228
+ return super.getWebURL(base);
229
+ }
230
+ // metadataDirectory property requires Path module and filesystem access, which is
231
+ // complex in node/browser. Omitting for now, might need specific implementation.
232
+ // get metadataDirectory(): string { ... }
233
+ /**
234
+ * Returns a sonic adventure from the current track to the specified track.
235
+ * @param to The target track for the sonic adventure.
236
+ */
237
+ async sonicAdventure(to) {
238
+ const section = await this.section();
239
+ const hasSonicAdventure = (s) => typeof s.sonicAdventure === 'function';
240
+ if (!hasSonicAdventure(section)) {
241
+ throw new Error('Section does not support sonicAdventure');
242
+ }
243
+ return section.sonicAdventure(this, to);
244
+ }
245
+ // /**
246
+ // * @returns The LibrarySection this item belongs to.
247
+ // */
248
+ // override section(): LibrarySection | undefined {
249
+ // let parent = this._parent; // Access protected _parent from base
250
+ // // Navigate up until we find a LibrarySection or hit the top
251
+ // while (parent) {
252
+ // // Basic check based on key structure, might need refinement
253
+ // if (parent instanceof PlexObject && parent.key?.startsWith('/library/sections/')) {
254
+ // return parent as LibrarySection;
255
+ // }
256
+ // // Check if parent has a _parent property to continue traversal
257
+ // if (!('_parent' in parent) || !parent._parent) {
258
+ // break;
259
+ // }
260
+ // parent = parent._parent as PlexObject | undefined;
261
+ // }
262
+ // return undefined;
263
+ // }
264
+ /**
265
+ * Populates the object's properties from the provided Plex API data,
266
+ * overriding the base Audio class method to add Track-specific attributes.
267
+ * @param data The raw data object from the Plex API.
268
+ * @protected
269
+ */
270
+ _loadData(data) {
271
+ super._loadData(data);
272
+ // Assign directly, assuming data properties are already numbers
273
+ this.audienceRating = data.audienceRating;
274
+ this.chapterSource = data.chapterSource;
275
+ this.duration = data.duration;
276
+ this.grandparentArt = data.grandparentArt;
277
+ this.grandparentGuid = data.grandparentGuid;
278
+ this.grandparentKey = data.grandparentKey;
279
+ this.grandparentRatingKey = data.grandparentRatingKey;
280
+ this.grandparentTheme = data.grandparentTheme;
281
+ this.grandparentThumb = data.grandparentThumb;
282
+ this.grandparentTitle = data.grandparentTitle;
283
+ this.originalTitle = data.originalTitle;
284
+ this.parentGuid = data.parentGuid;
285
+ this.parentIndex = data.parentIndex; // Disc Number
286
+ this.parentKey = data.parentKey;
287
+ this.parentRatingKey = data.parentRatingKey;
288
+ this.parentThumb = data.parentThumb;
289
+ this.parentTitle = data.parentTitle;
290
+ this.primaryExtraKey = data.primaryExtraKey;
291
+ this.rating = data.rating;
292
+ this.skipCount = data.skipCount;
293
+ this.sourceURI = data.source; // remote playlist item
294
+ this.viewOffset = data.viewOffset ?? 0;
295
+ this.year = data.year;
296
+ // ratingKey, index, title, etc., are loaded by super._loadData or PlexObject base
297
+ this.chapters = data.Chapter?.map(d => new Chapter(this.server, d, undefined, this));
298
+ this.collections = data.Collection?.map(d => new Collection(this.server, d, undefined, this));
299
+ this.genres = data.Genre?.map(d => new Genre(this.server, d, undefined, this));
300
+ this.guids = data.Guid?.map(d => new Guid(this.server, d, undefined, this));
301
+ this.labels = data.Label?.map(d => new Label(this.server, d, undefined, this));
302
+ this.media = data.Media?.map(d => new Media(this.server, d, undefined, this));
303
+ }
304
+ }
305
+ /**
306
+ * Represents a single Artist.
307
+ */
308
+ export class Artist extends Audio {
309
+ /* implements AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, ArtMixin, PosterMixin, ThemeMixin, ArtistEditMixins */
310
+ static { this.TAG = 'Directory'; }
311
+ static { this.TYPE = 'artist'; }
312
+ get locations() {
313
+ // Replicate listAttrs logic (assuming Location tag with path attribute)
314
+ return this._data?.Location?.map((loc) => loc.path).filter(Boolean) ?? [];
315
+ }
316
+ // Constructor calls super and _loadData
317
+ constructor(server, data, initpath, parent) {
318
+ super(server, data, initpath, parent);
319
+ // super constructor already calls _loadData, call again to apply Artist specifics
320
+ this._loadData(data);
321
+ }
322
+ /**
323
+ * Returns a list of Album objects by the artist.
324
+ * @param options Additional search options.
325
+ */
326
+ async albums(options = {}) {
327
+ if (!this.librarySectionID) {
328
+ await this.reload();
329
+ }
330
+ const section = await this.section();
331
+ if (!section || typeof section.search !== 'function') {
332
+ console.error('Cannot search for albums without a valid section');
333
+ return [];
334
+ }
335
+ // Combine artist filter with any provided filters
336
+ const filters = {
337
+ ...options.filters,
338
+ 'artist.id': this.ratingKey,
339
+ };
340
+ const { filters: _ignored, ...rest } = options;
341
+ return section.search({ libtype: 'album', ...rest, filters }, Album);
342
+ }
343
+ /**
344
+ * Returns the Album that matches the specified title for this artist.
345
+ * Case-insensitive exact match on title.
346
+ */
347
+ async album(title) {
348
+ if (!this.librarySectionID) {
349
+ await this.reload();
350
+ }
351
+ const section = await this.section();
352
+ if (!section || typeof section.search !== 'function') {
353
+ return undefined;
354
+ }
355
+ const filters = { 'artist.id': this.ratingKey };
356
+ const results = await section.search({ libtype: 'album', title__iexact: title, filters }, Album);
357
+ return results[0];
358
+ }
359
+ /**
360
+ * Returns the Track that matches the specified criteria.
361
+ * @param title Title of the track.
362
+ * @param album Album name (required if title not specified).
363
+ * @param track Track number (required if title not specified).
364
+ */
365
+ async track(args) {
366
+ const key = `${this.key}/allLeaves`;
367
+ let query = {};
368
+ if ('title' in args) {
369
+ query = { title__iexact: args.title };
370
+ }
371
+ else if ('album' in args && 'track' in args) {
372
+ query = { parentTitle__iexact: args.album, index: args.track };
373
+ }
374
+ else {
375
+ throw new Error('Missing argument: title or album and track are required');
376
+ }
377
+ try {
378
+ // fetchItem might throw NotFound, return undefined in that case
379
+ const data = await fetchItem(this.server, key, query);
380
+ return new Track(this.server, data, key, this);
381
+ }
382
+ catch (e) {
383
+ if (e.constructor.name === 'NotFound') {
384
+ return undefined;
385
+ }
386
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
387
+ throw e;
388
+ }
389
+ }
390
+ /**
391
+ * Returns a list of Track objects by the artist.
392
+ * @param options Additional fetch options.
393
+ */
394
+ async tracks(options = {}) {
395
+ const key = `${this.key}/allLeaves`;
396
+ return fetchItems(this.server, key, options, Track, this);
397
+ }
398
+ /** Alias of track(). */
399
+ async get(args) {
400
+ return this.track(args);
401
+ }
402
+ // /**
403
+ // * Returns a list of popular tracks by the artist.
404
+ // */
405
+ async popularTracks() {
406
+ const section = await this.section();
407
+ if (!section || typeof section.search !== 'function') {
408
+ return [];
409
+ }
410
+ return section.search({
411
+ libtype: 'track',
412
+ 'album.subformat!': 'Compilation,Live',
413
+ 'artist.id': this.ratingKey,
414
+ group: 'title',
415
+ 'ratingCount>>': 0,
416
+ sort: 'ratingCount:desc',
417
+ limit: 100,
418
+ }, Track);
419
+ }
420
+ // /**
421
+ // * Returns the artist radio station Playlist or undefined.
422
+ // */
423
+ // async station(): Promise<Playlist | undefined> {
424
+ // const key = `${this.key}?includeStations=1`;
425
+ // try {
426
+ // const stations = await fetchItems(
427
+ // this.server,
428
+ // key,
429
+ // undefined,
430
+ // PlexObject as any,
431
+ // this,
432
+ // 'Stations',
433
+ // );
434
+ // return stations[0]; // fetchItems with rtag extracts the items under that tag
435
+ // } catch (e) {
436
+ // console.error('Failed to fetch artist station', e);
437
+ // return undefined;
438
+ // }
439
+ // }
440
+ // // Known Linter Issue: Signature mismatch / override requirement incorrectly reported.
441
+ // section(): LibrarySection | undefined {
442
+ // // Known Linter Issue: Linter incorrectly flags _parent access.
443
+ // let parent = this._parent;
444
+ // while (parent) {
445
+ // if (parent instanceof PlexObject && parent.key?.startsWith('/library/sections/')) {
446
+ // return parent as LibrarySection;
447
+ // }
448
+ // if (!('_parent' in parent) || !parent._parent) {
449
+ // break;
450
+ // }
451
+ // parent = parent._parent as PlexObject | undefined;
452
+ // }
453
+ // return undefined;
454
+ // }
455
+ /**
456
+ * Load attribute values from Plex XML response.
457
+ * @protected
458
+ */
459
+ _loadData(data) {
460
+ super._loadData(data);
461
+ const albumSortInt = data.albumSort ? parseInt(String(data.albumSort), 10) : NaN;
462
+ this.albumSort = isNaN(albumSortInt) ? -1 : albumSortInt;
463
+ const audienceRatingFloat = data.audienceRating !== undefined ? parseFloat(String(data.audienceRating)) : NaN;
464
+ this.audienceRating = isNaN(audienceRatingFloat) ? undefined : audienceRatingFloat;
465
+ this.key = data.key?.replace('/children', '');
466
+ const ratingFloat = data.rating !== undefined ? parseFloat(String(data.rating)) : NaN;
467
+ this.rating = isNaN(ratingFloat) ? undefined : ratingFloat;
468
+ this.theme = data.theme;
469
+ this.countries = data.Country?.map(d => new Country(this.server, d, undefined, this));
470
+ this.genres = data.Genre?.map(d => new Genre(this.server, d, undefined, this));
471
+ this.guids = data.Guid?.map(d => new Guid(this.server, d, undefined, this));
472
+ this.labels = data.Label?.map(d => new Label(this.server, d, undefined, this));
473
+ this.similar = data.Similar?.map(d => new Similar(this.server, d, undefined, this));
474
+ this.styles = data.Style?.map(d => new Style(this.server, d, undefined, this));
475
+ this.collections = data.Collection?.map(d => new Collection(this.server, d, undefined, this));
476
+ }
477
+ }
478
+ /**
479
+ * Represents a single Album.
480
+ */
481
+ export class Album extends Audio {
482
+ static { this.TAG = 'Directory'; }
483
+ static { this.TYPE = 'album'; }
484
+ constructor(server, data, initpath, parent) {
485
+ super(server, data, initpath, parent);
486
+ this._loadData(data);
487
+ }
488
+ // TODO: not sure why this isn't working yet
489
+ // /**
490
+ // * Returns the Track that matches the specified criteria.
491
+ // * @param titleOrIndex Title of the track (string) or track number (number).
492
+ // * @param track Track number (optional, only used if titleOrIndex is not a number).
493
+ // */
494
+ // async track(titleOrIndex: string | number, track?: number): Promise<Track | undefined> {
495
+ // const key = `${this.key}/children`;
496
+ // let query: Record<string, any> = {};
497
+ // if (typeof titleOrIndex === 'string') {
498
+ // query = { title__iexact: titleOrIndex };
499
+ // // Allow specifying track number even with title, though less common
500
+ // if (track !== undefined) {
501
+ // query.index = track;
502
+ // }
503
+ // } else if (typeof titleOrIndex === 'number') {
504
+ // query = { index: titleOrIndex };
505
+ // } else {
506
+ // throw new Error('Missing argument: title or track number is required');
507
+ // }
508
+ // return fetchItem(this.server, key, query, Track);
509
+ // }
510
+ /**
511
+ * Returns a list of Track objects in the album.
512
+ * @param options Additional fetch options.
513
+ */
514
+ async tracks(options = {}) {
515
+ const key = `${this.key}/children`;
516
+ return fetchItems(this.server, key, options, Track, this);
517
+ }
518
+ /**
519
+ * Return the album's Artist.
520
+ */
521
+ async artist() {
522
+ if (!this.parentKey) {
523
+ throw new Error('Missing parentKey to fetch artist');
524
+ }
525
+ const data = await fetchItem(this.server, this.parentKey);
526
+ return new Artist(this.server, data, this.parentKey, this);
527
+ }
528
+ /**
529
+ * Returns the default title for a sync item.
530
+ */
531
+ _defaultSyncTitle() {
532
+ return `${this.parentTitle ?? 'Unknown Artist'} - ${this.title ?? 'Unknown Album'}`;
533
+ }
534
+ // section() method - Albums typically don't directly need this,
535
+ // but could be inherited or implemented if needed to find parent section.
536
+ /**
537
+ * Load attribute values from Plex XML response.
538
+ * @protected
539
+ */
540
+ _loadData(data) {
541
+ super._loadData(data);
542
+ // Assign directly, assuming data properties have correct types
543
+ this.audienceRating = data.audienceRating;
544
+ this.key = data.key?.replace('/children', ''); // Apply FIX_BUG_50
545
+ this.leafCount = data.leafCount;
546
+ this.loudnessAnalysisVersion = data.loudnessAnalysisVersion;
547
+ // Attempt direct Date parsing for originallyAvailableAt
548
+ try {
549
+ this.originallyAvailableAt = data.originallyAvailableAt
550
+ ? new Date(data.originallyAvailableAt)
551
+ : undefined;
552
+ // Check if the date is valid
553
+ if (this.originallyAvailableAt && isNaN(this.originallyAvailableAt.getTime())) {
554
+ this.originallyAvailableAt = undefined;
555
+ }
556
+ }
557
+ catch {
558
+ this.originallyAvailableAt = undefined;
559
+ }
560
+ this.parentGuid = data.parentGuid;
561
+ this.parentKey = data.parentKey;
562
+ this.parentRatingKey = data.parentRatingKey;
563
+ this.parentTheme = data.parentTheme;
564
+ this.parentThumb = data.parentThumb;
565
+ this.parentTitle = data.parentTitle;
566
+ this.rating = data.rating;
567
+ this.studio = data.studio;
568
+ this.viewedLeafCount = data.viewedLeafCount;
569
+ this.year = data.year;
570
+ this.collections = data.Collection?.map(d => new Collection(this.server, d, undefined, this));
571
+ this.formats = data.Format?.map(d => new Format(this.server, d, undefined, this));
572
+ this.genres = data.Genre?.map(d => new Genre(this.server, d, undefined, this));
573
+ this.guids = data.Guid?.map((d) => new Guid(this.server, d, undefined, this));
574
+ this.labels = data.Label?.map(d => new Label(this.server, d, undefined, this));
575
+ this.styles = data.Style?.map(d => new Style(this.server, d, undefined, this));
576
+ this.subformats = data.Subformat?.map(d => new Subformat(this.server, d, undefined, this));
577
+ }
578
+ }
@@ -0,0 +1,106 @@
1
+ import type { MediaTagData } from './video.types.js';
2
+ export interface UltraBlurColorsData {
3
+ topLeft?: string;
4
+ topRight?: string;
5
+ bottomRight?: string;
6
+ bottomLeft?: string;
7
+ }
8
+ export interface AlbumData {
9
+ key: string;
10
+ type: string;
11
+ title: string;
12
+ summary?: string;
13
+ librarySectionID?: number;
14
+ addedAt?: number;
15
+ updatedAt?: number;
16
+ audienceRating?: number;
17
+ leafCount?: number;
18
+ loudnessAnalysisVersion?: number;
19
+ originallyAvailableAt?: string;
20
+ parentGuid?: string;
21
+ parentKey?: string;
22
+ parentRatingKey?: number;
23
+ parentTheme?: string;
24
+ parentThumb?: string;
25
+ parentTitle?: string;
26
+ rating?: number;
27
+ studio?: string;
28
+ viewedLeafCount?: number;
29
+ year?: number;
30
+ Collection?: MediaTagData[];
31
+ Format?: MediaTagData[];
32
+ Genre?: MediaTagData[];
33
+ Guid?: MediaTagData[];
34
+ Label?: MediaTagData[];
35
+ Style?: MediaTagData[];
36
+ Subformat?: MediaTagData[];
37
+ UltraBlurColors?: UltraBlurColorsData[];
38
+ }
39
+ export interface TrackData {
40
+ key: string;
41
+ type: string;
42
+ title?: string;
43
+ rating?: number;
44
+ year?: number;
45
+ index?: number;
46
+ addedAt?: number;
47
+ updatedAt?: number;
48
+ parentKey?: string;
49
+ parentRatingKey?: number;
50
+ parentGuid?: string;
51
+ parentThumb?: string;
52
+ parentTitle?: string;
53
+ parentIndex?: number;
54
+ grandparentKey?: string;
55
+ grandparentRatingKey?: number;
56
+ grandparentGuid?: string;
57
+ grandparentThumb?: string;
58
+ grandparentTitle?: string;
59
+ grandparentArt?: string;
60
+ grandparentTheme?: string;
61
+ audienceRating?: number;
62
+ chapterSource?: string;
63
+ duration?: number;
64
+ originalTitle?: string;
65
+ primaryExtraKey?: string;
66
+ skipCount?: number;
67
+ source?: string;
68
+ viewOffset?: number;
69
+ Chapter?: import('./video.types.js').ChapterData[];
70
+ Collection?: MediaTagData[];
71
+ Genre?: MediaTagData[];
72
+ Guid?: MediaTagData[];
73
+ Label?: MediaTagData[];
74
+ Media?: import('./video.types.js').MediaData[];
75
+ }
76
+ export interface ArtistData {
77
+ key: string;
78
+ type: string;
79
+ title?: string;
80
+ rating?: number;
81
+ albumSort?: number | string;
82
+ audienceRating?: number;
83
+ theme?: string;
84
+ ratingKey?: string;
85
+ guid?: string;
86
+ titleSort?: string;
87
+ summary?: string;
88
+ index?: number;
89
+ thumb?: string;
90
+ art?: string;
91
+ addedAt?: number;
92
+ updatedAt?: number;
93
+ Country?: MediaTagData[];
94
+ Genre?: MediaTagData[];
95
+ Guid?: MediaTagData[];
96
+ Label?: MediaTagData[];
97
+ Similar?: MediaTagData[];
98
+ Style?: MediaTagData[];
99
+ Collection?: MediaTagData[];
100
+ UltraBlurColors?: UltraBlurColorsData;
101
+ Image?: Array<{
102
+ alt?: string;
103
+ type?: string;
104
+ url?: string;
105
+ }>;
106
+ }
@@ -0,0 +1 @@
1
+ export {};