plexsonic 0.1.8 → 0.1.9

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.8",
3
+ "version": "0.1.9",
4
4
  "description": "PlexMusic to OpenSubsonic bridge",
5
5
  "main": "./src/index.js",
6
6
  "bin": {
package/src/server.js CHANGED
@@ -777,6 +777,17 @@ function partFromTrack(track) {
777
777
  return Array.isArray(media?.Part) ? media.Part[0] : null;
778
778
  }
779
779
 
780
+ function audioStreamFromTrack(track) {
781
+ const part = partFromTrack(track);
782
+ const streams = Array.isArray(part?.Stream) ? part.Stream : [];
783
+ if (streams.length === 0) {
784
+ return null;
785
+ }
786
+
787
+ const audioStream = streams.find((stream) => parseNonNegativeInt(stream?.streamType, -1) === 2);
788
+ return audioStream || streams[0] || null;
789
+ }
790
+
780
791
  function detectAudioSuffix(track) {
781
792
  const media = mediaFromTrack(track);
782
793
  const container = String(media?.container || '').toLowerCase();
@@ -1138,6 +1149,7 @@ function deriveAlbumsFromTracks(tracks, fallbackArtistId, fallbackArtistName) {
1138
1149
  function songAttrs(track, albumTitle = null, albumCoverArt = null, albumMetadata = null) {
1139
1150
  const media = mediaFromTrack(track);
1140
1151
  const part = partFromTrack(track);
1152
+ const audioStream = audioStreamFromTrack(track);
1141
1153
  const trackId = String(track?.ratingKey || '').trim();
1142
1154
  const albumId = firstNonEmptyText(
1143
1155
  [track?.parentRatingKey, typeof albumCoverArt === 'string' ? albumCoverArt : null],
@@ -1164,6 +1176,9 @@ function songAttrs(track, albumTitle = null, albumCoverArt = null, albumMetadata
1164
1176
  const genreTags = genreTagsRaw.length > 0 ? genreTagsRaw : albumGenreTags;
1165
1177
  const genre = genreTags[0] || undefined;
1166
1178
  const genres = genreTags.length > 0 ? genreTags.join('; ') : undefined;
1179
+ const styleValues = metadataFieldValues([track, albumMetadata], ['Style', 'style']);
1180
+ const style = styleValues[0] || undefined;
1181
+ const styles = styleValues.length > 0 ? styleValues.join('; ') : undefined;
1167
1182
  const discNumber = parsePositiveInt(track?.parentIndex ?? track?.discNumber, 0) || undefined;
1168
1183
  const discSubtitle = firstNonEmptyText(
1169
1184
  [track?.parentSubtitle, track?.discSubtitle, track?.parentOriginalTitle],
@@ -1177,19 +1192,45 @@ function songAttrs(track, albumTitle = null, albumCoverArt = null, albumMetadata
1177
1192
  undefined,
1178
1193
  );
1179
1194
  const country = metadataFieldText([track, albumMetadata], ['Country', 'country']);
1180
- const style = metadataFieldText([track, albumMetadata], ['Style', 'style']);
1181
1195
  const moodValues = metadataFieldValues([track, albumMetadata], ['Mood', 'mood']);
1182
1196
  const mood = moodValues[0] || undefined;
1183
1197
  const moods = moodValues.length > 0 ? moodValues.join('; ') : undefined;
1198
+ const recordLabelValues = metadataFieldValues(
1199
+ [track, albumMetadata],
1200
+ ['RecordLabel', 'recordLabel', 'recordlabel', 'Label', 'label', 'Studio', 'studio'],
1201
+ );
1202
+ const recordLabel = recordLabelValues[0] || undefined;
1203
+ const recordLabels = recordLabelValues.length > 0 ? recordLabelValues.join('; ') : undefined;
1184
1204
  const language =
1185
1205
  metadataFieldText([track, albumMetadata], ['Language', 'language', 'Lang', 'lang']) || streamLanguage;
1186
1206
  const albumType = metadataFieldText(
1187
1207
  [albumMetadata, track],
1188
1208
  ['albumType', 'AlbumType', 'subtype', 'subType', 'parentSubtype', 'format'],
1189
1209
  );
1210
+ const compilationValues = metadataFieldValues(
1211
+ [track, albumMetadata],
1212
+ ['Compilation', 'compilation', 'isCompilation', 'iscompilation'],
1213
+ );
1214
+ const soundtrackValues = metadataFieldValues(
1215
+ [track, albumMetadata],
1216
+ ['Soundtrack', 'soundtrack', 'isSoundtrack', 'issoundtrack'],
1217
+ );
1218
+ const compilation = compilationValues.length > 0
1219
+ ? parseBooleanLike(compilationValues[0], false)
1220
+ : safeLower(albumType).includes('compilation');
1221
+ const soundtrack = soundtrackValues.length > 0
1222
+ ? parseBooleanLike(soundtrackValues[0], false)
1223
+ : safeLower(albumType).includes('soundtrack');
1224
+ const sampleRate = parsePositiveInt(
1225
+ audioStream?.samplingRate ?? audioStream?.sampleRate ?? audioStream?.audioSamplingRate,
1226
+ 0,
1227
+ ) || undefined;
1228
+ const bitDepth = parsePositiveInt(audioStream?.bitDepth ?? audioStream?.bitsPerSample, 0) || undefined;
1229
+ const path = firstNonEmptyText([part?.file], undefined);
1190
1230
  const trackNumber = parsePositiveInt(track?.index ?? track?.track, 0);
1191
1231
  const playCount = parseNonNegativeInt(track?.viewCount ?? track?.playCount, 0);
1192
1232
  const played = toIsoFromEpochSeconds(track?.lastViewedAt);
1233
+ const year = parsePositiveInt(track?.year ?? track?.parentYear ?? albumMetadata?.year, 0) || undefined;
1193
1234
 
1194
1235
  return {
1195
1236
  id: trackId,
@@ -1210,16 +1251,24 @@ function songAttrs(track, albumTitle = null, albumCoverArt = null, albumMetadata
1210
1251
  suffix: detectAudioSuffix(track),
1211
1252
  size: part?.size,
1212
1253
  bitRate: media?.bitrate,
1254
+ sampleRate,
1255
+ bitDepth,
1256
+ path,
1213
1257
  coverArt,
1214
1258
  genre,
1215
1259
  genres,
1216
1260
  country,
1217
1261
  style,
1262
+ styles,
1218
1263
  mood,
1219
1264
  moods,
1265
+ recordLabel,
1266
+ recordLabels,
1220
1267
  language,
1221
1268
  albumType,
1222
- year: track.year,
1269
+ compilation: compilation || undefined,
1270
+ soundtrack: soundtrack || undefined,
1271
+ year,
1223
1272
  played,
1224
1273
  created: toIsoFromEpochSeconds(track.addedAt),
1225
1274
  playCount: playCount || undefined,
@@ -1319,6 +1368,9 @@ async function hydrateTracksWithGenre({ baseUrl, plexToken, tracks, request = nu
1319
1368
  }
1320
1369
 
1321
1370
  const merged = { ...track };
1371
+ if (!merged.Media && detailed?.Media) {
1372
+ merged.Media = detailed.Media;
1373
+ }
1322
1374
  if (Array.isArray(detailed?.Genre) && detailed.Genre.length > 0) {
1323
1375
  merged.Genre = detailed.Genre;
1324
1376
  }
@@ -1330,12 +1382,29 @@ async function hydrateTracksWithGenre({ baseUrl, plexToken, tracks, request = nu
1330
1382
  'country',
1331
1383
  'Style',
1332
1384
  'style',
1385
+ 'Compilation',
1386
+ 'compilation',
1387
+ 'isCompilation',
1388
+ 'iscompilation',
1389
+ 'Soundtrack',
1390
+ 'soundtrack',
1391
+ 'isSoundtrack',
1392
+ 'issoundtrack',
1393
+ 'RecordLabel',
1394
+ 'recordLabel',
1395
+ 'recordlabel',
1396
+ 'Label',
1397
+ 'label',
1398
+ 'Studio',
1399
+ 'studio',
1333
1400
  'Mood',
1334
1401
  'mood',
1335
1402
  'Language',
1336
1403
  'language',
1337
1404
  'Lang',
1338
1405
  'lang',
1406
+ 'year',
1407
+ 'parentYear',
1339
1408
  'albumType',
1340
1409
  'AlbumType',
1341
1410
  'parentSubtype',
@@ -161,6 +161,8 @@ const NUMERIC_ATTRS = new Set([
161
161
  'position',
162
162
  'lastModified',
163
163
  'start',
164
+ 'sampleRate',
165
+ 'bitDepth',
164
166
  ]);
165
167
 
166
168
  const BOOLEAN_ATTRS = new Set([
@@ -185,6 +187,8 @@ const BOOLEAN_ATTRS = new Set([
185
187
  'smart',
186
188
  'synced',
187
189
  'readonly',
190
+ 'compilation',
191
+ 'soundtrack',
188
192
  ]);
189
193
 
190
194
  const ARRAY_CHILDREN_BY_PARENT = {
@@ -220,46 +224,34 @@ function shouldUseArray(parentName, childName) {
220
224
  }
221
225
 
222
226
  function coerceAttrValue(key, value) {
223
- if (key === 'genres') {
224
- const genreNames = (() => {
225
- if (Array.isArray(value)) {
226
- return value
227
- .flatMap((entry) => {
228
- if (entry == null) {
229
- return [];
230
- }
231
- if (typeof entry === 'string') {
232
- return [entry];
233
- }
234
- if (typeof entry === 'object') {
235
- return [String(entry.name || entry.tag || entry.value || entry.title || '').trim()];
236
- }
237
- return [String(entry).trim()];
238
- })
239
- .filter(Boolean);
240
- }
241
-
242
- const raw = String(value || '').trim();
243
- if (!raw) {
244
- return [];
245
- }
246
- const parts = raw.includes(';') ? raw.split(';') : raw.split(',');
247
- return parts.map((part) => part.trim()).filter(Boolean);
248
- })();
249
-
250
- return genreNames.map((name) => ({ name }));
251
- }
252
-
253
- if (key === 'moods') {
254
- if (Array.isArray(value)) {
255
- return value.map((entry) => String(entry || '').trim()).filter(Boolean);
227
+ const splitList = (rawValue) => {
228
+ if (Array.isArray(rawValue)) {
229
+ return rawValue.map((entry) => String(entry || '').trim()).filter(Boolean);
256
230
  }
257
- const raw = String(value || '').trim();
231
+ const raw = String(rawValue || '').trim();
258
232
  if (!raw) {
259
233
  return [];
260
234
  }
261
235
  const parts = raw.includes(';') ? raw.split(';') : raw.split(',');
262
236
  return parts.map((part) => part.trim()).filter(Boolean);
237
+ };
238
+
239
+ if (key === 'genres') {
240
+ const genreNames = splitList(value);
241
+ return genreNames.map((name) => ({ name }));
242
+ }
243
+
244
+ if (key === 'moods') {
245
+ return splitList(value);
246
+ }
247
+
248
+ if (key === 'styles') {
249
+ return splitList(value);
250
+ }
251
+
252
+ if (key === 'recordLabels') {
253
+ const labels = splitList(value);
254
+ return labels.map((name) => ({ name }));
263
255
  }
264
256
 
265
257
  if (BOOLEAN_ATTRS.has(key)) {