plexsonic 0.1.11 → 0.1.13
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/package.json +1 -1
- package/src/server.js +285 -164
- package/src/subsonic-xml.js +24 -74
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -197,6 +197,10 @@ function parsePlexWebhookPayload(body) {
|
|
|
197
197
|
if (!value || typeof value !== 'string') {
|
|
198
198
|
return null;
|
|
199
199
|
}
|
|
200
|
+
if (!/^\d+$/.test(albumId)) {
|
|
201
|
+
return sendSubsonicOk(reply, node('album'));
|
|
202
|
+
}
|
|
203
|
+
|
|
200
204
|
try {
|
|
201
205
|
return JSON.parse(value);
|
|
202
206
|
} catch {
|
|
@@ -406,7 +410,7 @@ function requestPublicOrigin(request, config) {
|
|
|
406
410
|
if (refererHeader) {
|
|
407
411
|
try {
|
|
408
412
|
host = new URL(refererHeader).host || '';
|
|
409
|
-
} catch {}
|
|
413
|
+
} catch { }
|
|
410
414
|
}
|
|
411
415
|
}
|
|
412
416
|
|
|
@@ -703,7 +707,7 @@ function groupArtistsForSubsonic(artists) {
|
|
|
703
707
|
});
|
|
704
708
|
|
|
705
709
|
return keys.map((key) => {
|
|
706
|
-
const
|
|
710
|
+
const artistNodes = groups
|
|
707
711
|
.get(key)
|
|
708
712
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
709
713
|
.map((artist) =>
|
|
@@ -712,12 +716,12 @@ function groupArtistsForSubsonic(artists) {
|
|
|
712
716
|
name: artist.name,
|
|
713
717
|
albumCount: artistAlbumCountValue(artist.artist),
|
|
714
718
|
coverArt: artist.id,
|
|
719
|
+
roles: ['artist'],
|
|
715
720
|
...subsonicRatingAttrs(artist.artist),
|
|
716
721
|
}),
|
|
717
|
-
)
|
|
718
|
-
.join('');
|
|
722
|
+
);
|
|
719
723
|
|
|
720
|
-
return node('index', { name: key },
|
|
724
|
+
return node('index', { name: key }, artistNodes);
|
|
721
725
|
});
|
|
722
726
|
}
|
|
723
727
|
|
|
@@ -752,7 +756,7 @@ function groupNamedEntriesForSubsonic(entries) {
|
|
|
752
756
|
});
|
|
753
757
|
|
|
754
758
|
return keys.map((key) => {
|
|
755
|
-
const
|
|
759
|
+
const items = groups
|
|
756
760
|
.get(key)
|
|
757
761
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
758
762
|
.map((item) =>
|
|
@@ -761,10 +765,9 @@ function groupNamedEntriesForSubsonic(entries) {
|
|
|
761
765
|
name: item.name,
|
|
762
766
|
coverArt: item.coverArt,
|
|
763
767
|
}),
|
|
764
|
-
)
|
|
765
|
-
.join('');
|
|
768
|
+
);
|
|
766
769
|
|
|
767
|
-
return node('index', { name: key },
|
|
770
|
+
return node('index', { name: key }, items);
|
|
768
771
|
});
|
|
769
772
|
}
|
|
770
773
|
|
|
@@ -1071,6 +1074,50 @@ function albumAttrs(album, fallbackArtistId = null, fallbackArtistName = null) {
|
|
|
1071
1074
|
};
|
|
1072
1075
|
}
|
|
1073
1076
|
|
|
1077
|
+
function buildArtistEntry(idCandidate, nameCandidate) {
|
|
1078
|
+
const name = firstNonEmptyText([nameCandidate], '');
|
|
1079
|
+
if (!name) {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
const id = String(idCandidate || '').trim();
|
|
1083
|
+
return {
|
|
1084
|
+
id: id || undefined,
|
|
1085
|
+
name,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function albumArtistEntries(album, fallbackArtistId = null, fallbackArtistName = null) {
|
|
1090
|
+
const artistId = firstNonEmptyText(
|
|
1091
|
+
[album?.parentRatingKey, album?.artistId, fallbackArtistId, album?.grandparentRatingKey],
|
|
1092
|
+
'',
|
|
1093
|
+
);
|
|
1094
|
+
const artistName = firstNonEmptyText(
|
|
1095
|
+
[album?.parentTitle, album?.artist, fallbackArtistName, album?.grandparentTitle],
|
|
1096
|
+
'',
|
|
1097
|
+
);
|
|
1098
|
+
const entry = buildArtistEntry(artistId, artistName);
|
|
1099
|
+
return entry ? [entry] : [];
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function renderArtistListNode(listName, entries, options = {}) {
|
|
1103
|
+
if (!entries || entries.length === 0) {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
const artistNodes = entries.map((entry) => emptyNode('artist', { id: entry.id, name: entry.name }));
|
|
1107
|
+
const attrs = options.flatten ? { flatten: 'true' } : {};
|
|
1108
|
+
return node(listName, attrs, artistNodes);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function albumNode(name, album, attrs, fallbackArtistId = null, fallbackArtistName = null, extra = null) {
|
|
1112
|
+
const artistEntries = albumArtistEntries(album, fallbackArtistId, fallbackArtistName);
|
|
1113
|
+
const artistsNode = renderArtistListNode('artists', artistEntries, { flatten: true });
|
|
1114
|
+
const inner = [artistsNode, extra].filter(Boolean);
|
|
1115
|
+
if (inner.length === 0) {
|
|
1116
|
+
return emptyNode(name, attrs);
|
|
1117
|
+
}
|
|
1118
|
+
return node(name, attrs, inner);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1074
1121
|
function artistAlbumCountValue(artist) {
|
|
1075
1122
|
return parseNonNegativeInt(
|
|
1076
1123
|
artist?.albumCount ?? artist?.childCount ?? artist?.album_count ?? artist?.leafCount,
|
|
@@ -1164,7 +1211,7 @@ function songAttrs(track, albumTitle = null, albumCoverArt = null, albumMetadata
|
|
|
1164
1211
|
'Unknown Album',
|
|
1165
1212
|
);
|
|
1166
1213
|
const normalizedArtist = firstNonEmptyText(
|
|
1167
|
-
[track?.
|
|
1214
|
+
[track?.originalTitle, track?.grandparentTitle],
|
|
1168
1215
|
'Unknown Artist',
|
|
1169
1216
|
);
|
|
1170
1217
|
const coverArt = firstNonEmptyText(
|
|
@@ -1276,6 +1323,58 @@ function songAttrs(track, albumTitle = null, albumCoverArt = null, albumMetadata
|
|
|
1276
1323
|
};
|
|
1277
1324
|
}
|
|
1278
1325
|
|
|
1326
|
+
function trackArtistEntries(track) {
|
|
1327
|
+
const artistId = firstNonEmptyText(
|
|
1328
|
+
[track?.artistId, track?.guid, track?.grandparentRatingKey],
|
|
1329
|
+
'',
|
|
1330
|
+
);
|
|
1331
|
+
const artistName = firstNonEmptyText(
|
|
1332
|
+
[track?.originalTitle, track?.artist, track?.grandparentTitle],
|
|
1333
|
+
'',
|
|
1334
|
+
);
|
|
1335
|
+
const entry = buildArtistEntry(artistId, artistName);
|
|
1336
|
+
return entry ? [entry] : [];
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function trackAlbumArtistEntries(track, albumMetadata = null) {
|
|
1340
|
+
const albumArtistId = firstNonEmptyText(
|
|
1341
|
+
[albumMetadata?.artistId, albumMetadata?.parentRatingKey, track?.artistId, track?.grandparentRatingKey],
|
|
1342
|
+
'',
|
|
1343
|
+
);
|
|
1344
|
+
const albumArtistName = firstNonEmptyText(
|
|
1345
|
+
[albumMetadata?.artist, albumMetadata?.parentTitle, track?.artist, track?.grandparentTitle],
|
|
1346
|
+
'',
|
|
1347
|
+
);
|
|
1348
|
+
const entry = buildArtistEntry(albumArtistId, albumArtistName);
|
|
1349
|
+
return entry ? [entry] : [];
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function buildSongArtistChildren(track, albumMetadata = null) {
|
|
1353
|
+
const artistsNode = renderArtistListNode('artists', trackArtistEntries(track), { flatten: true });
|
|
1354
|
+
const albumArtistsNode = renderArtistListNode('albumArtists', trackAlbumArtistEntries(track, albumMetadata), {
|
|
1355
|
+
flatten: true,
|
|
1356
|
+
});
|
|
1357
|
+
return [artistsNode, albumArtistsNode].filter(Boolean);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function songNode(track, albumTitle = null, albumCoverArt = null, albumMetadata = null) {
|
|
1361
|
+
const attrs = songAttrs(track, albumTitle, albumCoverArt, albumMetadata);
|
|
1362
|
+
const inner = buildSongArtistChildren(track, albumMetadata);
|
|
1363
|
+
if (inner.length === 0) {
|
|
1364
|
+
return emptyNode('song', attrs);
|
|
1365
|
+
}
|
|
1366
|
+
return node('song', attrs, inner);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function songChildNode(track, albumTitle = null, albumCoverArt = null, albumMetadata = null, extraAttrs = {}) {
|
|
1370
|
+
const attrs = { ...songAttrs(track, albumTitle, albumCoverArt, albumMetadata), ...extraAttrs };
|
|
1371
|
+
const inner = buildSongArtistChildren(track, albumMetadata);
|
|
1372
|
+
if (inner.length === 0) {
|
|
1373
|
+
return emptyNode('child', attrs);
|
|
1374
|
+
}
|
|
1375
|
+
return node('child', attrs, inner);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1279
1378
|
function sortTracksByDiscAndIndex(tracks) {
|
|
1280
1379
|
return [...(Array.isArray(tracks) ? tracks : [])].sort((a, b) => {
|
|
1281
1380
|
const discA = parsePositiveInt(a?.parentIndex ?? a?.discNumber, 1);
|
|
@@ -1820,39 +1919,39 @@ async function runPlexSearch({
|
|
|
1820
1919
|
const [extraArtists, extraAlbums, extraTracks] = await Promise.all([
|
|
1821
1920
|
missingArtist
|
|
1822
1921
|
? searchSectionMetadata({
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1922
|
+
baseUrl: plexState.baseUrl,
|
|
1923
|
+
plexToken: plexState.plexToken,
|
|
1924
|
+
sectionId: plexState.musicSectionId,
|
|
1925
|
+
type: 8,
|
|
1926
|
+
query,
|
|
1927
|
+
offset: 0,
|
|
1928
|
+
limit: artistWindow,
|
|
1929
|
+
signal,
|
|
1930
|
+
})
|
|
1832
1931
|
: [],
|
|
1833
1932
|
missingAlbum
|
|
1834
1933
|
? searchSectionMetadata({
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1934
|
+
baseUrl: plexState.baseUrl,
|
|
1935
|
+
plexToken: plexState.plexToken,
|
|
1936
|
+
sectionId: plexState.musicSectionId,
|
|
1937
|
+
type: 9,
|
|
1938
|
+
query,
|
|
1939
|
+
offset: 0,
|
|
1940
|
+
limit: albumWindow,
|
|
1941
|
+
signal,
|
|
1942
|
+
})
|
|
1844
1943
|
: [],
|
|
1845
1944
|
missingTrack
|
|
1846
1945
|
? searchSectionMetadata({
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1946
|
+
baseUrl: plexState.baseUrl,
|
|
1947
|
+
plexToken: plexState.plexToken,
|
|
1948
|
+
sectionId: plexState.musicSectionId,
|
|
1949
|
+
type: 10,
|
|
1950
|
+
query,
|
|
1951
|
+
offset: 0,
|
|
1952
|
+
limit: songWindow,
|
|
1953
|
+
signal,
|
|
1954
|
+
})
|
|
1856
1955
|
: [],
|
|
1857
1956
|
]);
|
|
1858
1957
|
|
|
@@ -1868,6 +1967,10 @@ function isAbortError(error) {
|
|
|
1868
1967
|
return error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
|
|
1869
1968
|
}
|
|
1870
1969
|
|
|
1970
|
+
function isNumericRatingKey(value) {
|
|
1971
|
+
return /^\d+$/.test(String(value || '').trim());
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1871
1974
|
function isUpstreamTerminationError(error) {
|
|
1872
1975
|
if (!error) {
|
|
1873
1976
|
return false;
|
|
@@ -1929,7 +2032,7 @@ async function fetchWithRetry({
|
|
|
1929
2032
|
|
|
1930
2033
|
try {
|
|
1931
2034
|
await response.body?.cancel?.();
|
|
1932
|
-
} catch {}
|
|
2035
|
+
} catch { }
|
|
1933
2036
|
} catch (error) {
|
|
1934
2037
|
if (isAbortError(error)) {
|
|
1935
2038
|
throw error;
|
|
@@ -2359,13 +2462,13 @@ function requiredPlexStateForSubsonic(reply, plexContext, tokenCipher) {
|
|
|
2359
2462
|
try {
|
|
2360
2463
|
const serverToken = decodePlexTokenOrThrow(tokenCipher, plexContext.server_token_enc);
|
|
2361
2464
|
plexTokenCandidates.push(serverToken);
|
|
2362
|
-
} catch {}
|
|
2465
|
+
} catch { }
|
|
2363
2466
|
}
|
|
2364
2467
|
if (plexContext.plex_token_enc) {
|
|
2365
2468
|
try {
|
|
2366
2469
|
const accountToken = decodePlexTokenOrThrow(tokenCipher, plexContext.plex_token_enc);
|
|
2367
2470
|
plexTokenCandidates.push(accountToken);
|
|
2368
|
-
} catch {}
|
|
2471
|
+
} catch { }
|
|
2369
2472
|
}
|
|
2370
2473
|
|
|
2371
2474
|
const plexToken = uniqueNonEmptyValues(plexTokenCandidates);
|
|
@@ -4174,7 +4277,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4174
4277
|
if (selectedServer.server_token_enc) {
|
|
4175
4278
|
try {
|
|
4176
4279
|
selectedServerToken = decodePlexTokenOrThrow(tokenCipher, selectedServer.server_token_enc);
|
|
4177
|
-
} catch {}
|
|
4280
|
+
} catch { }
|
|
4178
4281
|
}
|
|
4179
4282
|
|
|
4180
4283
|
try {
|
|
@@ -4504,8 +4607,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4504
4607
|
playerId: session?.clientIdentifier || undefined,
|
|
4505
4608
|
minutesAgo,
|
|
4506
4609
|
});
|
|
4507
|
-
})
|
|
4508
|
-
.join('');
|
|
4610
|
+
});
|
|
4509
4611
|
|
|
4510
4612
|
return sendSubsonicOk(reply, node('nowPlaying', {}, entries));
|
|
4511
4613
|
} catch (error) {
|
|
@@ -4599,20 +4701,17 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4599
4701
|
coverArt: artist.ratingKey,
|
|
4600
4702
|
...subsonicRatingAttrs(artist),
|
|
4601
4703
|
}),
|
|
4602
|
-
)
|
|
4603
|
-
.join('');
|
|
4704
|
+
);
|
|
4604
4705
|
|
|
4605
4706
|
const starredAlbums = albums
|
|
4606
4707
|
.filter((album) => isPlexLiked(album.userRating))
|
|
4607
|
-
.map((album) =>
|
|
4608
|
-
.join('');
|
|
4708
|
+
.map((album) => albumNode('album', album, albumAttrs(album)));
|
|
4609
4709
|
|
|
4610
4710
|
const starredSongs = tracks
|
|
4611
4711
|
.filter((track) => isPlexLiked(track.userRating))
|
|
4612
|
-
.map((track) =>
|
|
4613
|
-
.join('');
|
|
4712
|
+
.map((track) => songNode(track));
|
|
4614
4713
|
|
|
4615
|
-
return sendSubsonicOk(reply, node('starred', {},
|
|
4714
|
+
return sendSubsonicOk(reply, node('starred', {}, [...starredArtists, ...starredAlbums, ...starredSongs]));
|
|
4616
4715
|
} catch (error) {
|
|
4617
4716
|
request.log.error(error, 'Failed to load starred items');
|
|
4618
4717
|
return sendSubsonicError(reply, 10, 'Failed to load starred items');
|
|
@@ -4648,20 +4747,17 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4648
4747
|
coverArt: artist.ratingKey,
|
|
4649
4748
|
...subsonicRatingAttrs(artist),
|
|
4650
4749
|
}),
|
|
4651
|
-
)
|
|
4652
|
-
.join('');
|
|
4750
|
+
);
|
|
4653
4751
|
|
|
4654
4752
|
const starredAlbums = albums
|
|
4655
4753
|
.filter((album) => isPlexLiked(album.userRating))
|
|
4656
|
-
.map((album) =>
|
|
4657
|
-
.join('');
|
|
4754
|
+
.map((album) => albumNode('album', album, albumId3Attrs(album)));
|
|
4658
4755
|
|
|
4659
4756
|
const starredSongs = tracks
|
|
4660
4757
|
.filter((track) => isPlexLiked(track.userRating))
|
|
4661
|
-
.map((track) =>
|
|
4662
|
-
.join('');
|
|
4758
|
+
.map((track) => songNode(track));
|
|
4663
4759
|
|
|
4664
|
-
return sendSubsonicOk(reply, node('starred2', {},
|
|
4760
|
+
return sendSubsonicOk(reply, node('starred2', {}, [...starredArtists, ...starredAlbums, ...starredSongs]));
|
|
4665
4761
|
} catch (error) {
|
|
4666
4762
|
request.log.error(error, 'Failed to load starred items');
|
|
4667
4763
|
return sendSubsonicError(reply, 10, 'Failed to load starred items');
|
|
@@ -4712,7 +4808,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4712
4808
|
}
|
|
4713
4809
|
}
|
|
4714
4810
|
|
|
4715
|
-
const
|
|
4811
|
+
const genreNodes = [...counts.values()]
|
|
4716
4812
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
4717
4813
|
.map((genre) =>
|
|
4718
4814
|
node(
|
|
@@ -4723,10 +4819,9 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4723
4819
|
},
|
|
4724
4820
|
genre.name,
|
|
4725
4821
|
),
|
|
4726
|
-
)
|
|
4727
|
-
.join('');
|
|
4822
|
+
);
|
|
4728
4823
|
|
|
4729
|
-
return sendSubsonicOk(reply, node('genres', {},
|
|
4824
|
+
return sendSubsonicOk(reply, node('genres', {}, genreNodes));
|
|
4730
4825
|
} catch (error) {
|
|
4731
4826
|
request.log.error(error, 'Failed to load genres');
|
|
4732
4827
|
return sendSubsonicError(reply, 10, 'Failed to load genres');
|
|
@@ -4774,8 +4869,8 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4774
4869
|
const sortedSongs = sortTracksForLibraryBrowse(matchedSongs);
|
|
4775
4870
|
const page = takePage(sortedSongs, offset, count)
|
|
4776
4871
|
.map((track) => withResolvedTrackGenres(track, albumGenreTagMap));
|
|
4777
|
-
const
|
|
4778
|
-
return sendSubsonicOk(reply, node('songsByGenre', {},
|
|
4872
|
+
const songs = page.map((track) => songNode(track));
|
|
4873
|
+
return sendSubsonicOk(reply, node('songsByGenre', {}, songs));
|
|
4779
4874
|
} catch (error) {
|
|
4780
4875
|
request.log.error(error, 'Failed to load songs by genre');
|
|
4781
4876
|
return sendSubsonicError(reply, 10, 'Failed to load songs by genre');
|
|
@@ -4801,8 +4896,8 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4801
4896
|
const allTracks = await getCachedLibraryTracks({ accountId: account.id, plexState, request });
|
|
4802
4897
|
|
|
4803
4898
|
const randomTracks = shuffleInPlace(allTracks.slice()).slice(0, size);
|
|
4804
|
-
const
|
|
4805
|
-
return sendSubsonicOk(reply, node('randomSongs', {},
|
|
4899
|
+
const songs = randomTracks.map((track) => songNode(track));
|
|
4900
|
+
return sendSubsonicOk(reply, node('randomSongs', {}, songs));
|
|
4806
4901
|
} catch (error) {
|
|
4807
4902
|
request.log.error(error, 'Failed to load random songs');
|
|
4808
4903
|
return sendSubsonicError(reply, 10, 'Failed to load random songs');
|
|
@@ -4845,8 +4940,8 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4845
4940
|
applyCachedRatingOverridesForAccount({ accountId: account.id, plexState, items: tracks });
|
|
4846
4941
|
|
|
4847
4942
|
const topTracks = tracks.slice(0, size);
|
|
4848
|
-
const
|
|
4849
|
-
return sendSubsonicOk(reply, node('topSongs', {},
|
|
4943
|
+
const songs = topTracks.map((track) => songNode(track));
|
|
4944
|
+
return sendSubsonicOk(reply, node('topSongs', {}, songs));
|
|
4850
4945
|
} catch (error) {
|
|
4851
4946
|
request.log.error(error, 'Failed to load top songs');
|
|
4852
4947
|
return sendSubsonicError(reply, 10, 'Failed to load top songs');
|
|
@@ -4966,8 +5061,8 @@ export async function buildServer(config = loadConfig()) {
|
|
|
4966
5061
|
}
|
|
4967
5062
|
applyCachedRatingOverridesForAccount({ accountId: account.id, plexState, items: tracks });
|
|
4968
5063
|
|
|
4969
|
-
const
|
|
4970
|
-
return sendSubsonicOk(reply, node('similarSongs', {},
|
|
5064
|
+
const songs = tracks.map((track) => songNode(track));
|
|
5065
|
+
return sendSubsonicOk(reply, node('similarSongs', {}, songs));
|
|
4971
5066
|
} catch (error) {
|
|
4972
5067
|
request.log.error(error, 'Failed to load similar songs');
|
|
4973
5068
|
return sendSubsonicError(reply, 10, 'Failed to load similar songs');
|
|
@@ -5010,8 +5105,8 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5010
5105
|
}
|
|
5011
5106
|
applyCachedRatingOverridesForAccount({ accountId: account.id, plexState, items: tracks });
|
|
5012
5107
|
|
|
5013
|
-
const
|
|
5014
|
-
return sendSubsonicOk(reply, node('similarSongs2', {},
|
|
5108
|
+
const songs = tracks.map((track) => songNode(track));
|
|
5109
|
+
return sendSubsonicOk(reply, node('similarSongs2', {}, songs));
|
|
5015
5110
|
} catch (error) {
|
|
5016
5111
|
request.log.error(error, 'Failed to load similar songs2');
|
|
5017
5112
|
return sendSubsonicError(reply, 10, 'Failed to load similar songs');
|
|
@@ -5143,11 +5238,11 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5143
5238
|
|
|
5144
5239
|
const lyricCandidates = matchedTrackId
|
|
5145
5240
|
? await fetchPlexTrackLyricsCandidates({
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5241
|
+
baseUrl: plexState.baseUrl,
|
|
5242
|
+
plexToken: plexState.plexToken,
|
|
5243
|
+
trackId: matchedTrackId,
|
|
5244
|
+
signal: searchScope.signal,
|
|
5245
|
+
})
|
|
5151
5246
|
: [];
|
|
5152
5247
|
|
|
5153
5248
|
const plainLyrics = buildPlainLyricsFromStructuredLyrics(
|
|
@@ -5232,9 +5327,9 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5232
5327
|
});
|
|
5233
5328
|
|
|
5234
5329
|
const structuredLyrics = extractStructuredLyricsFromTrack(track, lyricCandidates);
|
|
5235
|
-
const
|
|
5330
|
+
const structuredLyricsNodes = structuredLyrics
|
|
5236
5331
|
.map((lyrics) => {
|
|
5237
|
-
const
|
|
5332
|
+
const lineNodes = lyrics.lines
|
|
5238
5333
|
.map((line) =>
|
|
5239
5334
|
node(
|
|
5240
5335
|
'line',
|
|
@@ -5243,8 +5338,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5243
5338
|
},
|
|
5244
5339
|
line.value,
|
|
5245
5340
|
),
|
|
5246
|
-
)
|
|
5247
|
-
.join('');
|
|
5341
|
+
);
|
|
5248
5342
|
|
|
5249
5343
|
return node(
|
|
5250
5344
|
'structuredLyrics',
|
|
@@ -5255,12 +5349,11 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5255
5349
|
synced: lyrics.synced,
|
|
5256
5350
|
offset: lyrics.offset,
|
|
5257
5351
|
},
|
|
5258
|
-
|
|
5352
|
+
lineNodes,
|
|
5259
5353
|
);
|
|
5260
|
-
})
|
|
5261
|
-
.join('');
|
|
5354
|
+
});
|
|
5262
5355
|
|
|
5263
|
-
return sendSubsonicOk(reply, node('lyricsList', {},
|
|
5356
|
+
return sendSubsonicOk(reply, node('lyricsList', {}, structuredLyricsNodes));
|
|
5264
5357
|
} catch (error) {
|
|
5265
5358
|
if (isAbortError(error)) {
|
|
5266
5359
|
const reason = String(lyricsScope.reason() || '');
|
|
@@ -5364,7 +5457,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5364
5457
|
);
|
|
5365
5458
|
}
|
|
5366
5459
|
|
|
5367
|
-
const
|
|
5460
|
+
const artistNodes = matchedArtists
|
|
5368
5461
|
.map((artist) =>
|
|
5369
5462
|
emptyNode('artist', {
|
|
5370
5463
|
id: artist.ratingKey,
|
|
@@ -5373,16 +5466,13 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5373
5466
|
coverArt: artist.ratingKey,
|
|
5374
5467
|
...subsonicRatingAttrs(artist),
|
|
5375
5468
|
}),
|
|
5376
|
-
)
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
.
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
.join('');
|
|
5384
|
-
|
|
5385
|
-
return sendSubsonicOk(reply, node('searchResult3', {}, `${artistXml}${albumXml}${songXml}`));
|
|
5469
|
+
);
|
|
5470
|
+
const albumNodes = matchedAlbums
|
|
5471
|
+
.map((album) => albumNode('album', album, albumId3Attrs(album)));
|
|
5472
|
+
const songNodes = matchedTracks
|
|
5473
|
+
.map((track) => songNode(track));
|
|
5474
|
+
|
|
5475
|
+
return sendSubsonicOk(reply, node('searchResult3', {}, [...artistNodes, ...albumNodes, ...songNodes]));
|
|
5386
5476
|
} catch (error) {
|
|
5387
5477
|
if (isAbortError(error)) {
|
|
5388
5478
|
const reason = String(searchScope.reason() || '');
|
|
@@ -5462,7 +5552,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5462
5552
|
songCount,
|
|
5463
5553
|
);
|
|
5464
5554
|
|
|
5465
|
-
const
|
|
5555
|
+
const artistNodes = matchedArtists
|
|
5466
5556
|
.map((artist) =>
|
|
5467
5557
|
emptyNode('artist', {
|
|
5468
5558
|
id: artist.ratingKey,
|
|
@@ -5470,16 +5560,13 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5470
5560
|
coverArt: artist.ratingKey,
|
|
5471
5561
|
...subsonicRatingAttrs(artist),
|
|
5472
5562
|
}),
|
|
5473
|
-
)
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
.
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
.join('');
|
|
5481
|
-
|
|
5482
|
-
return sendSubsonicOk(reply, node('searchResult2', {}, `${artistXml}${albumXml}${songXml}`));
|
|
5563
|
+
);
|
|
5564
|
+
const albumNodes = matchedAlbums
|
|
5565
|
+
.map((album) => albumNode('album', album, albumAttrs(album)));
|
|
5566
|
+
const songNodes = matchedTracks
|
|
5567
|
+
.map((track) => songNode(track));
|
|
5568
|
+
|
|
5569
|
+
return sendSubsonicOk(reply, node('searchResult2', {}, [...artistNodes, ...albumNodes, ...songNodes]));
|
|
5483
5570
|
} catch (error) {
|
|
5484
5571
|
if (isAbortError(error)) {
|
|
5485
5572
|
const reason = String(searchScope.reason() || '');
|
|
@@ -5559,7 +5646,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5559
5646
|
songCount,
|
|
5560
5647
|
);
|
|
5561
5648
|
|
|
5562
|
-
const
|
|
5649
|
+
const artistNodes = matchedArtists
|
|
5563
5650
|
.map((artist) =>
|
|
5564
5651
|
emptyNode('artist', {
|
|
5565
5652
|
id: artist.ratingKey,
|
|
@@ -5567,12 +5654,10 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5567
5654
|
coverArt: artist.ratingKey,
|
|
5568
5655
|
...subsonicRatingAttrs(artist),
|
|
5569
5656
|
}),
|
|
5570
|
-
)
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
.join('');
|
|
5575
|
-
const matchXml = matchedTracks
|
|
5657
|
+
);
|
|
5658
|
+
const albumNodes = matchedAlbums
|
|
5659
|
+
.map((album) => albumNode('album', album, albumAttrs(album)));
|
|
5660
|
+
const matchNodes = matchedTracks
|
|
5576
5661
|
.map((track) =>
|
|
5577
5662
|
emptyNode('match', {
|
|
5578
5663
|
id: track.ratingKey,
|
|
@@ -5580,10 +5665,9 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5580
5665
|
album: track.parentTitle,
|
|
5581
5666
|
artist: track.grandparentTitle,
|
|
5582
5667
|
}),
|
|
5583
|
-
)
|
|
5584
|
-
.join('');
|
|
5668
|
+
);
|
|
5585
5669
|
|
|
5586
|
-
return sendSubsonicOk(reply, node('searchResult', {},
|
|
5670
|
+
return sendSubsonicOk(reply, node('searchResult', {}, [...artistNodes, ...albumNodes, ...matchNodes]));
|
|
5587
5671
|
} catch (error) {
|
|
5588
5672
|
if (isAbortError(error)) {
|
|
5589
5673
|
const reason = String(searchScope.reason() || '');
|
|
@@ -5656,8 +5740,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
5656
5740
|
});
|
|
5657
5741
|
|
|
5658
5742
|
const entries = tracks
|
|
5659
|
-
.map((track) => emptyNode('entry', songAttrs(track)))
|
|
5660
|
-
.join('');
|
|
5743
|
+
.map((track) => emptyNode('entry', songAttrs(track)));
|
|
5661
5744
|
const changedIso = new Date(Number(queueState.updatedAt || Date.now())).toISOString();
|
|
5662
5745
|
|
|
5663
5746
|
return sendSubsonicOk(
|
|
@@ -6196,11 +6279,10 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6196
6279
|
});
|
|
6197
6280
|
|
|
6198
6281
|
const nowIso = new Date().toISOString();
|
|
6199
|
-
const
|
|
6200
|
-
.map((playlist) => emptyNode('playlist', playlistAttrs(playlist, account.username, nowIso)))
|
|
6201
|
-
.join('');
|
|
6282
|
+
const playlistNodes = playlists
|
|
6283
|
+
.map((playlist) => emptyNode('playlist', playlistAttrs(playlist, account.username, nowIso)));
|
|
6202
6284
|
|
|
6203
|
-
return sendSubsonicOk(reply, node('playlists', {},
|
|
6285
|
+
return sendSubsonicOk(reply, node('playlists', {}, playlistNodes));
|
|
6204
6286
|
} catch (error) {
|
|
6205
6287
|
request.log.error(error, 'Failed to load playlists');
|
|
6206
6288
|
return sendSubsonicError(reply, 10, 'Failed to load playlists');
|
|
@@ -6247,8 +6329,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6247
6329
|
.map((track) => {
|
|
6248
6330
|
const attrs = songAttrs(track, track.parentTitle || undefined, track.parentRatingKey || undefined);
|
|
6249
6331
|
return emptyNode('entry', attrs);
|
|
6250
|
-
})
|
|
6251
|
-
.join('');
|
|
6332
|
+
});
|
|
6252
6333
|
|
|
6253
6334
|
return sendSubsonicOk(reply, node('playlist', playlistAttrs(playlist, account.username, nowIso), entries));
|
|
6254
6335
|
} catch (error) {
|
|
@@ -6421,7 +6502,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6421
6502
|
try {
|
|
6422
6503
|
const artists = await getCachedLibraryArtists({ accountId: account.id, plexState, request });
|
|
6423
6504
|
|
|
6424
|
-
const indexes = groupArtistsForSubsonic(artists)
|
|
6505
|
+
const indexes = groupArtistsForSubsonic(artists);
|
|
6425
6506
|
return sendSubsonicOk(reply, node('artists', { ignoredArticles: 'The El La Los Las Le Les' }, indexes));
|
|
6426
6507
|
} catch (error) {
|
|
6427
6508
|
request.log.error(error, 'Failed to load artists from Plex');
|
|
@@ -6465,7 +6546,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6465
6546
|
})
|
|
6466
6547
|
.filter(Boolean);
|
|
6467
6548
|
|
|
6468
|
-
const indexes = groupNamedEntriesForSubsonic(folderEntries)
|
|
6549
|
+
const indexes = groupNamedEntriesForSubsonic(folderEntries);
|
|
6469
6550
|
return sendSubsonicOk(
|
|
6470
6551
|
reply,
|
|
6471
6552
|
node(
|
|
@@ -6494,6 +6575,10 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6494
6575
|
return sendSubsonicError(reply, 70, 'Missing artist id');
|
|
6495
6576
|
}
|
|
6496
6577
|
|
|
6578
|
+
if (!isNumericRatingKey(artistId)) {
|
|
6579
|
+
return sendSubsonicOk(reply, node('artist', {}, ''));
|
|
6580
|
+
}
|
|
6581
|
+
|
|
6497
6582
|
const context = repo.getAccountPlexContext(account.id);
|
|
6498
6583
|
const plexState = requiredPlexStateForSubsonic(reply, context, tokenCipher);
|
|
6499
6584
|
if (!plexState) {
|
|
@@ -6549,8 +6634,15 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6549
6634
|
}
|
|
6550
6635
|
|
|
6551
6636
|
const albumXml = finalAlbums
|
|
6552
|
-
.map((album) =>
|
|
6553
|
-
|
|
6637
|
+
.map((album) =>
|
|
6638
|
+
albumNode(
|
|
6639
|
+
'album',
|
|
6640
|
+
album,
|
|
6641
|
+
albumId3Attrs(album, artist.ratingKey, artist.title),
|
|
6642
|
+
artist.ratingKey,
|
|
6643
|
+
artist.title,
|
|
6644
|
+
),
|
|
6645
|
+
);
|
|
6554
6646
|
|
|
6555
6647
|
return sendSubsonicOk(
|
|
6556
6648
|
reply,
|
|
@@ -6583,6 +6675,10 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6583
6675
|
return sendSubsonicError(reply, 70, 'Missing artist id');
|
|
6584
6676
|
}
|
|
6585
6677
|
|
|
6678
|
+
if (!isNumericRatingKey(artistId)) {
|
|
6679
|
+
return sendSubsonicOk(reply, node('artistInfo'));
|
|
6680
|
+
}
|
|
6681
|
+
|
|
6586
6682
|
const context = repo.getAccountPlexContext(account.id);
|
|
6587
6683
|
const plexState = requiredPlexStateForSubsonic(reply, context, tokenCipher);
|
|
6588
6684
|
if (!plexState) {
|
|
@@ -6622,11 +6718,9 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6622
6718
|
const biography = artistBioFromPlex(artist);
|
|
6623
6719
|
const musicBrainzId = extractMusicBrainzArtistId(artist);
|
|
6624
6720
|
const children = [
|
|
6625
|
-
biography ? node('biography', {}, biography) :
|
|
6626
|
-
musicBrainzId ? node('musicBrainzId', {}, musicBrainzId) :
|
|
6627
|
-
]
|
|
6628
|
-
.filter(Boolean)
|
|
6629
|
-
.join('');
|
|
6721
|
+
biography ? node('biography', {}, biography) : null,
|
|
6722
|
+
musicBrainzId ? node('musicBrainzId', {}, musicBrainzId) : null,
|
|
6723
|
+
].filter(Boolean);
|
|
6630
6724
|
|
|
6631
6725
|
return sendSubsonicOk(reply, node('artistInfo', {}, children));
|
|
6632
6726
|
} catch (error) {
|
|
@@ -6646,6 +6740,10 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6646
6740
|
return sendSubsonicError(reply, 70, 'Missing artist id');
|
|
6647
6741
|
}
|
|
6648
6742
|
|
|
6743
|
+
if (!isNumericRatingKey(artistId)) {
|
|
6744
|
+
return sendSubsonicOk(reply, node('artistInfo2'));
|
|
6745
|
+
}
|
|
6746
|
+
|
|
6649
6747
|
const context = repo.getAccountPlexContext(account.id);
|
|
6650
6748
|
const plexState = requiredPlexStateForSubsonic(reply, context, tokenCipher);
|
|
6651
6749
|
if (!plexState) {
|
|
@@ -6685,11 +6783,9 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6685
6783
|
const biography = artistBioFromPlex(artist);
|
|
6686
6784
|
const musicBrainzId = extractMusicBrainzArtistId(artist);
|
|
6687
6785
|
const children = [
|
|
6688
|
-
biography ? node('biography', {}, biography) :
|
|
6689
|
-
musicBrainzId ? node('musicBrainzId', {}, musicBrainzId) :
|
|
6690
|
-
]
|
|
6691
|
-
.filter(Boolean)
|
|
6692
|
-
.join('');
|
|
6786
|
+
biography ? node('biography', {}, biography) : null,
|
|
6787
|
+
musicBrainzId ? node('musicBrainzId', {}, musicBrainzId) : null,
|
|
6788
|
+
].filter(Boolean);
|
|
6693
6789
|
|
|
6694
6790
|
return sendSubsonicOk(reply, node('artistInfo2', {}, children));
|
|
6695
6791
|
} catch (error) {
|
|
@@ -6709,6 +6805,10 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6709
6805
|
return sendSubsonicError(reply, 70, 'Missing album id');
|
|
6710
6806
|
}
|
|
6711
6807
|
|
|
6808
|
+
if (!isNumericRatingKey(albumId)) {
|
|
6809
|
+
return sendSubsonicOk(reply, node('album'));
|
|
6810
|
+
}
|
|
6811
|
+
|
|
6712
6812
|
const context = repo.getAccountPlexContext(account.id);
|
|
6713
6813
|
const plexState = requiredPlexStateForSubsonic(reply, context, tokenCipher);
|
|
6714
6814
|
if (!plexState) {
|
|
@@ -6742,20 +6842,22 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6742
6842
|
|
|
6743
6843
|
const totalDuration = sortedTracks.reduce((sum, track) => sum + durationSeconds(track.duration), 0);
|
|
6744
6844
|
|
|
6745
|
-
const
|
|
6746
|
-
.map((track) =>
|
|
6747
|
-
.join('');
|
|
6845
|
+
const songNodes = sortedTracks
|
|
6846
|
+
.map((track) => songNode(track, album.title, album.ratingKey, album));
|
|
6748
6847
|
|
|
6749
6848
|
return sendSubsonicOk(
|
|
6750
6849
|
reply,
|
|
6751
|
-
|
|
6850
|
+
albumNode(
|
|
6752
6851
|
'album',
|
|
6852
|
+
album,
|
|
6753
6853
|
{
|
|
6754
6854
|
...albumId3Attrs(album, album.parentRatingKey || null, album.parentTitle || null),
|
|
6755
6855
|
songCount: sortedTracks.length,
|
|
6756
6856
|
duration: totalDuration,
|
|
6757
6857
|
},
|
|
6758
|
-
|
|
6858
|
+
album.parentRatingKey || null,
|
|
6859
|
+
album.parentTitle || null,
|
|
6860
|
+
songNodes,
|
|
6759
6861
|
),
|
|
6760
6862
|
);
|
|
6761
6863
|
} catch (error) {
|
|
@@ -6804,15 +6906,20 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6804
6906
|
const children = folderResult.items
|
|
6805
6907
|
.map((item) => {
|
|
6806
6908
|
if (isLikelyPlexTrack(item)) {
|
|
6807
|
-
return
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6909
|
+
return songChildNode(
|
|
6910
|
+
item,
|
|
6911
|
+
item.parentTitle || null,
|
|
6912
|
+
item.parentRatingKey || null,
|
|
6913
|
+
null,
|
|
6914
|
+
{
|
|
6915
|
+
isDir: false,
|
|
6916
|
+
parent: currentDirectoryId,
|
|
6917
|
+
},
|
|
6918
|
+
);
|
|
6812
6919
|
}
|
|
6813
6920
|
|
|
6814
6921
|
if (isLikelyPlexAlbum(item)) {
|
|
6815
|
-
return
|
|
6922
|
+
return albumNode('child', item, {
|
|
6816
6923
|
...albumAttrs(item),
|
|
6817
6924
|
isDir: true,
|
|
6818
6925
|
parent: currentDirectoryId,
|
|
@@ -6837,8 +6944,7 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6837
6944
|
name: title,
|
|
6838
6945
|
});
|
|
6839
6946
|
})
|
|
6840
|
-
.filter(Boolean)
|
|
6841
|
-
.join('');
|
|
6947
|
+
.filter(Boolean);
|
|
6842
6948
|
|
|
6843
6949
|
const container = folderResult.container || {};
|
|
6844
6950
|
const directoryName = isRootFolder
|
|
@@ -6892,8 +6998,15 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6892
6998
|
}
|
|
6893
6999
|
|
|
6894
7000
|
const children = finalAlbums
|
|
6895
|
-
.map((album) =>
|
|
6896
|
-
|
|
7001
|
+
.map((album) =>
|
|
7002
|
+
albumNode(
|
|
7003
|
+
'child',
|
|
7004
|
+
album,
|
|
7005
|
+
albumAttrs(album, artist.ratingKey, artist.title),
|
|
7006
|
+
artist.ratingKey,
|
|
7007
|
+
artist.title,
|
|
7008
|
+
),
|
|
7009
|
+
);
|
|
6897
7010
|
|
|
6898
7011
|
return sendSubsonicOk(
|
|
6899
7012
|
reply,
|
|
@@ -6919,8 +7032,15 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6919
7032
|
if (fallback?.artist) {
|
|
6920
7033
|
const fallbackAlbums = fallback.albums || [];
|
|
6921
7034
|
const children = fallbackAlbums
|
|
6922
|
-
.map((album) =>
|
|
6923
|
-
|
|
7035
|
+
.map((album) =>
|
|
7036
|
+
albumNode(
|
|
7037
|
+
'child',
|
|
7038
|
+
album,
|
|
7039
|
+
albumAttrs(album, fallback.artist.ratingKey, fallback.artist.title),
|
|
7040
|
+
fallback.artist.ratingKey,
|
|
7041
|
+
fallback.artist.title,
|
|
7042
|
+
),
|
|
7043
|
+
);
|
|
6924
7044
|
|
|
6925
7045
|
return sendSubsonicOk(
|
|
6926
7046
|
reply,
|
|
@@ -6960,13 +7080,11 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6960
7080
|
|
|
6961
7081
|
const children = sortedTracks
|
|
6962
7082
|
.map((track) =>
|
|
6963
|
-
|
|
6964
|
-
...songAttrs(track, album.title, album.ratingKey, album),
|
|
7083
|
+
songChildNode(track, album.title, album.ratingKey, album, {
|
|
6965
7084
|
isDir: false,
|
|
6966
7085
|
parent: album.ratingKey,
|
|
6967
7086
|
}),
|
|
6968
|
-
)
|
|
6969
|
-
.join('');
|
|
7087
|
+
);
|
|
6970
7088
|
|
|
6971
7089
|
return sendSubsonicOk(
|
|
6972
7090
|
reply,
|
|
@@ -7041,13 +7159,16 @@ export async function buildServer(config = loadConfig()) {
|
|
|
7041
7159
|
});
|
|
7042
7160
|
const page = sliceAlbumPage(filtered, offset, size);
|
|
7043
7161
|
reply.header('x-total-count', String(filtered.length));
|
|
7044
|
-
const
|
|
7162
|
+
const albumNodes = page
|
|
7045
7163
|
.map((album) =>
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7164
|
+
albumNode(
|
|
7165
|
+
'album',
|
|
7166
|
+
album,
|
|
7167
|
+
containerName === 'albumList2' ? albumId3Attrs(album) : albumAttrs(album),
|
|
7168
|
+
),
|
|
7169
|
+
);
|
|
7049
7170
|
|
|
7050
|
-
return sendSubsonicOk(reply, node(containerName, {},
|
|
7171
|
+
return sendSubsonicOk(reply, node(containerName, {}, albumNodes));
|
|
7051
7172
|
} catch (error) {
|
|
7052
7173
|
request.log.error(error, 'Failed to load album list');
|
|
7053
7174
|
return sendSubsonicError(reply, 10, 'Failed to load album list');
|
package/src/subsonic-xml.js
CHANGED
|
@@ -27,12 +27,7 @@ const SERVER_TYPE = 'Plexsonic';
|
|
|
27
27
|
const SERVER_VERSION = APP_VERSION;
|
|
28
28
|
const OPEN_SUBSONIC = true;
|
|
29
29
|
|
|
30
|
-
const TOKEN_START = '\u0001';
|
|
31
|
-
const TOKEN_END = '\u0002';
|
|
32
|
-
const TOKEN_PATTERN = /\u0001(\d+)\u0002/g;
|
|
33
30
|
|
|
34
|
-
let nodeSeq = 0;
|
|
35
|
-
const nodeRegistry = new Map();
|
|
36
31
|
|
|
37
32
|
export function xmlEscape(value) {
|
|
38
33
|
const sanitized = sanitizeXmlText(String(value));
|
|
@@ -62,54 +57,19 @@ function sanitizeXmlText(value) {
|
|
|
62
57
|
return output;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
function storeNode(node) {
|
|
66
|
-
const id = ++nodeSeq;
|
|
67
|
-
nodeRegistry.set(id, node);
|
|
68
|
-
return `${TOKEN_START}${id}${TOKEN_END}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parseTokenText(text) {
|
|
72
|
-
const out = [];
|
|
73
|
-
let index = 0;
|
|
74
|
-
let match;
|
|
75
|
-
|
|
76
|
-
TOKEN_PATTERN.lastIndex = 0;
|
|
77
|
-
while ((match = TOKEN_PATTERN.exec(text)) !== null) {
|
|
78
|
-
const before = text.slice(index, match.index);
|
|
79
|
-
if (before) {
|
|
80
|
-
out.push(before);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const nodeId = Number(match[1]);
|
|
84
|
-
const tokenNode = nodeRegistry.get(nodeId);
|
|
85
|
-
if (tokenNode) {
|
|
86
|
-
out.push(tokenNode);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
index = match.index + match[0].length;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const tail = text.slice(index);
|
|
93
|
-
if (tail) {
|
|
94
|
-
out.push(tail);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return out;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
60
|
function normalizeChildren(input) {
|
|
101
|
-
if (input == null) {
|
|
61
|
+
if (input == null || input === '') {
|
|
102
62
|
return [];
|
|
103
63
|
}
|
|
104
64
|
if (Array.isArray(input)) {
|
|
105
65
|
return input.flatMap((item) => normalizeChildren(item));
|
|
106
66
|
}
|
|
107
|
-
if (typeof input === 'string') {
|
|
108
|
-
return parseTokenText(input);
|
|
109
|
-
}
|
|
110
67
|
if (typeof input === 'object' && input.kind === 'node') {
|
|
111
68
|
return [input];
|
|
112
69
|
}
|
|
70
|
+
if (typeof input === 'string') {
|
|
71
|
+
return [input];
|
|
72
|
+
}
|
|
113
73
|
return [String(input)];
|
|
114
74
|
}
|
|
115
75
|
|
|
@@ -137,9 +97,7 @@ function renderXmlPart(part) {
|
|
|
137
97
|
return `<${part.name}${attrs}>${inner}</${part.name}>`;
|
|
138
98
|
}
|
|
139
99
|
|
|
140
|
-
|
|
141
|
-
return normalizeChildren(inner).map(renderXmlPart).join('');
|
|
142
|
-
}
|
|
100
|
+
|
|
143
101
|
|
|
144
102
|
const NUMERIC_ATTRS = new Set([
|
|
145
103
|
'code',
|
|
@@ -200,6 +158,7 @@ const ARRAY_CHILDREN_BY_PARENT = {
|
|
|
200
158
|
index: new Set(['artist']),
|
|
201
159
|
artist: new Set(['album']),
|
|
202
160
|
album: new Set(['song']),
|
|
161
|
+
albumArtists: new Set(['artist']),
|
|
203
162
|
songsByGenre: new Set(['song']),
|
|
204
163
|
randomSongs: new Set(['song']),
|
|
205
164
|
topSongs: new Set(['song']),
|
|
@@ -304,20 +263,24 @@ function nodeToJson(node) {
|
|
|
304
263
|
out[child.name] = value;
|
|
305
264
|
}
|
|
306
265
|
|
|
307
|
-
const arrayChildren = ARRAY_CHILDREN_BY_PARENT[node.name];
|
|
308
|
-
if (arrayChildren) {
|
|
309
|
-
for (const childName of arrayChildren) {
|
|
310
|
-
if (!Object.prototype.hasOwnProperty.call(out, childName)) {
|
|
311
|
-
out[childName] = [];
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
266
|
const keys = Object.keys(out);
|
|
317
267
|
if (keys.length === 1 && keys[0] === 'value') {
|
|
318
268
|
return out.value;
|
|
319
269
|
}
|
|
320
270
|
|
|
271
|
+
if (node.name === 'artists' || node.name === 'albumArtists') {
|
|
272
|
+
if (node.attrs?.flatten === 'true') {
|
|
273
|
+
const artistValue = out.artist;
|
|
274
|
+
if (Array.isArray(artistValue)) {
|
|
275
|
+
return artistValue;
|
|
276
|
+
}
|
|
277
|
+
if (artistValue != null) {
|
|
278
|
+
return [artistValue];
|
|
279
|
+
}
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
321
284
|
if (node.name === 'openSubsonicExtensions') {
|
|
322
285
|
const extensions = out.openSubsonicExtension;
|
|
323
286
|
if (Array.isArray(extensions)) {
|
|
@@ -347,23 +310,23 @@ function subsonicRoot(status, children = []) {
|
|
|
347
310
|
}
|
|
348
311
|
|
|
349
312
|
export function emptyNode(name, attrs = {}) {
|
|
350
|
-
return
|
|
313
|
+
return {
|
|
351
314
|
kind: 'node',
|
|
352
315
|
name,
|
|
353
316
|
attrs,
|
|
354
317
|
children: [],
|
|
355
318
|
selfClosing: true,
|
|
356
|
-
}
|
|
319
|
+
};
|
|
357
320
|
}
|
|
358
321
|
|
|
359
|
-
export function node(name, attrs = {}, inner =
|
|
360
|
-
return
|
|
322
|
+
export function node(name, attrs = {}, inner = null) {
|
|
323
|
+
return {
|
|
361
324
|
kind: 'node',
|
|
362
325
|
name,
|
|
363
326
|
attrs,
|
|
364
327
|
children: normalizeChildren(inner),
|
|
365
328
|
selfClosing: false,
|
|
366
|
-
}
|
|
329
|
+
};
|
|
367
330
|
}
|
|
368
331
|
|
|
369
332
|
export function okResponse(inner = '') {
|
|
@@ -410,16 +373,3 @@ export function failedResponseJson(code, message) {
|
|
|
410
373
|
return { 'subsonic-response': json };
|
|
411
374
|
}
|
|
412
375
|
|
|
413
|
-
// Backward-compatible export for older callsites.
|
|
414
|
-
export function responseJson(xml) {
|
|
415
|
-
// Kept only for compatibility during migration; no XML parsing path is used by server responses.
|
|
416
|
-
return {
|
|
417
|
-
'subsonic-response': {
|
|
418
|
-
status: 'failed',
|
|
419
|
-
error: {
|
|
420
|
-
code: 10,
|
|
421
|
-
message: 'responseJson(xml) is deprecated',
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
};
|
|
425
|
-
}
|