Passed
Push — feature/329_Subsonic_API ( 793ffa...a9dee6 )
by Pauli
15:05
created

SubsonicController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 27
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
nc 1
nop 12
dl 0
loc 27
rs 9.9
c 1
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2019
11
 */
12
13
namespace OCA\Music\Controller;
14
15
use \OCP\AppFramework\Controller;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Controller was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use \OCP\AppFramework\Http\DataDisplayResponse;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\DataDisplayResponse was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use \OCP\AppFramework\Http\JSONResponse;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\JSONResponse was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use \OCP\IRequest;
0 ignored issues
show
Bug introduced by
The type OCP\IRequest was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use \OCP\IURLGenerator;
0 ignored issues
show
Bug introduced by
The type OCP\IURLGenerator was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
21
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
22
use \OCA\Music\AppFramework\Core\Logger;
23
use \OCA\Music\AppFramework\Utility\MethodAnnotationReader;
24
25
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
26
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
27
use \OCA\Music\BusinessLayer\Library;
28
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
29
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
30
31
use \OCA\Music\Db\SortBy;
32
33
use \OCA\Music\Http\FileResponse;
34
use \OCA\Music\Http\XMLResponse;
35
36
use \OCA\Music\Utility\CoverHelper;
37
use \OCA\Music\Utility\Util;
38
use OCA\Music\Middleware\SubsonicException;
39
40
class SubsonicController extends Controller {
41
	const API_VERSION = '1.4.0';
42
43
	private $albumBusinessLayer;
44
	private $artistBusinessLayer;
45
	private $playlistBusinessLayer;
46
	private $trackBusinessLayer;
47
	private $library;
48
	private $urlGenerator;
49
	private $rootFolder;
50
	private $l10n;
51
	private $coverHelper;
52
	private $logger;
53
	private $userId;
54
	private $format;
55
	private $callback;
56
57
	public function __construct($appname,
58
								IRequest $request,
59
								$l10n,
60
								IURLGenerator $urlGenerator,
61
								AlbumBusinessLayer $albumBusinessLayer,
62
								ArtistBusinessLayer $artistBusinessLayer,
63
								PlaylistBusinessLayer $playlistBusinessLayer,
64
								TrackBusinessLayer $trackBusinessLayer,
65
								Library $library,
66
								$rootFolder,
67
								CoverHelper $coverHelper,
68
								Logger $logger) {
69
		parent::__construct($appname, $request);
70
71
		$this->albumBusinessLayer = $albumBusinessLayer;
72
		$this->artistBusinessLayer = $artistBusinessLayer;
73
		$this->playlistBusinessLayer = $playlistBusinessLayer;
74
		$this->trackBusinessLayer = $trackBusinessLayer;
75
		$this->library = $library;
76
		$this->urlGenerator = $urlGenerator;
77
		$this->l10n = $l10n;
78
79
		// used to deliver actual media file
80
		$this->rootFolder = $rootFolder;
81
82
		$this->coverHelper = $coverHelper;
83
		$this->logger = $logger;
84
	}
85
86
	/**
87
	 * Called by the middleware once the user credentials have been checked
88
	 * @param string $userId
89
	 */
90
	public function setAuthenticatedUser($userId) {
91
		$this->userId = $userId;
92
	}
93
94
	/**
95
	 * @NoAdminRequired
96
	 * @PublicPage
97
	 * @NoCSRFRequired
98
	 * @SubsonicAPI
99
	 */
100
	public function handleRequest($method) {
101
		$this->format = $this->request->getParam('f', 'xml');
102
		$this->callback = $this->request->getParam('callback');
103
104
		if ($this->format != 'json' && $this->format != 'xml' && $this->format != 'jsonp') {
105
			throw new SubsonicException("Unsupported format {$this->format}", 0);
106
		}
107
108
		if ($this->format === 'jsonp' && $this->callback === null) {
109
			$this->format = 'json';
110
			throw new SubsonicException("Argument 'callback' is required with jsonp format", 10);
111
		}
112
113
		// Allow calling all methods with or without the postfix ".view"
114
		if (Util::endsWith($method, ".view")) {
115
			$method = \substr($method, 0, -\strlen(".view"));
116
		}
117
118
		// Allow calling any functions annotated to be part of the API, except for
119
		// recursive call back to this dispatcher function.
120
		if ($method !== 'handleRequest' && \method_exists($this, $method)) {
121
			$annotationReader = new MethodAnnotationReader($this, $method);
122
			if ($annotationReader->hasAnnotation('SubsonicAPI')) {
123
				return $this->$method();
124
			}
125
		}
126
127
		$this->logger->log("Request $method not supported", 'warn');
128
		return $this->subsonicErrorResponse(70, "Requested action $method is not supported");
129
	}
130
131
	/* -------------------------------------------------------------------------
132
	 * REST API methods
133
	 *------------------------------------------------------------------------*/
134
135
	/**
136
	 * @SubsonicAPI
137
	 */
138
	private function ping() {
139
		return $this->subsonicResponse([]);
140
	}
141
142
	/**
143
	 * @SubsonicAPI
144
	 */
145
	private function getLicense() {
146
		return $this->subsonicResponse([
147
			'license' => [
148
				'valid' => 'true',
149
				'email' => '',
150
				'licenseExpires' => 'never'
151
			]
152
		]);
153
	}
154
155
	/**
156
	 * @SubsonicAPI
157
	 */
158
	private function getMusicFolders() {
159
		// Only single root folder is supported
160
		return $this->subsonicResponse([
161
			'musicFolders' => ['musicFolder' => [
162
				['id' => 'root', 
163
				'name' => $this->l10n->t('Music')]
164
			]]
165
		]);
166
	}
167
168
	/**
169
	 * @SubsonicAPI
170
	 */
171
	private function getIndexes() {
172
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, SortBy::Name);
0 ignored issues
show
Bug introduced by
OCA\Music\Db\SortBy::Name of type integer is incompatible with the type OCA\Music\BusinessLayer\SortBy expected by parameter $sortBy of OCA\Music\BusinessLayer\...::findAllHavingAlbums(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

172
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, /** @scrutinizer ignore-type */ SortBy::Name);
Loading history...
173
174
		$indexes = [];
175
		foreach ($artists as $artist) {
176
			$indexes[$artist->getIndexingChar()][] = $this->artistAsChild($artist);
177
		}
178
179
		$result = [];
180
		foreach ($indexes as $indexChar => $bucketArtists) {
181
			$result[] = ['name' => $indexChar, 'artist' => $bucketArtists];
182
		}
183
184
		return $this->subsonicResponse(['indexes' => ['index' => $result]]);
185
	}
