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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plexsonic",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "PlexMusic to OpenSubsonic bridge",
5
5
  "main": "./src/index.js",
6
6
  "bin": {
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
- return sendSubsonicOk(reply, node('artistInfo'));
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
- return sendSubsonicOk(reply, node('artistInfo2'));
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) => {
@@ -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)) {