plexsonic 0.1.12 → 0.1.20
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/.env.example +4 -0
- package/README.md +26 -0
- package/package.json +1 -1
- package/src/config.js +27 -0
- package/src/db.js +236 -0
- package/src/plex.js +119 -24
- package/src/server.js +6496 -2509
- package/src/subsonic-xml.js +234 -261
package/src/subsonic-xml.js
CHANGED
|
@@ -21,23 +21,61 @@
|
|
|
21
21
|
import { APP_VERSION } from './version.js';
|
|
22
22
|
|
|
23
23
|
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
24
|
+
const ROOT_NAME = 'subsonic-response';
|
|
24
25
|
const XMLNS = 'http://subsonic.org/restapi';
|
|
25
26
|
const API_VERSION = '1.16.1';
|
|
26
27
|
const SERVER_TYPE = 'Plexsonic';
|
|
27
28
|
const SERVER_VERSION = APP_VERSION;
|
|
28
29
|
const OPEN_SUBSONIC = true;
|
|
29
30
|
|
|
31
|
+
const ROOT_ATTR_KEYS = new Set([
|
|
32
|
+
'status',
|
|
33
|
+
'version',
|
|
34
|
+
'type',
|
|
35
|
+
'serverVersion',
|
|
36
|
+
'openSubsonic',
|
|
37
|
+
'xmlns',
|
|
38
|
+
]);
|
|
30
39
|
|
|
40
|
+
const XML_ATTR_LIST_KEYS = new Set(['genres', 'moods', 'styles', 'recordLabels']);
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
// Dedicated XML schema rules: these define repeated child elements by parent element.
|
|
43
|
+
const ARRAY_CHILDREN_BY_PARENT = {
|
|
44
|
+
openSubsonicExtensions: new Set(['openSubsonicExtension']),
|
|
45
|
+
albumList: new Set(['album']),
|
|
46
|
+
albumList2: new Set(['album']),
|
|
47
|
+
artists: new Set(['index']),
|
|
48
|
+
indexes: new Set(['index']),
|
|
49
|
+
index: new Set(['artist']),
|
|
50
|
+
artist: new Set(['album']),
|
|
51
|
+
album: new Set(['song']),
|
|
52
|
+
albumArtists: new Set(['artist']),
|
|
53
|
+
songsByGenre: new Set(['song']),
|
|
54
|
+
randomSongs: new Set(['song']),
|
|
55
|
+
topSongs: new Set(['song']),
|
|
56
|
+
similarSongs: new Set(['song']),
|
|
57
|
+
similarSongs2: new Set(['song']),
|
|
58
|
+
searchResult: new Set(['artist', 'album', 'match']),
|
|
59
|
+
searchResult2: new Set(['artist', 'album', 'song']),
|
|
60
|
+
searchResult3: new Set(['artist', 'album', 'song']),
|
|
61
|
+
musicFolders: new Set(['musicFolder']),
|
|
62
|
+
genres: new Set(['genre']),
|
|
63
|
+
playlists: new Set(['playlist']),
|
|
64
|
+
directory: new Set(['child']),
|
|
65
|
+
playlist: new Set(['entry']),
|
|
66
|
+
playQueue: new Set(['entry']),
|
|
67
|
+
nowPlaying: new Set(['entry']),
|
|
68
|
+
lyricsList: new Set(['structuredLyrics']),
|
|
69
|
+
structuredLyrics: new Set(['line']),
|
|
70
|
+
starred: new Set(['artist', 'album', 'song']),
|
|
71
|
+
starred2: new Set(['artist', 'album', 'song']),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Dedicated scalar-child rules for XML where values should be child elements, not attributes.
|
|
75
|
+
const SCALAR_CHILDREN_BY_PARENT = {
|
|
76
|
+
artistInfo: new Set(['biography', 'musicBrainzId']),
|
|
77
|
+
artistInfo2: new Set(['biography', 'musicBrainzId']),
|
|
78
|
+
};
|
|
41
79
|
|
|
42
80
|
function sanitizeXmlText(value) {
|
|
43
81
|
let output = '';
|
|
@@ -57,328 +95,263 @@ function sanitizeXmlText(value) {
|
|
|
57
95
|
return output;
|
|
58
96
|
}
|
|
59
97
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
98
|
+
export function xmlEscape(value) {
|
|
99
|
+
const sanitized = sanitizeXmlText(String(value));
|
|
100
|
+
return sanitized
|
|
101
|
+
.replaceAll('&', '&')
|
|
102
|
+
.replaceAll('<', '<')
|
|
103
|
+
.replaceAll('>', '>')
|
|
104
|
+
.replaceAll('"', '"')
|
|
105
|
+
.replaceAll("'", ''');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function splitList(rawValue) {
|
|
109
|
+
if (Array.isArray(rawValue)) {
|
|
110
|
+
return rawValue.map((entry) => String(entry || '').trim()).filter(Boolean);
|
|
69
111
|
}
|
|
70
|
-
|
|
71
|
-
|
|
112
|
+
|
|
113
|
+
const raw = String(rawValue || '').trim();
|
|
114
|
+
if (!raw) {
|
|
115
|
+
return [];
|
|
72
116
|
}
|
|
73
|
-
return [String(input)];
|
|
74
|
-
}
|
|
75
117
|
|
|
76
|
-
|
|
77
|
-
return
|
|
78
|
-
.filter(([, value]) => value !== undefined && value !== null)
|
|
79
|
-
.map(([key, value]) => ` ${key}="${xmlEscape(value)}"`)
|
|
80
|
-
.join('');
|
|
118
|
+
const parts = raw.includes(';') ? raw.split(';') : raw.split(',');
|
|
119
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
81
120
|
}
|
|
82
121
|
|
|
83
|
-
function
|
|
84
|
-
if (
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
if (!part || part.kind !== 'node') {
|
|
88
|
-
return '';
|
|
122
|
+
function toXmlAttrValue(key, value) {
|
|
123
|
+
if (value === undefined || value === null) {
|
|
124
|
+
return null;
|
|
89
125
|
}
|
|
90
126
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
127
|
+
if (key === 'genres' || key === 'recordLabels') {
|
|
128
|
+
const names = Array.isArray(value)
|
|
129
|
+
? value
|
|
130
|
+
.map((entry) => {
|
|
131
|
+
if (entry && typeof entry === 'object') {
|
|
132
|
+
return String(entry.name || '').trim();
|
|
133
|
+
}
|
|
134
|
+
return String(entry || '').trim();
|
|
135
|
+
})
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
: splitList(value);
|
|
138
|
+
return names.join('; ');
|
|
94
139
|
}
|
|
95
140
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
141
|
+
if (key === 'moods' || key === 'styles') {
|
|
142
|
+
return splitList(value).join('; ');
|
|
143
|
+
}
|
|
100
144
|
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
return value.map((entry) => String(entry)).join(',');
|
|
147
|
+
}
|
|
101
148
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'offset',
|
|
106
|
-
'duration',
|
|
107
|
-
'songCount',
|
|
108
|
-
'albumCount',
|
|
109
|
-
'track',
|
|
110
|
-
'discNumber',
|
|
111
|
-
'bitRate',
|
|
112
|
-
'size',
|
|
113
|
-
'year',
|
|
114
|
-
'userRating',
|
|
115
|
-
'playCount',
|
|
116
|
-
'leafCount',
|
|
117
|
-
'leafCountAdded',
|
|
118
|
-
'leafCountRequested',
|
|
119
|
-
'position',
|
|
120
|
-
'lastModified',
|
|
121
|
-
'start',
|
|
122
|
-
'sampleRate',
|
|
123
|
-
'bitDepth',
|
|
124
|
-
]);
|
|
149
|
+
if (typeof value === 'boolean') {
|
|
150
|
+
return value ? 'true' : 'false';
|
|
151
|
+
}
|
|
125
152
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'valid',
|
|
129
|
-
'scanning',
|
|
130
|
-
'isDir',
|
|
131
|
-
'public',
|
|
132
|
-
'scrobblingEnabled',
|
|
133
|
-
'adminRole',
|
|
134
|
-
'settingsRole',
|
|
135
|
-
'downloadRole',
|
|
136
|
-
'uploadRole',
|
|
137
|
-
'playlistRole',
|
|
138
|
-
'coverArtRole',
|
|
139
|
-
'commentRole',
|
|
140
|
-
'podcastRole',
|
|
141
|
-
'streamRole',
|
|
142
|
-
'jukeboxRole',
|
|
143
|
-
'shareRole',
|
|
144
|
-
'videoConversionRole',
|
|
145
|
-
'smart',
|
|
146
|
-
'synced',
|
|
147
|
-
'readonly',
|
|
148
|
-
'compilation',
|
|
149
|
-
'soundtrack',
|
|
150
|
-
]);
|
|
153
|
+
return String(value);
|
|
154
|
+
}
|
|
151
155
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
randomSongs: new Set(['song']),
|
|
164
|
-
topSongs: new Set(['song']),
|
|
165
|
-
searchResult: new Set(['artist', 'album', 'match']),
|
|
166
|
-
searchResult2: new Set(['artist', 'album', 'song']),
|
|
167
|
-
searchResult3: new Set(['artist', 'album', 'song']),
|
|
168
|
-
musicFolders: new Set(['musicFolder']),
|
|
169
|
-
genres: new Set(['genre']),
|
|
170
|
-
playlists: new Set(['playlist']),
|
|
171
|
-
directory: new Set(['child']),
|
|
172
|
-
playlist: new Set(['entry']),
|
|
173
|
-
playQueue: new Set(['entry']),
|
|
174
|
-
nowPlaying: new Set(['entry']),
|
|
175
|
-
lyricsList: new Set(['structuredLyrics']),
|
|
176
|
-
structuredLyrics: new Set(['line']),
|
|
177
|
-
starred: new Set(['artist', 'album', 'song']),
|
|
178
|
-
starred2: new Set(['artist', 'album', 'song']),
|
|
179
|
-
};
|
|
156
|
+
function xmlAttrs(attrs = {}) {
|
|
157
|
+
return Object.entries(attrs)
|
|
158
|
+
.map(([key, value]) => {
|
|
159
|
+
const attrValue = toXmlAttrValue(key, value);
|
|
160
|
+
if (attrValue == null) {
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
return ` ${key}="${xmlEscape(attrValue)}"`;
|
|
164
|
+
})
|
|
165
|
+
.join('');
|
|
166
|
+
}
|
|
180
167
|
|
|
181
168
|
function shouldUseArray(parentName, childName) {
|
|
182
169
|
return ARRAY_CHILDREN_BY_PARENT[parentName]?.has(childName) || false;
|
|
183
170
|
}
|
|
184
171
|
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return rawValue.map((entry) => String(entry || '').trim()).filter(Boolean);
|
|
189
|
-
}
|
|
190
|
-
const raw = String(rawValue || '').trim();
|
|
191
|
-
if (!raw) {
|
|
192
|
-
return [];
|
|
193
|
-
}
|
|
194
|
-
const parts = raw.includes(';') ? raw.split(';') : raw.split(',');
|
|
195
|
-
return parts.map((part) => part.trim()).filter(Boolean);
|
|
196
|
-
};
|
|
172
|
+
function isScalarChild(parentName, childName) {
|
|
173
|
+
return SCALAR_CHILDREN_BY_PARENT[parentName]?.has(childName) || false;
|
|
174
|
+
}
|
|
197
175
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
176
|
+
function defaultArrayChildName(parentName) {
|
|
177
|
+
const children = ARRAY_CHILDREN_BY_PARENT[parentName];
|
|
178
|
+
if (!children || children.size !== 1) {
|
|
179
|
+
return null;
|
|
201
180
|
}
|
|
181
|
+
return [...children][0];
|
|
182
|
+
}
|
|
202
183
|
|
|
203
|
-
|
|
204
|
-
|
|
184
|
+
function isFlattenArtistList(name, value) {
|
|
185
|
+
if ((name !== 'artists' && name !== 'albumArtists') || !Array.isArray(value)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (value.length === 0) {
|
|
189
|
+
return true;
|
|
205
190
|
}
|
|
191
|
+
return value.every((entry) => entry && typeof entry === 'object' && !Array.isArray(entry));
|
|
192
|
+
}
|
|
206
193
|
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
function shouldRenderAsChild(parentName, key, value) {
|
|
195
|
+
if (parentName === ROOT_NAME) {
|
|
196
|
+
return !ROOT_ATTR_KEYS.has(key);
|
|
209
197
|
}
|
|
210
198
|
|
|
211
|
-
if (key === '
|
|
212
|
-
|
|
213
|
-
return labels.map((name) => ({ name }));
|
|
199
|
+
if (key === 'value') {
|
|
200
|
+
return false;
|
|
214
201
|
}
|
|
215
202
|
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
if (value === 'false') {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
203
|
+
if (isScalarChild(parentName, key)) {
|
|
204
|
+
return true;
|
|
223
205
|
}
|
|
224
206
|
|
|
225
|
-
if (
|
|
226
|
-
return
|
|
207
|
+
if (shouldUseArray(parentName, key)) {
|
|
208
|
+
return true;
|
|
227
209
|
}
|
|
228
210
|
|
|
229
|
-
|
|
211
|
+
if (XML_ATTR_LIST_KEYS.has(key)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
return value.some((entry) => entry && typeof entry === 'object');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return Boolean(value && typeof value === 'object');
|
|
230
220
|
}
|
|
231
221
|
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
out[key] = coerceAttrValue(key, value);
|
|
222
|
+
function renderXmlElement(name, value) {
|
|
223
|
+
if (value === undefined || value === null) {
|
|
224
|
+
return `<${name}/>`;
|
|
236
225
|
}
|
|
237
226
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
227
|
+
if (typeof value !== 'object') {
|
|
228
|
+
return `<${name}>${xmlEscape(value)}</${name}>`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
const childName =
|
|
233
|
+
(name === 'artists' || name === 'albumArtists')
|
|
234
|
+
? 'artist'
|
|
235
|
+
: defaultArrayChildName(name);
|
|
236
|
+
const attrs = isFlattenArtistList(name, value) ? { flatten: 'true' } : {};
|
|
237
|
+
|
|
238
|
+
if (!childName) {
|
|
239
|
+
const text = value.map((entry) => String(entry)).join(', ');
|
|
240
|
+
return `<${name}${xmlAttrs(attrs)}>${xmlEscape(text)}</${name}>`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const inner = value.map((entry) => renderXmlElement(childName, entry)).join('');
|
|
244
|
+
if (!inner) {
|
|
245
|
+
return `<${name}${xmlAttrs(attrs)}/>`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return `<${name}${xmlAttrs(attrs)}>${inner}</${name}>`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const attrs = {};
|
|
252
|
+
let textValue = '';
|
|
253
|
+
const childParts = [];
|
|
254
|
+
|
|
255
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
256
|
+
if (key === 'value') {
|
|
257
|
+
textValue += String(childValue || '');
|
|
243
258
|
continue;
|
|
244
259
|
}
|
|
245
260
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!Array.isArray(out[child.name])) {
|
|
249
|
-
out[child.name] = [];
|
|
250
|
-
}
|
|
251
|
-
out[child.name].push(value);
|
|
261
|
+
if (!shouldRenderAsChild(name, key, childValue)) {
|
|
262
|
+
attrs[key] = childValue;
|
|
252
263
|
continue;
|
|
253
264
|
}
|
|
254
265
|
|
|
255
|
-
if (
|
|
256
|
-
if (
|
|
257
|
-
|
|
266
|
+
if (Array.isArray(childValue)) {
|
|
267
|
+
if (shouldUseArray(name, key)) {
|
|
268
|
+
for (const entry of childValue) {
|
|
269
|
+
childParts.push(renderXmlElement(key, entry));
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
childParts.push(renderXmlElement(key, childValue));
|
|
258
273
|
}
|
|
259
|
-
out[child.name].push(value);
|
|
260
274
|
continue;
|
|
261
275
|
}
|
|
262
276
|
|
|
263
|
-
|
|
277
|
+
childParts.push(renderXmlElement(key, childValue));
|
|
264
278
|
}
|
|
265
279
|
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
if (!Object.prototype.hasOwnProperty.call(out, childName)) {
|
|
270
|
-
out[childName] = [];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
280
|
+
const inner = `${xmlEscape(textValue)}${childParts.join('')}`;
|
|
281
|
+
if (!inner) {
|
|
282
|
+
return `<${name}${xmlAttrs(attrs)}/>`;
|
|
273
283
|
}
|
|
274
284
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return out.value;
|
|
278
|
-
}
|
|
285
|
+
return `<${name}${xmlAttrs(attrs)}>${inner}</${name}>`;
|
|
286
|
+
}
|
|
279
287
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (Array.isArray(artistValue)) {
|
|
284
|
-
return artistValue;
|
|
285
|
-
}
|
|
286
|
-
if (artistValue != null) {
|
|
287
|
-
return [artistValue];
|
|
288
|
-
}
|
|
289
|
-
return [];
|
|
290
|
-
}
|
|
288
|
+
function normalizeInner(inner) {
|
|
289
|
+
if (!inner || typeof inner !== 'object' || Array.isArray(inner)) {
|
|
290
|
+
return {};
|
|
291
291
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
292
|
+
const out = {};
|
|
293
|
+
for (const [key, value] of Object.entries(inner)) {
|
|
294
|
+
if (Array.isArray(value)) {
|
|
295
|
+
const childName = defaultArrayChildName(key);
|
|
296
|
+
if (childName && key !== 'artists' && key !== 'albumArtists') {
|
|
297
|
+
out[key] = { [childName]: value };
|
|
298
|
+
} else {
|
|
299
|
+
out[key] = value;
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
297
302
|
}
|
|
298
|
-
|
|
303
|
+
out[key] = value;
|
|
299
304
|
}
|
|
300
|
-
|
|
301
305
|
return out;
|
|
302
306
|
}
|
|
303
307
|
|
|
304
|
-
function
|
|
308
|
+
function buildRoot(status, inner = {}) {
|
|
305
309
|
return {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
type: SERVER_TYPE,
|
|
313
|
-
serverVersion: SERVER_VERSION,
|
|
314
|
-
openSubsonic: OPEN_SUBSONIC,
|
|
315
|
-
xmlns: XMLNS,
|
|
316
|
-
},
|
|
317
|
-
children,
|
|
310
|
+
status,
|
|
311
|
+
version: API_VERSION,
|
|
312
|
+
type: SERVER_TYPE,
|
|
313
|
+
serverVersion: SERVER_VERSION,
|
|
314
|
+
openSubsonic: OPEN_SUBSONIC,
|
|
315
|
+
...normalizeInner(inner),
|
|
318
316
|
};
|
|
319
317
|
}
|
|
320
318
|
|
|
321
|
-
export function
|
|
319
|
+
export function okResponseJson(inner = {}) {
|
|
322
320
|
return {
|
|
323
|
-
|
|
324
|
-
name,
|
|
325
|
-
attrs,
|
|
326
|
-
children: [],
|
|
327
|
-
selfClosing: true,
|
|
321
|
+
[ROOT_NAME]: buildRoot('ok', inner),
|
|
328
322
|
};
|
|
329
323
|
}
|
|
330
324
|
|
|
331
|
-
export function
|
|
325
|
+
export function failedResponseJson(code, message) {
|
|
332
326
|
return {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
327
|
+
[ROOT_NAME]: buildRoot('failed', {
|
|
328
|
+
error: {
|
|
329
|
+
code,
|
|
330
|
+
message: String(message),
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
338
333
|
};
|
|
339
334
|
}
|
|
340
335
|
|
|
341
|
-
export function
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
children: [],
|
|
355
|
-
selfClosing: true,
|
|
356
|
-
};
|
|
357
|
-
const root = subsonicRoot('failed', [errorNode]);
|
|
358
|
-
return `${XML_HEADER}${renderXmlPart(root)}`;
|
|
336
|
+
export function subsonicJsonToXml(payload) {
|
|
337
|
+
const rootValue = payload?.[ROOT_NAME];
|
|
338
|
+
const safeRoot = rootValue && typeof rootValue === 'object' && !Array.isArray(rootValue)
|
|
339
|
+
? rootValue
|
|
340
|
+
: buildRoot('failed', {
|
|
341
|
+
error: {
|
|
342
|
+
code: 0,
|
|
343
|
+
message: 'Invalid Subsonic payload',
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const xmlRoot = safeRoot.xmlns ? safeRoot : { ...safeRoot, xmlns: XMLNS };
|
|
348
|
+
return `${XML_HEADER}${renderXmlElement(ROOT_NAME, xmlRoot)}`;
|
|
359
349
|
}
|
|
360
350
|
|
|
361
|
-
export function
|
|
362
|
-
|
|
363
|
-
const json = nodeToJson(root);
|
|
364
|
-
delete json.xmlns;
|
|
365
|
-
return { 'subsonic-response': json };
|
|
351
|
+
export function okResponse(inner = {}) {
|
|
352
|
+
return subsonicJsonToXml(okResponseJson(inner));
|
|
366
353
|
}
|
|
367
354
|
|
|
368
|
-
export function
|
|
369
|
-
|
|
370
|
-
kind: 'node',
|
|
371
|
-
name: 'error',
|
|
372
|
-
attrs: {
|
|
373
|
-
code,
|
|
374
|
-
message: String(message),
|
|
375
|
-
},
|
|
376
|
-
children: [],
|
|
377
|
-
selfClosing: true,
|
|
378
|
-
};
|
|
379
|
-
const root = subsonicRoot('failed', [errorNode]);
|
|
380
|
-
const json = nodeToJson(root);
|
|
381
|
-
delete json.xmlns;
|
|
382
|
-
return { 'subsonic-response': json };
|
|
355
|
+
export function failedResponse(code, message) {
|
|
356
|
+
return subsonicJsonToXml(failedResponseJson(code, message));
|
|
383
357
|
}
|
|
384
|
-
|