186
187
	/**
188
	 * @SubsonicAPI
189
	 */
190
	private function getMusicDirectory() {
191
		$id = $this->getRequiredParam('id');
192
193
		if (Util::startsWith($id, 'artist-')) {
194
			return $this->doGetMusicDirectoryForArtist($id);
195
		} else {
196
			return $this->doGetMusicDirectoryForAlbum($id);
197
		}
198
	}
199
200
	/**
201
	 * @SubsonicAPI
202
	 */
203
	private function getAlbumList() {
204
		$type = $this->getRequiredParam('type');
205
		$size = $this->request->getParam('size', 10);
206
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
207
		// $offset = $this->request->getParam('offset', 0); parameter not supported for now
208
209
		$albums = [];
210
		if ($type == 'random') {
211
			$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
212
			$albums = self::randomItems($allAlbums, $size);
213
		}
214
		// TODO: support 'newest', 'highest', 'frequent', 'recent'
215
216
		return $this->subsonicResponse(['albumList' =>
217
				['album' => \array_map([$this, 'albumAsChild'], $albums)]
218
		]);
219
	}
220
221
	/**
222
	 * @SubsonicAPI
223
	 */
224
	private function getRandomSongs() {
225
		$size = $this->request->getParam('size', 10);
226
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
227
		// $genre = $this->request->getParam('genre'); not supported
228
		// $fromYear = $this->request->getParam('fromYear'); not supported
229
		// $toYear = $this->request->getParam('genre'); not supported
230
231
		$allTracks = $this->trackBusinessLayer->findAll($this->userId);
232
		$tracks = self::randomItems($allTracks, $size);
233
234
		return $this->subsonicResponse(['randomSongs' =>
235
				['song' => \array_map([$this, 'trackAsChild'], $tracks)]
236
		]);
237
	}
