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 +1 -1
- package/src/server.js +71 -2
- package/src/subsonic-xml.js +26 -34
package/package.json
CHANGED
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
|
-
|
|
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',
|
package/src/subsonic-xml.js
CHANGED
|
@@ -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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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(
|
|
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)) {
|