plexsonic 0.1.12 → 0.1.20

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/.env.example CHANGED
@@ -4,6 +4,10 @@ BIND_HOST=127.0.0.1
4
4
  # Leave empty to derive from incoming request headers.
5
5
  BASE_URL=
6
6
  SQLITE_PATH=./data/app.db
7
+ CACHE_SQLITE_PATH=./data/cache.db
8
+ TRANSCODE_CACHE_PATH=./data/transcodes
9
+ TRANSCODE_CLEANUP_INTERVAL_SEC=3600
10
+ TRANSCODE_ARTIFACT_MAX_AGE_SEC=604800
7
11
  SESSION_SECRET=change-this-session-secret-before-production
8
12
  # 32-byte key in hex or base64 for Plex token encryption.
9
13
  TOKEN_ENC_KEY=
package/README.md CHANGED
@@ -10,6 +10,12 @@ It provides:
10
10
  - Web test page for manual API checks
11
11
  - Playback/scrobble/rating/playlist actions mapped to Plex
12
12
 
13
+ ## Local Cache
14
+
15
+ Plexsonic keeps a local SQLite cache of Plex music metadata to make browse/search responses fast and reduce repeated Plex API calls.
16
+ The cache is stored separately from account/session data (`CACHE_SQLITE_PATH`, default `./data/cache.db`), refreshes automatically when Plex changes are detected, and can be safely rebuilt if removed.
17
+ Cache entries are scoped by Plex account + Plex server/library, so local Plexsonic users linked to the same Plex account can share cached metadata.
18
+
13
19
  To support this project, please subscribe to my [Patreon](https://www.patreon.com/ClassicOldSong).
14
20
 
15
21
  [![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3DClassicOldsong%26type%3Dpatrons&style=for-the-badge)](https://patreon.com/ClassicOldsong)
@@ -53,6 +59,10 @@ PORT=3127
53
59
  BIND_HOST=127.0.0.1
54
60
  BASE_URL=
55
61
  SQLITE_PATH=./data/app.db
62
+ CACHE_SQLITE_PATH=./data/cache.db
63
+ TRANSCODE_CACHE_PATH=./data/transcodes
64
+ TRANSCODE_CLEANUP_INTERVAL_SEC=3600
65
+ TRANSCODE_ARTIFACT_MAX_AGE_SEC=604800
56
66
  SESSION_SECRET=replace-with-a-long-random-secret
57
67
  TOKEN_ENC_KEY=
58
68
  PLEX_PRODUCT=Plexsonic Bridge
@@ -69,6 +79,11 @@ LOG_REQUESTS=0
69
79
  - `PORT`: HTTP port (default `3127`).
70
80
  - `BIND_HOST`: listen interface (`127.0.0.1` local only, `0.0.0.0` for LAN).
71
81
  - `BASE_URL`: optional public URL override used for callback generation. If empty, origin is derived from request headers.
82
+ - `SQLITE_PATH`: credentials/session/application database path.
83
+ - `CACHE_SQLITE_PATH`: WAL-backed Plex metadata cache database path.
84
+ - `TRANSCODE_CACHE_PATH`: local cache directory for ffmpeg-transcoded stream outputs.
85
+ - `TRANSCODE_CLEANUP_INTERVAL_SEC`: periodic cleanup interval for transcode artifacts (`0` disables cleanup).
86
+ - `TRANSCODE_ARTIFACT_MAX_AGE_SEC`: max artifact age before cleanup removes it (`0` disables cleanup).
72
87
  - `PLEX_WEBHOOK_TOKEN`: optional shared secret for `/webhooks/plex`. If set, webhook calls must provide this token.
73
88
  - `SESSION_SECRET`: cookie/session signing secret. Keep stable across restarts.
74
89
  - `TOKEN_ENC_KEY`: optional but recommended 32-byte key (hex or base64) used to encrypt stored Plex tokens.
@@ -85,6 +100,17 @@ openssl rand -hex 32
85
100
  openssl rand -hex 32
86
101
  ```
87
102
 
103
+ ## Streaming Transcode
104
+
105
+ `/rest/stream.view` can transcode in-bridge (ffmpeg) to `mp3`, `aac`, `opus`, and `flac`.
106
+
107
+ - `format` chooses target codec/container when supported (`mp3`, `aac`, `opus`, `flac`).
108
+ - `maxBitRate` is respected for lossy outputs.
109
+ - If only `maxBitRate` is provided, Plexsonic defaults to `opus` transcode.
110
+ - `estimateContentLength` adds a best-effort `Content-Length` for uncached transcodes.
111
+ - Transcode outputs are cached on disk (`TRANSCODE_CACHE_PATH`) to enable byte-range seeking on subsequent requests.
112
+ - Cached transcode artifacts are auto-pruned by age (`TRANSCODE_CLEANUP_INTERVAL_SEC` / `TRANSCODE_ARTIFACT_MAX_AGE_SEC`).
113
+
88
114
  ## Run
89
115
 
90
116
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plexsonic",
3
- "version": "0.1.12",
3
+ "version": "0.1.20",
4
4
  "description": "PlexMusic to OpenSubsonic bridge",
5
5
  "main": "./src/index.js",
6
6
  "bin": {
package/src/config.js CHANGED
@@ -28,6 +28,8 @@ const DEFAULT_PLEX_PRODUCT = 'Plexsonic Bridge';
28
28
  const DEFAULT_SESSION_SECRET = 'dev-session-secret-change-me-before-production-plexsonic';
29
29
  const DEFAULT_LOG_LEVEL = 'warn';
30
30
  const DEFAULT_LOG_REQUESTS = false;
31
+ const DEFAULT_TRANSCODE_CLEANUP_INTERVAL_SECONDS = 3600;
32
+ const DEFAULT_TRANSCODE_ARTIFACT_MAX_AGE_SECONDS = 604800;
31
33
 
32
34
  function parsePort(rawPort) {
33
35
  const value = Number.parseInt(rawPort ?? `${DEFAULT_PORT}`, 10);
@@ -57,6 +59,17 @@ function parseBoolean(value, fallback = false) {
57
59
  return ['1', 'true', 'yes', 'on'].includes(normalized);
58
60
  }
59
61
 
62
+ function parseNonNegativeInt(value, fallback) {
63
+ if (value == null || value === '') {
64
+ return fallback;
65
+ }
66
+ const parsed = Number.parseInt(String(value), 10);
67
+ if (!Number.isFinite(parsed) || parsed < 0) {
68
+ return fallback;
69
+ }
70
+ return parsed;
71
+ }
72
+
60
73
  function normalizeOptionalBaseUrl(rawBaseUrl) {
61
74
  const value = String(rawBaseUrl || '').trim();
62
75
  if (!value) {
@@ -77,12 +90,26 @@ export function loadConfig(env = process.env) {
77
90
  const bindHost = env.BIND_HOST || DEFAULT_HOST;
78
91
  const baseUrl = normalizeOptionalBaseUrl(env.BASE_URL);
79
92
  const sqlitePath = env.SQLITE_PATH || './data/app.db';
93
+ const cacheSqlitePath = env.CACHE_SQLITE_PATH || './data/cache.db';
94
+ const transcodeCachePath = env.TRANSCODE_CACHE_PATH || './data/transcodes';
95
+ const transcodeCleanupIntervalSeconds = parseNonNegativeInt(
96
+ env.TRANSCODE_CLEANUP_INTERVAL_SEC,
97
+ DEFAULT_TRANSCODE_CLEANUP_INTERVAL_SECONDS,
98
+ );
99
+ const transcodeArtifactMaxAgeSeconds = parseNonNegativeInt(
100
+ env.TRANSCODE_ARTIFACT_MAX_AGE_SEC,
101
+ DEFAULT_TRANSCODE_ARTIFACT_MAX_AGE_SECONDS,
102
+ );
80
103
 
81
104
  return {
82
105
  bindHost,
83
106
  baseUrl,
84
107
  port,
85
108
  sqlitePath,
109
+ cacheSqlitePath,
110
+ transcodeCachePath,
111
+ transcodeCleanupIntervalSeconds,
112
+ transcodeArtifactMaxAgeSeconds,
86
113
  sessionSecret: normalizeSessionSecret(env.SESSION_SECRET),
87
114
  tokenEncKey: env.TOKEN_ENC_KEY || null,
88
115
  plexInsecureTls: env.PLEX_INSECURE_TLS === '1',
package/src/db.js CHANGED
@@ -121,6 +121,221 @@ export function migrate(db) {
121
121
  }
122
122
  }
123
123
 
124
+ export function migrateCache(db) {
125
+ const hasCacheAlbumTable = Boolean(
126
+ db.prepare(`
127
+ SELECT 1 AS exists_flag
128
+ FROM sqlite_master
129
+ WHERE type = 'table' AND name = 'plex_library_cache_albums'
130
+ LIMIT 1
131
+ `).get(),
132
+ );
133
+ if (hasCacheAlbumTable) {
134
+ const albumColumns = db.prepare(`PRAGMA table_info(plex_library_cache_albums)`).all();
135
+ const hasPlayCountColumn = albumColumns.some((column) => column.name === 'play_count');
136
+ if (!hasPlayCountColumn) {
137
+ db.exec(`
138
+ DROP TABLE IF EXISTS plex_library_cache_track_album_artists;
139
+ DROP TABLE IF EXISTS plex_library_cache_track_artists;
140
+ DROP TABLE IF EXISTS plex_library_cache_track_genres;
141
+ DROP TABLE IF EXISTS plex_library_cache_tracks;
142
+ DROP TABLE IF EXISTS plex_library_cache_album_genres;
143
+ DROP TABLE IF EXISTS plex_library_cache_albums;
144
+ DROP TABLE IF EXISTS plex_library_cache_artists;
145
+ DROP TABLE IF EXISTS plex_library_cache_state;
146
+ `);
147
+ }
148
+ }
149
+
150
+ db.exec(`
151
+ CREATE TABLE IF NOT EXISTS plex_library_cache_state (
152
+ cache_key TEXT PRIMARY KEY,
153
+ last_fingerprint TEXT NOT NULL DEFAULT '',
154
+ last_checked_at INTEGER NOT NULL DEFAULT 0,
155
+ last_synced_at INTEGER NOT NULL DEFAULT 0,
156
+ dirty INTEGER NOT NULL DEFAULT 0,
157
+ updated_at INTEGER NOT NULL
158
+ );
159
+
160
+ CREATE TABLE IF NOT EXISTS plex_library_cache_artists (
161
+ cache_key TEXT NOT NULL,
162
+ rating_key TEXT NOT NULL,
163
+ order_index INTEGER NOT NULL DEFAULT 0,
164
+ sort_key TEXT NOT NULL DEFAULT '',
165
+ title TEXT,
166
+ title_sort TEXT,
167
+ summary TEXT,
168
+ thumb TEXT,
169
+ key_path TEXT,
170
+ guid TEXT,
171
+ source_uri TEXT,
172
+ added_at INTEGER,
173
+ updated_at INTEGER,
174
+ user_rating INTEGER,
175
+ child_count INTEGER,
176
+ leaf_count INTEGER,
177
+ album_count INTEGER,
178
+ type TEXT,
179
+ updated_cache_at INTEGER NOT NULL,
180
+ PRIMARY KEY (cache_key, rating_key)
181
+ );
182
+
183
+ CREATE TABLE IF NOT EXISTS plex_library_cache_albums (
184
+ cache_key TEXT NOT NULL,
185
+ rating_key TEXT NOT NULL,
186
+ order_index INTEGER NOT NULL DEFAULT 0,
187
+ sort_key TEXT NOT NULL DEFAULT '',
188
+ title TEXT,
189
+ title_sort TEXT,
190
+ original_title TEXT,
191
+ parent_rating_key TEXT,
192
+ parent_title TEXT,
193
+ thumb TEXT,
194
+ key_path TEXT,
195
+ guid TEXT,
196
+ source_uri TEXT,
197
+ added_at INTEGER,
198
+ updated_at INTEGER,
199
+ last_viewed_at INTEGER,
200
+ user_rating INTEGER,
201
+ play_count INTEGER,
202
+ child_count INTEGER,
203
+ leaf_count INTEGER,
204
+ duration INTEGER,
205
+ year INTEGER,
206
+ type TEXT,
207
+ updated_cache_at INTEGER NOT NULL,
208
+ PRIMARY KEY (cache_key, rating_key)
209
+ );
210
+
211
+ CREATE TABLE IF NOT EXISTS plex_library_cache_album_genres (
212
+ cache_key TEXT NOT NULL,
213
+ album_rating_key TEXT NOT NULL,
214
+ order_index INTEGER NOT NULL DEFAULT 0,
215
+ genre_name TEXT NOT NULL,
216
+ PRIMARY KEY (cache_key, album_rating_key, genre_name)
217
+ );
218
+
219
+ CREATE TABLE IF NOT EXISTS plex_library_cache_tracks (
220
+ cache_key TEXT NOT NULL,
221
+ rating_key TEXT NOT NULL,
222
+ order_index INTEGER NOT NULL DEFAULT 0,
223
+ sort_key TEXT NOT NULL DEFAULT '',
224
+ title TEXT,
225
+ title_sort TEXT,
226
+ original_title TEXT,
227
+ parent_rating_key TEXT,
228
+ parent_title TEXT,
229
+ grandparent_rating_key TEXT,
230
+ grandparent_title TEXT,
231
+ artist_id TEXT,
232
+ key_path TEXT,
233
+ guid TEXT,
234
+ source_uri TEXT,
235
+ thumb TEXT,
236
+ added_at INTEGER,
237
+ updated_at INTEGER,
238
+ last_viewed_at INTEGER,
239
+ user_rating INTEGER,
240
+ view_count INTEGER,
241
+ duration INTEGER,
242
+ track_index INTEGER,
243
+ parent_index INTEGER,
244
+ disc_number INTEGER,
245
+ parent_year INTEGER,
246
+ year INTEGER,
247
+ media_bitrate INTEGER,
248
+ media_container TEXT,
249
+ part_size INTEGER,
250
+ part_file TEXT,
251
+ audio_sampling_rate INTEGER,
252
+ audio_bit_depth INTEGER,
253
+ audio_stream_language TEXT,
254
+ composer TEXT,
255
+ country TEXT,
256
+ style TEXT,
257
+ mood TEXT,
258
+ record_label TEXT,
259
+ language TEXT,
260
+ album_type TEXT,
261
+ is_compilation INTEGER,
262
+ is_soundtrack INTEGER,
263
+ type TEXT,
264
+ updated_cache_at INTEGER NOT NULL,
265
+ PRIMARY KEY (cache_key, rating_key)
266
+ );
267
+
268
+ CREATE TABLE IF NOT EXISTS plex_library_cache_track_genres (
269
+ cache_key TEXT NOT NULL,
270
+ track_rating_key TEXT NOT NULL,
271
+ order_index INTEGER NOT NULL DEFAULT 0,
272
+ genre_name TEXT NOT NULL,
273
+ PRIMARY KEY (cache_key, track_rating_key, genre_name)
274
+ );
275
+
276
+ CREATE TABLE IF NOT EXISTS plex_library_cache_track_artists (
277
+ cache_key TEXT NOT NULL,
278
+ track_rating_key TEXT NOT NULL,
279
+ order_index INTEGER NOT NULL DEFAULT 0,
280
+ artist_id TEXT,
281
+ artist_name TEXT NOT NULL,
282
+ PRIMARY KEY (cache_key, track_rating_key, order_index)
283
+ );
284
+
285
+ CREATE TABLE IF NOT EXISTS plex_library_cache_track_album_artists (
286
+ cache_key TEXT NOT NULL,
287
+ track_rating_key TEXT NOT NULL,
288
+ order_index INTEGER NOT NULL DEFAULT 0,
289
+ artist_id TEXT,
290
+ artist_name TEXT NOT NULL,
291
+ PRIMARY KEY (cache_key, track_rating_key, order_index)
292
+ );
293
+
294
+ CREATE INDEX IF NOT EXISTS idx_cache_artists_order
295
+ ON plex_library_cache_artists(cache_key, order_index);
296
+ CREATE INDEX IF NOT EXISTS idx_cache_artists_title
297
+ ON plex_library_cache_artists(cache_key, sort_key);
298
+ CREATE INDEX IF NOT EXISTS idx_cache_artists_rating
299
+ ON plex_library_cache_artists(cache_key, rating_key);
300
+ CREATE INDEX IF NOT EXISTS idx_cache_artists_starred
301
+ ON plex_library_cache_artists(cache_key, user_rating, order_index);
302
+
303
+ CREATE INDEX IF NOT EXISTS idx_cache_albums_order
304
+ ON plex_library_cache_albums(cache_key, order_index);
305
+ CREATE INDEX IF NOT EXISTS idx_cache_albums_title
306
+ ON plex_library_cache_albums(cache_key, sort_key);
307
+ CREATE INDEX IF NOT EXISTS idx_cache_albums_parent
308
+ ON plex_library_cache_albums(cache_key, parent_rating_key);
309
+ CREATE INDEX IF NOT EXISTS idx_cache_albums_rating
310
+ ON plex_library_cache_albums(cache_key, rating_key);
311
+ CREATE INDEX IF NOT EXISTS idx_cache_albums_starred
312
+ ON plex_library_cache_albums(cache_key, user_rating, order_index);
313
+
314
+ CREATE INDEX IF NOT EXISTS idx_cache_album_genres_name
315
+ ON plex_library_cache_album_genres(cache_key, genre_name);
316
+
317
+ CREATE INDEX IF NOT EXISTS idx_cache_tracks_order
318
+ ON plex_library_cache_tracks(cache_key, order_index);
319
+ CREATE INDEX IF NOT EXISTS idx_cache_tracks_title
320
+ ON plex_library_cache_tracks(cache_key, sort_key);
321
+ CREATE INDEX IF NOT EXISTS idx_cache_tracks_parent
322
+ ON plex_library_cache_tracks(cache_key, parent_rating_key);
323
+ CREATE INDEX IF NOT EXISTS idx_cache_tracks_grandparent
324
+ ON plex_library_cache_tracks(cache_key, grandparent_rating_key);
325
+ CREATE INDEX IF NOT EXISTS idx_cache_tracks_rating
326
+ ON plex_library_cache_tracks(cache_key, rating_key);
327
+ CREATE INDEX IF NOT EXISTS idx_cache_tracks_starred
328
+ ON plex_library_cache_tracks(cache_key, user_rating, order_index);
329
+
330
+ CREATE INDEX IF NOT EXISTS idx_cache_track_genres_name
331
+ ON plex_library_cache_track_genres(cache_key, genre_name);
332
+ CREATE INDEX IF NOT EXISTS idx_cache_track_artists_artist
333
+ ON plex_library_cache_track_artists(cache_key, artist_id, artist_name);
334
+ CREATE INDEX IF NOT EXISTS idx_cache_track_album_artists_artist
335
+ ON plex_library_cache_track_album_artists(cache_key, artist_id, artist_name);
336
+ `);
337
+ }
338
+
124
339
  export function createRepositories(db) {
125
340
  const createAccountStmt = db.prepare(`
126
341
  INSERT INTO accounts (id, username, password_hash, subsonic_password_enc, enabled, created_at)
@@ -265,6 +480,23 @@ export function createRepositories(db) {
265
480
  LEFT JOIN plex_selected_library psl ON psl.account_id = a.id
266
481
  WHERE a.id = ?
267
482
  `);
483
+ const listAccountPlexContextsStmt = db.prepare(`
484
+ SELECT
485
+ a.id AS account_id,
486
+ a.username AS username,
487
+ a.enabled AS enabled,
488
+ pl.plex_token_enc AS plex_token_enc,
489
+ pss.machine_id AS machine_id,
490
+ pss.name AS server_name,
491
+ pss.base_url AS server_base_url,
492
+ pss.server_token_enc AS server_token_enc,
493
+ psl.music_section_id AS music_section_id,
494
+ psl.music_section_name AS music_section_name
495
+ FROM accounts a
496
+ LEFT JOIN plex_links pl ON pl.account_id = a.id
497
+ LEFT JOIN plex_selected_server pss ON pss.account_id = a.id
498
+ LEFT JOIN plex_selected_library psl ON psl.account_id = a.id
499
+ `);
268
500
 
269
501
  const deletePlexLinkStmt = db.prepare(`
270
502
  DELETE FROM plex_links
@@ -409,6 +641,10 @@ export function createRepositories(db) {
409
641
  return getAccountPlexContextStmt.get(accountId) || null;
410
642
  },
411
643
 
644
+ listAccountPlexContexts() {
645
+ return listAccountPlexContextsStmt.all() || [];
646
+ },
647
+
412
648
  unlinkPlex(accountId) {
413
649
  const tx = db.transaction(() => {
414
650
  deleteSelectedLibraryStmt.run(accountId);
package/src/plex.js CHANGED
@@ -198,6 +198,83 @@ function extractMetadataList(container) {
198
198
  return asArray(container.Metadata ?? container.Directory ?? container.Video ?? container.Track ?? []);
199
199
  }
200
200
 
201
+ function parseContainerTotalSize(container) {
202
+ const parsed = Number.parseInt(String(container?.totalSize ?? ''), 10);
203
+ if (!Number.isFinite(parsed) || parsed < 0) {
204
+ return null;
205
+ }
206
+ return parsed;
207
+ }
208
+
209
+ async function fetchAllMetadataPages({
210
+ baseUrl,
211
+ plexToken,
212
+ path,
213
+ searchParams = null,
214
+ signal = undefined,
215
+ pageSize = 1000,
216
+ }) {
217
+ const normalizedPageSize = Math.min(Math.max(Number.parseInt(String(pageSize), 10) || 1000, 1), 2000);
218
+ const merged = [];
219
+ const seenRatingKeys = new Set();
220
+ let start = 0;
221
+ let totalSize = null;
222
+ let attempts = 0;
223
+
224
+ while (attempts < 2000) {
225
+ const payload = await fetchPmsJson(
226
+ baseUrl,
227
+ plexToken,
228
+ path,
229
+ {
230
+ ...(searchParams || {}),
231
+ 'X-Plex-Container-Start': start,
232
+ 'X-Plex-Container-Size': normalizedPageSize,
233
+ },
234
+ { signal },
235
+ );
236
+ const container = payload?.MediaContainer || {};
237
+ const page = extractMetadataList(container);
238
+ const pageTotalSize = parseContainerTotalSize(container);
239
+ if (pageTotalSize != null) {
240
+ totalSize = pageTotalSize;
241
+ }
242
+
243
+ if (page.length === 0) {
244
+ break;
245
+ }
246
+
247
+ let added = 0;
248
+ for (const item of page) {
249
+ const ratingKey = String(item?.ratingKey ?? '').trim();
250
+ if (ratingKey) {
251
+ if (seenRatingKeys.has(ratingKey)) {
252
+ continue;
253
+ }
254
+ seenRatingKeys.add(ratingKey);
255
+ }
256
+ merged.push(item);
257
+ added += 1;
258
+ }
259
+
260
+ start += page.length;
261
+ attempts += 1;
262
+
263
+ if (totalSize != null && start >= totalSize) {
264
+ break;
265
+ }
266
+ if (page.length < normalizedPageSize) {
267
+ break;
268
+ }
269
+ if (added === 0) {
270
+ // Guard against servers that ignore paging and keep returning the same page.
271
+ break;
272
+ }
273
+ }
274
+
275
+ return merged;
276
+ }
277
+
201
278
  function normalizeLibrarySection(section) {
202
279
  const parseTimestamp = (value) => {
203
280
  const parsed = Number.parseInt(String(value ?? ''), 10);
@@ -806,27 +883,36 @@ export async function searchSectionHubs({
806
883
  }
807
884
 
808
885
  export async function listArtists({ baseUrl, plexToken, sectionId }) {
809
- const payload = await fetchPmsJson(baseUrl, plexToken, `/library/sections/${encodeURIComponent(sectionId)}/all`, {
810
- type: 8,
886
+ return fetchAllMetadataPages({
887
+ baseUrl,
888
+ plexToken,
889
+ path: `/library/sections/${encodeURIComponent(sectionId)}/all`,
890
+ searchParams: {
891
+ type: 8,
892
+ },
811
893
  });
812
-
813
- return extractMetadataList(payload.MediaContainer);
814
894
  }
815
895
 
816
896
  export async function listAlbums({ baseUrl, plexToken, sectionId }) {
817
- const payload = await fetchPmsJson(baseUrl, plexToken, `/library/sections/${encodeURIComponent(sectionId)}/all`, {
818
- type: 9,
897
+ return fetchAllMetadataPages({
898
+ baseUrl,
899
+ plexToken,
900
+ path: `/library/sections/${encodeURIComponent(sectionId)}/all`,
901
+ searchParams: {
902
+ type: 9,
903
+ },
819
904
  });
820
-
821
- return extractMetadataList(payload.MediaContainer);
822
905
  }
823
906
 
824
907
  export async function listTracks({ baseUrl, plexToken, sectionId }) {
825
- const payload = await fetchPmsJson(baseUrl, plexToken, `/library/sections/${encodeURIComponent(sectionId)}/all`, {
826
- type: 10,
908
+ return fetchAllMetadataPages({
909
+ baseUrl,
910
+ plexToken,
911
+ path: `/library/sections/${encodeURIComponent(sectionId)}/all`,
912
+ searchParams: {
913
+ type: 10,
914
+ },
827
915
  });
828
-
829
- return extractMetadataList(payload.MediaContainer);
830
916
  }
831
917
 
832
918
  export async function probeSectionFingerprint({ baseUrl, plexToken, sectionId, signal = undefined }) {
@@ -868,19 +954,25 @@ export async function getArtist({ baseUrl, plexToken, artistId }) {
868
954
  }
869
955
 
870
956
  export async function listArtistAlbums({ baseUrl, plexToken, artistId }) {
871
- const payload = await fetchPmsJson(baseUrl, plexToken, `/library/metadata/${encodeURIComponent(artistId)}/children`, {
872
- type: 9,
957
+ return fetchAllMetadataPages({
958
+ baseUrl,
959
+ plexToken,
960
+ path: `/library/metadata/${encodeURIComponent(artistId)}/children`,
961
+ searchParams: {
962
+ type: 9,
963
+ },
873
964
  });
874
-
875
- return extractMetadataList(payload.MediaContainer);
876
965
  }
877
966
 
878
967
  export async function listArtistTracks({ baseUrl, plexToken, artistId }) {
879
- const payload = await fetchPmsJson(baseUrl, plexToken, `/library/metadata/${encodeURIComponent(artistId)}/allLeaves`, {
880
- type: 10,
968
+ return fetchAllMetadataPages({
969
+ baseUrl,
970
+ plexToken,
971
+ path: `/library/metadata/${encodeURIComponent(artistId)}/allLeaves`,
972
+ searchParams: {
973
+ type: 10,
974
+ },
881
975
  });
882
-
883
- return extractMetadataList(payload.MediaContainer);
884
976
  }
885
977
 
886
978
  export async function getAlbum({ baseUrl, plexToken, albumId }) {
@@ -893,11 +985,14 @@ export async function getAlbum({ baseUrl, plexToken, albumId }) {
893
985
  }
894
986
 
895
987
  export async function listAlbumTracks({ baseUrl, plexToken, albumId }) {
896
- const payload = await fetchPmsJson(baseUrl, plexToken, `/library/metadata/${encodeURIComponent(albumId)}/children`, {
897
- type: 10,
988
+ return fetchAllMetadataPages({
989
+ baseUrl,
990
+ plexToken,
991
+ path: `/library/metadata/${encodeURIComponent(albumId)}/children`,
992
+ searchParams: {
993
+ type: 10,
994
+ },
898
995
  });
899
-
900
- return extractMetadataList(payload.MediaContainer);
901
996
  }
902
997
 
903
998
  export async function getTrack({ baseUrl, plexToken, trackId, signal = undefined }) {