238
239
	/**
240
	 * @SubsonicAPI
241
	 */
242
	private function getCoverArt() {
243
		$id = $this->getRequiredParam('id');
244
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
245
246
		try {
247
			$coverData = $this->coverHelper->getCover($id, $this->userId, $userFolder);
248
			if ($coverData !== null) {
0 ignored issues
show
introduced by
The condition $coverData !== null is always true.
Loading history...
249
				return new FileResponse($coverData);
250
			}
251
		} catch (BusinessLayerException $e) {
252
			return $this->subsonicErrorResponse(70, 'album not found');
253
		}
254
255
		return $this->subsonicErrorResponse(70, 'album has no cover');
256
	}
257
258
	/**
259
	 * @SubsonicAPI
260
	 */
261
	private function stream() {
262
		// We don't support transcaoding, so 'stream' and 'download' act identically
263
		return $this->download();
264
	}
265
266
	/**
267
	 * @SubsonicAPI
268
	 */
269
	private function download() {
270
		$id = $this->getRequiredParam('id');
271
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
272
273
		try {
274
			$track = $this->trackBusinessLayer->find($trackId, $this->userId);
0 ignored issues
show
Bug introduced by
$trackId of type string is incompatible with the type integer expected by parameter $id of OCA\Music\AppFramework\B...r\BusinessLayer::find(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

274
			$track = $this->trackBusinessLayer->find(/** @scrutinizer ignore-type */ $trackId, $this->userId);
Loading history...
275
		} catch (BusinessLayerException $e) {
276
			return $this->subsonicErrorResponse(70, $e->getMessage());
277
		}
278
279
		$files = $this->rootFolder->getUserFolder($this->userId)->getById($track->getFileId());
280
281
		if (\count($files) === 1) {
282
			return new FileResponse($files[0]);
283
		} else {
284
			return $this->subsonicErrorResponse(70, 'file not found');
285
		}
286
	}
287
288
	/**
289
	 * @SubsonicAPI
290
	 */
291
	private function search2() {
292
		$query = $this->getRequiredParam('query');
293
		$artistCount = $this->request->getParam('artistCount', 20);
294
		$artistOffset = $this->request->getParam('artistOffset', 0);
295
		$albumCount = $this->request->getParam('albumCount', 20);
296
		$albumOffset = $this->request->getParam('albumOffset', 0);
297
		$songCount = $this->request->getParam('songCount', 20);
298
		$songOffset = $this->request->getParam('songOffset', 0);
299
300
		if (empty($query)) {
301
			throw new SubsonicException("The 'query' argument is mandatory", 10);
302
		}
303
304
		$artists = $this->artistBusinessLayer->findAllByName($query, $this->userId, true, $artistCount, $artistOffset);
305
		$albums = $this->albumBusinessLayer->findAllByName($query, $this->userId, true, $albumCount, $albumOffset);
306
		$tracks = $this->trackBusinessLayer->findAllByName($query, $this->userId, true, $songCount, $songOffset);
307
308
		$results = [];
309
		if (!empty($artists)) {
310
			$results['artist'] = \array_map([$this, 'artistAsChild'], $artists);
311
		}
312
		if (!empty($albums)) {
313
			$results['album'] = \array_map([$this, 'albumAsChild'], $albums);
314
		}
315
		if (!empty($tracks)) {
316
			$results['song'] = \array_map([$this, 'trackAsChild'], $tracks);
317
		}
318
319
		return $this->subsonicResponse(['searchResult2' => $results]);
320
	}
321
322
	/**
323
	 * @SubsonicAPI
324
	 */
325
	private function getPlaylists() {
326
		$playlists = $this->playlistBusinessLayer->findAll($this->userId);
327
328
		return $this->subsonicResponse(['playlists' =>
329
			['playlist' => \array_map([$this, 'playlistAsChild'], $playlists)]
330
		]);
331
	}
332
333
	/**
334
	 * @SubsonicAPI
335
	 */
336
	private function getPlaylist() {
337
		$id = $this->getRequiredParam('id');
338
		$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
339
		$tracks = $this->playlistBusinessLayer->getPlaylistTracks($id, $this->userId);
340
341
		$playlistNode = $this->playlistAsChild($playlist);
342
		$playlistNode['entry'] = \array_map([$this, 'trackAsChild'], $tracks);
343
344
		return $this->subsonicResponse(['playlist' => $playlistNode]);
345
	}
346
347
	/**
348
	 * @SubsonicAPI
349
	 */
350
	private function createPlaylist() {
351
		$name = $this->getRequiredParam('name');
352
		$songIds = $this->getRepeatedParam('songId');
353
		$songIds = \array_map('self::ripIdPrefix', $songIds);
354
355
		$playlist = $this->playlistBusinessLayer->create($name, $this->userId);
356
		$this->playlistBusinessLayer->addTracks($songIds, $playlist->getId(), $this->userId);
357
358
		return $this->subsonicResponse([]);
359
	}
360
361
	/* -------------------------------------------------------------------------
362
	 * Helper methods
363
	 *------------------------------------------------------------------------*/
364
365
	private function getRequiredParam($paramName) {
366
		$param = $this->request->getParam($paramName);
367
368
		if ($param === null) {
369
			throw new SubsonicException("Required parameter '$paramName' missing", 10);
370
		}
371
372
		return $param;
373
	}
374
375
	/** 
376
	 * Get values for parameter which may be present multiple times in the query
377
	 * string or POST data.
378
	 * @param string $paramName
379
	 * @return string[]
380
	 */
381
	private function getRepeatedParam($paramName) {
382
		// We can't use the IRequest object nor $_GET and $_POST to get the data
383
		// because all of these are based to the idea of unique parameter names.
384
		// If the same name is repeated, only the last value is saved. Hence, we
385
		// need to parse the raw data manually.
386
387
		// query string is always present (although it could be empty)
388
		$values = $this->parseRepeatedKeyValues($paramName, $_SERVER['QUERY_STRING']);
389
390
		// POST data is available if the method is POST
391
		if ($this->request->getMethod() == 'POST') {
392
			$values = \array_merge($values,
393
					$this->parseRepeatedKeyValues($paramName, file_get_contents('php://input')));
394
		}
395
396
		return $values;
397
	}
398
399
	/**
400
	 * Parse a string like "someKey=value1&someKey=value2&anotherKey=valueA&someKey=value3"
401
	 * and return an array of values for the given key
402
	 * @param string $key
403
	 * @param string $data
404
	 */
405
	private function parseRepeatedKeyValues($key, $data) {
406
		$result = [];
407
408
		$keyValuePairs = \explode('&', $data);
409
410
		foreach ($keyValuePairs as $pair) {
411
			$keyAndValue = \explode('=', $pair);
412
413
			if ($keyAndValue[0] == $key) {
414
				$result[] = $keyAndValue[1];
415
			}
416
		}
417
418
		return $result;
419
	}
420
421
	private function doGetMusicDirectoryForArtist($id) {
422
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
423
424
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
0 ignored issues
show
Bug introduced by
$artistId of type string is incompatible with the type integer expected by parameter $id of OCA\Music\AppFramework\B...r\BusinessLayer::find(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

424
		$artist = $this->artistBusinessLayer->find(/** @scrutinizer ignore-type */ $artistId, $this->userId);
Loading history...
425
		$artistName = $artist->getNameString($this->l10n);
426
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
427
428
		$children = [];
429
		foreach ($albums as $album) {
430
			$children[] = $this->albumAsChild($album, $artistName);
431
		}
432
433
		return $this->subsonicResponse([
434
			'directory' => [
435
				'id' => $id,
436
				'parent' => 'root',
437
				'name' => $artistName,
438
				'child' => $children
439
			]
440
		]);
441
	}
442
443
	private function doGetMusicDirectoryForAlbum($id) {
444
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
445
446
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
447
		$albumName = $album->getNameString($this->l10n);
448
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
449
450
		return $this->subsonicResponse([
451
			'directory' => [
452
				'id' => $id,
453
				'parent' => 'artist-' . $album->getAlbumArtistId(),
454
				'name' => $albumName,
455
				'child' => \array_map([$this, 'trackAsChild'], $tracks)
456
			]
457
		]);
458
	}
459
460
	private function artistAsChild($artist) {
461
		return [
462
			'name' => $artist->getNameString($this->l10n),
463
			'id' => 'artist-' . $artist->getId()
464
		];
465
	}
466
467
	private function albumAsChild($album, $artistName = null) {
468
		$artistId = $album->getAlbumArtistId();
469
470
		if (empty($artistName)) {
471
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
472
			$artistName = $artist->getNameString($this->l10n);
473
		}
474
475
		$result = [
476
			'id' => 'album-' . $album->getId(),
477
			'parent' => 'artist-' . $artistId,
478
			'title' => $album->getNameString($this->l10n),
479
			'artist' => $artistName,
480
			'isDir' => true
481
		];
482
483
		if (!empty($album->getCoverFileId())) {
484
			$result['coverArt'] = $album->getId();
485
		}
486
487
		return $result;
488
	}
489
490
	private function trackAsChild($track, $album = null, $albumName = null) {
491
		$albumId = $track->getAlbumId();
492
		if ($album == null) {
493
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
494
		}
495
		if (empty($albumName)) {
496
			$albumName = $album->getNameString($this->l10n);
497
		}
498
499
		$trackArtist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
500
		$result = [
501
			'id' => 'track-' . $track->getId(),
502
			'parent' => 'album-' . $albumId,
503
			'title' => $track->getTitle(),
504
			'artist' => $trackArtist->getNameString($this->l10n),
505
			'isDir' => false,
506
			'album' => $albumName,
507
			//'genre' => '',
508
			'year' => $track->getYear(),
509
			'size' => $track->getSize(),
510
			'contentType' => $track->getMimetype(),
511
			'suffix' => \end(\explode('.', $track->getFilename())),
512
			'duration' => $track->getLength() ?: 0,
513
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
514
			//'path' => ''
515
		];
516
517
		if (!empty($album->getCoverFileId())) {
518
			$result['coverArt'] = $album->getId();
519
		}
520
521
		if ($track->getNumber() !== null) {
522
			$result['track'] = $track->getNumber();
523
		}
524
525
		return $result;
526
	}
527
528
	private function playlistAsChild($playlist) {
529
		return [
530
			'id' => $playlist->getId(),
531
			'name' => $playlist->getName(),
532
			'owner' => $this->userId,
533
			'public' => false,
534
			'songCount' => $playlist->getTrackCount(),
535
			// comment => '',
536
			// duration => '',
537
			// created => '',
538
			// coverArt => ''
539
		];
540
	}
541
542
	/**
543
	 * Given a prefixed ID like 'artist-123' or 'track-45', return just the numeric part.
544
	 * @param string $id
545
	 * @return string
546
	 */
547
	private static function ripIdPrefix($id) {
548
		return \explode('-', $id)[1];
549
	}
550
551
	private static function randomItems($itemArray, $count) {
552
		$count = \min($count, \count($itemArray)); // can't return more than all items
553
		$indices = \array_rand($itemArray, $count);
554
		if ($count == 1) { // return type is not array when randomizing a single index
555
			$indices = [$indices];
556
		}
557
558
		$result = [];
559
		foreach ($indices as $index) {
560
			$result[] = $itemArray[$index];
561
		}
562
563
		return $result;
564
	}
565
566
	private function subsonicResponse($content, $status = 'ok') {
567
		$content['status'] = $status; 
568
		$content['version'] = self::API_VERSION;
569
		$responseData = ['subsonic-response' => $content];
570
571
		if ($this->format == 'json') {
572
			$response = new JSONResponse($responseData);
573
		} else if ($this->format == 'jsonp') {
574
			$responseData = \json_encode($responseData);
575
			$response = new DataDisplayResponse("{$this->callback}($responseData);");
576
			$response->addHeader('Content-Type', 'text/javascript; charset=UTF-8');
577
		} else {
578
			$response = new XMLResponse($responseData);
579
		}
580
581
		return $response;
582
	}
583
584
	public function subsonicErrorResponse($errorCode, $errorMessage) {
585
		return $this->subsonicResponse([
586
				'error' => [
587
					'code' => $errorCode,
588
					'message' => $errorMessage
589
				]
590
			], 'failed');
591
	}
592
}
593