plexsonic 0.1.5 → 0.1.6
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 +215 -2
- package/src/subsonic-xml.js +5 -0
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -868,6 +868,111 @@ function isPlexLiked(value) {
|
|
|
868
868
|
return normalized != null && normalized >= 2 && normalized % 2 === 0;
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
+
function normalizePlainText(value) {
|
|
872
|
+
return String(value || '')
|
|
873
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
874
|
+
.replace(/<\/p>/gi, '\n')
|
|
875
|
+
.replace(/<[^>]*>/g, '')
|
|
876
|
+
.replace(/\r\n/g, '\n')
|
|
877
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
878
|
+
.trim();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function asArray(value) {
|
|
882
|
+
if (Array.isArray(value)) {
|
|
883
|
+
return value;
|
|
884
|
+
}
|
|
885
|
+
if (value == null) {
|
|
886
|
+
return [];
|
|
887
|
+
}
|
|
888
|
+
return [value];
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function plexGuidIds(item) {
|
|
892
|
+
const candidates = [];
|
|
893
|
+
|
|
894
|
+
for (const guid of asArray(item?.Guid)) {
|
|
895
|
+
if (typeof guid === 'string') {
|
|
896
|
+
candidates.push(guid);
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (guid && typeof guid === 'object' && typeof guid.id === 'string') {
|
|
900
|
+
candidates.push(guid.id);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
for (const raw of [item?.guid, item?.guids]) {
|
|
905
|
+
if (typeof raw === 'string') {
|
|
906
|
+
candidates.push(raw);
|
|
907
|
+
} else if (Array.isArray(raw)) {
|
|
908
|
+
for (const entry of raw) {
|
|
909
|
+
if (typeof entry === 'string') {
|
|
910
|
+
candidates.push(entry);
|
|
911
|
+
} else if (entry && typeof entry === 'object' && typeof entry.id === 'string') {
|
|
912
|
+
candidates.push(entry.id);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return uniqueNonEmptyValues(candidates);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function extractMusicBrainzArtistId(item) {
|
|
922
|
+
const guidIds = plexGuidIds(item);
|
|
923
|
+
const uuidPattern = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/i;
|
|
924
|
+
|
|
925
|
+
for (const guid of guidIds) {
|
|
926
|
+
const lower = safeLower(guid);
|
|
927
|
+
|
|
928
|
+
if (lower.startsWith('mbid://')) {
|
|
929
|
+
const id = guid.slice('mbid://'.length).split(/[/?#]/, 1)[0].trim();
|
|
930
|
+
if (id) {
|
|
931
|
+
return id;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (lower.startsWith('musicbrainz://')) {
|
|
936
|
+
const id = guid
|
|
937
|
+
.slice('musicbrainz://'.length)
|
|
938
|
+
.replace(/^artist\//i, '')
|
|
939
|
+
.split(/[?#]/, 1)[0]
|
|
940
|
+
.replace(/^\/+/, '')
|
|
941
|
+
.trim();
|
|
942
|
+
if (id) {
|
|
943
|
+
return id;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (lower.includes('musicbrainz.org/artist/')) {
|
|
948
|
+
const match = guid.match(/musicbrainz\.org\/artist\/([^/?#]+)/i);
|
|
949
|
+
if (match?.[1]) {
|
|
950
|
+
return match[1];
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (lower.includes('musicbrainz')) {
|
|
955
|
+
const uuid = guid.match(uuidPattern)?.[1];
|
|
956
|
+
if (uuid) {
|
|
957
|
+
return uuid;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return '';
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function artistBioFromPlex(item) {
|
|
966
|
+
return firstNonEmptyText(
|
|
967
|
+
[
|
|
968
|
+
normalizePlainText(item?.summary),
|
|
969
|
+
normalizePlainText(item?.tagline),
|
|
970
|
+
normalizePlainText(item?.description),
|
|
971
|
+
],
|
|
972
|
+
'',
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
871
976
|
function subsonicRatingToPlexRating(value, { liked = false } = {}) {
|
|
872
977
|
const rating = Number.parseInt(String(value ?? ''), 10);
|
|
873
978
|
if (!Number.isFinite(rating) || rating <= 0) {
|
|
@@ -6131,7 +6236,61 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6131
6236
|
return;
|
|
6132
6237
|
}
|
|
6133
6238
|
|
|
6134
|
-
|
|
6239
|
+
const artistId = String(getRequestParam(request, 'id') || '').trim();
|
|
6240
|
+
if (!artistId) {
|
|
6241
|
+
return sendSubsonicError(reply, 70, 'Missing artist id');
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
const context = repo.getAccountPlexContext(account.id);
|
|
6245
|
+
const plexState = requiredPlexStateForSubsonic(reply, context, tokenCipher);
|
|
6246
|
+
if (!plexState) {
|
|
6247
|
+
return;
|
|
6248
|
+
}
|
|
6249
|
+
|
|
6250
|
+
try {
|
|
6251
|
+
let artist = null;
|
|
6252
|
+
try {
|
|
6253
|
+
artist = await getArtist({
|
|
6254
|
+
baseUrl: plexState.baseUrl,
|
|
6255
|
+
plexToken: plexState.plexToken,
|
|
6256
|
+
artistId,
|
|
6257
|
+
});
|
|
6258
|
+
} catch (error) {
|
|
6259
|
+
if (!isPlexNotFoundError(error)) {
|
|
6260
|
+
throw error;
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
if (!artist) {
|
|
6265
|
+
const fallback = await resolveArtistFromCachedLibrary({
|
|
6266
|
+
accountId: account.id,
|
|
6267
|
+
plexState,
|
|
6268
|
+
request,
|
|
6269
|
+
artistId,
|
|
6270
|
+
});
|
|
6271
|
+
if (fallback?.artist) {
|
|
6272
|
+
artist = fallback.artist;
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6276
|
+
if (!artist) {
|
|
6277
|
+
return sendSubsonicError(reply, 70, 'Artist not found');
|
|
6278
|
+
}
|
|
6279
|
+
|
|
6280
|
+
const biography = artistBioFromPlex(artist);
|
|
6281
|
+
const musicBrainzId = extractMusicBrainzArtistId(artist);
|
|
6282
|
+
const children = [
|
|
6283
|
+
biography ? node('biography', {}, biography) : '',
|
|
6284
|
+
musicBrainzId ? node('musicBrainzId', {}, musicBrainzId) : '',
|
|
6285
|
+
]
|
|
6286
|
+
.filter(Boolean)
|
|
6287
|
+
.join('');
|
|
6288
|
+
|
|
6289
|
+
return sendSubsonicOk(reply, node('artistInfo', {}, children));
|
|
6290
|
+
} catch (error) {
|
|
6291
|
+
request.log.error(error, 'Failed to load artist info');
|
|
6292
|
+
return sendSubsonicError(reply, 10, 'Failed to load artist info');
|
|
6293
|
+
}
|
|
6135
6294
|
});
|
|
6136
6295
|
|
|
6137
6296
|
app.get('/rest/getArtistInfo2.view', async (request, reply) => {
|
|
@@ -6140,7 +6299,61 @@ export async function buildServer(config = loadConfig()) {
|
|
|
6140
6299
|
return;
|
|
6141
6300
|
}
|
|
6142
6301
|
|
|
6143
|
-
|
|
6302
|
+
const artistId = String(getRequestParam(request, 'id') || '').trim();
|
|
6303
|
+
if (!artistId) {
|
|
6304
|
+
return sendSubsonicError(reply, 70, 'Missing artist id');
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
const context = repo.getAccountPlexContext(account.id);
|
|
6308
|
+
const plexState = requiredPlexStateForSubsonic(reply, context, tokenCipher);
|
|
6309
|
+
if (!plexState) {
|
|
6310
|
+
return;
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6313
|
+
try {
|
|
6314
|
+
let artist = null;
|
|
6315
|
+
try {
|
|
6316
|
+
artist = await getArtist({
|
|
6317
|
+
baseUrl: plexState.baseUrl,
|
|
6318
|
+
plexToken: plexState.plexToken,
|
|
6319
|
+
artistId,
|
|
6320
|
+
});
|
|
6321
|
+
} catch (error) {
|
|
6322
|
+
if (!isPlexNotFoundError(error)) {
|
|
6323
|
+
throw error;
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
|
|
6327
|
+
if (!artist) {
|
|
6328
|
+
const fallback = await resolveArtistFromCachedLibrary({
|
|
6329
|
+
accountId: account.id,
|
|
6330
|
+
plexState,
|
|
6331
|
+
request,
|
|
6332
|
+
artistId,
|
|
6333
|
+
});
|
|
6334
|
+
if (fallback?.artist) {
|
|
6335
|
+
artist = fallback.artist;
|
|
6336
|
+
}
|
|
6337
|
+
}
|
|
6338
|
+
|
|
6339
|
+
if (!artist) {
|
|
6340
|
+
return sendSubsonicError(reply, 70, 'Artist not found');
|
|
6341
|
+
}
|
|
6342
|
+
|
|
6343
|
+
const biography = artistBioFromPlex(artist);
|
|
6344
|
+
const musicBrainzId = extractMusicBrainzArtistId(artist);
|
|
6345
|
+
const children = [
|
|
6346
|
+
biography ? node('biography', {}, biography) : '',
|
|
6347
|
+
musicBrainzId ? node('musicBrainzId', {}, musicBrainzId) : '',
|
|
6348
|
+
]
|
|
6349
|
+
.filter(Boolean)
|
|
6350
|
+
.join('');
|
|
6351
|
+
|
|
6352
|
+
return sendSubsonicOk(reply, node('artistInfo2', {}, children));
|
|
6353
|
+
} catch (error) {
|
|
6354
|
+
request.log.error(error, 'Failed to load artist info2');
|
|
6355
|
+
return sendSubsonicError(reply, 10, 'Failed to load artist info');
|
|
6356
|
+
}
|
|
6144
6357
|
});
|
|
6145
6358
|
|
|
6146
6359
|
app.get('/rest/getAlbum.view', async (request, reply) => {
|
package/src/subsonic-xml.js
CHANGED
|
@@ -321,6 +321,11 @@ function nodeToJson(node) {
|
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
const keys = Object.keys(out);
|
|
325
|
+
if (keys.length === 1 && keys[0] === 'value') {
|
|
326
|
+
return out.value;
|
|
327
|
+
}
|
|
328
|
+
|
|
324
329
|
if (node.name === 'openSubsonicExtensions') {
|
|
325
330
|
const extensions = out.openSubsonicExtension;
|
|
326
331
|
if (Array.isArray(extensions)) {
|