Passed
Push — master ( 543b86...d5fa9e )
by Pauli
02:10 queued 11s
created

SubsonicController::createIdLookupTable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 6
ccs 0
cts 5
cp 0
crap 6
rs 10
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, 2020
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\Files\File;
0 ignored issues
show
Bug introduced by
The type OCP\Files\File 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\Files\Folder;
0 ignored issues
show
Bug introduced by
The type OCP\Files\Folder 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
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...
21
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...
22
23
use \OCA\Music\AppFramework\Core\Logger;
24
use \OCA\Music\AppFramework\Utility\MethodAnnotationReader;
25
26
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
27
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
28
use \OCA\Music\BusinessLayer\GenreBusinessLayer;
29
use \OCA\Music\BusinessLayer\Library;
30
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
31
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
32
33
use \OCA\Music\Db\Album;
34
use \OCA\Music\Db\Artist;
35
use \OCA\Music\Db\Genre;
36
use \OCA\Music\Db\Playlist;
37
use \OCA\Music\Db\SortBy;
38
use \OCA\Music\Db\Track;
39
40
use \OCA\Music\Http\FileResponse;
41
use \OCA\Music\Http\XMLResponse;
42
43
use \OCA\Music\Middleware\SubsonicException;
44
45
use \OCA\Music\Utility\CoverHelper;
46
use \OCA\Music\Utility\DetailsHelper;
47
use \OCA\Music\Utility\Random;
48
use \OCA\Music\Utility\UserMusicFolder;
49
use \OCA\Music\Utility\Util;
50
51
class SubsonicController extends Controller {
52
	const API_VERSION = '1.10.1';
53
54
	private $albumBusinessLayer;
55
	private $artistBusinessLayer;
56
	private $playlistBusinessLayer;
57
	private $genreBusinessLayer;
58
	private $trackBusinessLayer;
59
	private $library;
60
	private $urlGenerator;
61
	private $userMusicFolder;
62
	private $l10n;
63
	private $coverHelper;
64
	private $detailsHelper;
65
	private $random;
66
	private $logger;
67
	private $userId;
68
	private $format;
69
	private $callback;
70
	private $timezone;
71
72
	public function __construct($appname,
73
								IRequest $request,
74
								$l10n,
75
								IURLGenerator $urlGenerator,
76
								AlbumBusinessLayer $albumBusinessLayer,
77
								ArtistBusinessLayer $artistBusinessLayer,
78
								GenreBusinessLayer $genreBusinessLayer,
79
								PlaylistBusinessLayer $playlistBusinessLayer,
80
								TrackBusinessLayer $trackBusinessLayer,
81
								Library $library,
82
								UserMusicFolder $userMusicFolder,
83
								CoverHelper $coverHelper,
84
								DetailsHelper $detailsHelper,
85
								Random $random,
86
								Logger $logger) {
87
		parent::__construct($appname, $request);
88
89
		$this->albumBusinessLayer = $albumBusinessLayer;
90
		$this->artistBusinessLayer = $artistBusinessLayer;
91
		$this->genreBusinessLayer = $genreBusinessLayer;
92
		$this->playlistBusinessLayer = $playlistBusinessLayer;
93
		$this->trackBusinessLayer = $trackBusinessLayer;
94
		$this->library = $library;
95
		$this->urlGenerator = $urlGenerator;
96
		$this->l10n = $l10n;
97
		$this->userMusicFolder = $userMusicFolder;
98
		$this->coverHelper = $coverHelper;
99
		$this->detailsHelper = $detailsHelper;
100
		$this->random = $random;
101
		$this->logger = $logger;
102
103
		// For timestamps in the Subsonic API, we would prefer to use the local timezone,
104
		// but the core has set the default timezone as 'UTC'. Get the timezone from php.ini
105
		// if available, and store it for later use.
106
		$tz = \ini_get('date.timezone') ?: 'UTC';
107
		$this->timezone = new \DateTimeZone($tz);
108
	}
109
110
	/**
111
	 * Called by the middleware to set the reponse format to be used
112
	 * @param string $format Response format: xml/json/jsonp
113
	 * @param string|null $callback Function name to use if the @a $format is 'jsonp'
114
	 */
115
	public function setResponseFormat($format, $callback) {
116
		$this->format = $format;
117
		$this->callback = $callback;
118
	}
119
120
	/**
121
	 * Called by the middleware once the user credentials have been checked
122
	 * @param string $userId
123
	 */
124
	public function setAuthenticatedUser($userId) {
125
		$this->userId = $userId;
126
	}
127
128
	/**
129
	 * @NoAdminRequired
130
	 * @PublicPage
131
	 * @NoCSRFRequired
132
	 */
133
	public function handleRequest($method) {
134
		$this->logger->log("Subsonic request $method", 'debug');
135
136
		// Allow calling all methods with or without the postfix ".view"
137
		if (Util::endsWith($method, ".view")) {
138
			$method = \substr($method, 0, -\strlen(".view"));
139
		}
140
141
		// Allow calling any functions annotated to be part of the API
142
		if (\method_exists($this, $method)) {
143
			$annotationReader = new MethodAnnotationReader($this, $method);
144
			if ($annotationReader->hasAnnotation('SubsonicAPI')) {
145
				return $this->$method();
146
			}
147
		}
148
149
		$this->logger->log("Request $method not supported", 'warn');
150
		return $this->subsonicErrorResponse(70, "Requested action $method is not supported");
151
	}
152
153
	/* -------------------------------------------------------------------------
154
	 * REST API methods
155
	 *------------------------------------------------------------------------*/
156
157
	/**
158
	 * @SubsonicAPI
159
	 */
160
	private function ping() {
161
		return $this->subsonicResponse([]);
162
	}
163
164
	/**
165
	 * @SubsonicAPI
166
	 */
167
	private function getLicense() {
168
		return $this->subsonicResponse([
169
			'license' => [
170
				'valid' => 'true',
171
				'email' => '',
172
				'licenseExpires' => 'never'
173
			]
174
		]);
175
	}
176
177
	/**
178
	 * @SubsonicAPI
179
	 */
180
	private function getMusicFolders() {
181
		// Only single root folder is supported
182
		return $this->subsonicResponse([
183
			'musicFolders' => ['musicFolder' => [
184
				['id' => 'artists', 'name' => $this->l10n->t('Artists')],
185
				['id' => 'folders', 'name' => $this->l10n->t('Folders')]
186
			]]
187
		]);
188
	}
189
190
	/**
191
	 * @SubsonicAPI
192
	 */
193
	private function getIndexes() {
194
		$id = $this->request->getParam('musicFolderId');
195
196
		if ($id === 'folders') {
197
			return $this->getIndexesForFolders();
198
		} else {
199
			return $this->getIndexesForArtists();
200
		}
201
	}
202
203
	/**
204
	 * @SubsonicAPI
205
	 */
206
	private function getMusicDirectory() {
207
		$id = $this->getRequiredParam('id');
208
209
		if (Util::startsWith($id, 'folder-')) {
210
			return $this->getMusicDirectoryForFolder($id);
211
		} elseif (Util::startsWith($id, 'artist-')) {
212
			return $this->getMusicDirectoryForArtist($id);
213
		} else {
214
			return $this->getMusicDirectoryForAlbum($id);
215
		}
216
	}
217
218
	/**
219
	 * @SubsonicAPI
220
	 */
221
	private function getAlbumList() {
222
		$albums = $this->albumsForGetAlbumList();
223
		return $this->subsonicResponse(['albumList' =>
224
				['album' => \array_map([$this, 'albumToOldApi'], $albums)]
225
		]);
226
	}
227
228
	/**
229
	 * @SubsonicAPI
230
	 */
231
	private function getAlbumList2() {
232
		/*
233
		 * According to the API specification, the difference between this and getAlbumList
234
		 * should be that this function would organize albums according the metadata while
235
		 * getAlbumList would organize them by folders. However, we organize by metadata
236
		 * also in getAlbumList, because that's more natural for the Music app and many/most
237
		 * clients do not support getAlbumList2.
238
		 */
239
		$albums = $this->albumsForGetAlbumList();
240
		return $this->subsonicResponse(['albumList2' =>
241
				['album' => \array_map([$this, 'albumToNewApi'], $albums)]
242
		]);
243
	}
244
245
	/**
246
	 * @SubsonicAPI
247
	 */
248
	private function getArtists() {
249
		return $this->getIndexesForArtists('artists');
250
	}
251
252
	/**
253
	 * @SubsonicAPI
254
	 */
255
	private function getArtist() {
256
		$id = $this->getRequiredParam('id');
257
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
258
259
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
260
		$artistName = $artist->getNameString($this->l10n);
261
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
262
263
		$artistNode = $this->artistToApi($artist);
264
		$artistNode['album'] = \array_map(function($album) use ($artistName) {
265
			return $this->albumToNewApi($album, $artistName);
266
		}, $albums);
267
268
		return $this->subsonicResponse(['artist' => $artistNode]);
269
	}
270
271
	/**
272
	 * @SubsonicAPI
273
	 */
274
	private function getAlbum() {
275
		$id = $this->getRequiredParam('id');
276
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
277
278
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
279
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
280
281
		$albumNode = $this->albumToNewApi($album);
282
		$albumNode['song'] = \array_map(function($track) use ($album) {
283
			$track->setAlbum($album);
284
			return $this->trackToApi($track);
285
		}, $tracks);
286
		return $this->subsonicResponse(['album' => $albumNode]);
287
	}
288
289
	/**
290
	 * @SubsonicAPI
291
	 */
292
	private function getSong() {
293
		$id = $this->getRequiredParam('id');
294
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
295
		$track = $this->trackBusinessLayer->find($trackId, $this->userId);
296
297
		return $this->subsonicResponse(['song' => $this->trackToApi($track)]);
298
	}
299
300
	/**
301
	 * @SubsonicAPI
302
	 */
303
	private function getRandomSongs() {
304
		$size = $this->request->getParam('size', 10);
305
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
306
		$genre = $this->request->getParam('genre');
307
		// $fromYear = $this->request->getParam('fromYear'); not supported
308
		// $toYear = $this->request->getParam('genre'); not supported
309
310
		if ($genre !== null) {
311
			$trackPool = $this->findTracksByGenre($genre);
312
		} else {
313
			$trackPool = $this->trackBusinessLayer->findAll($this->userId);
314
		}
315
		$tracks = Random::pickItems($trackPool, $size);
316
317
		return $this->subsonicResponse(['randomSongs' =>
318
				['song' => \array_map([$this, 'trackToApi'], $tracks)]
319
		]);
320
	}
321
322
	/**
323
	 * @SubsonicAPI
324
	 */
325
	private function getCoverArt() {
326
		$id = $this->getRequiredParam('id');
327
		$size = $this->request->getParam('size');
328
329
		$rootFolder = $this->userMusicFolder->getFolder($this->userId);
330
		$coverData = $this->coverHelper->getCover($id, $this->userId, $rootFolder, $size);
331
332
		if ($coverData !== null) {
333
			return new FileResponse($coverData);
334
		}
335
336
		return $this->subsonicErrorResponse(70, 'album has no cover');
337
	}
338
339
	/**
340
	 * @SubsonicAPI
341
	 */
342
	private function getLyrics() {
343
		$artistPar = $this->request->getParam('artist');
344
		$titlePar = $this->request->getParam('title');
345
346
		$matches = $this->trackBusinessLayer->findAllByNameAndArtistName($titlePar, $artistPar, $this->userId);
347
		$matchingCount = \count($matches);
348
349
		if ($matchingCount === 0) {
350
			$this->logger->log("No matching track for title '$titlePar' and artist '$artistPar'", 'debug');
351
			return $this->subsonicResponse(['lyrics' => new \stdClass]);
352
		}
353
		else {
354
			if ($matchingCount > 1) {
355
				$this->logger->log("Found $matchingCount tracks matching title ".
356
									"'$titlePar' and artist '$artistPar'; using the first", 'debug');
357
			}
358
			$track = $matches[0];
359
360
			$artist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
361
			$rootFolder = $this->userMusicFolder->getFolder($this->userId);
362
			$lyrics = $this->detailsHelper->getLyrics($track->getFileId(), $rootFolder);
363
364
			return $this->subsonicResponse(['lyrics' => [
365
					'artist' => $artist->getNameString($this->l10n),
366
					'title' => $track->getTitle(),
367
					'value' => $lyrics
368
			]]);
369
		}
370
	}
371
372
	/**
373
	 * @SubsonicAPI
374
	 */
375
	private function stream() {
376
		// We don't support transcaoding, so 'stream' and 'download' act identically
377
		return $this->download();
378
	}
379
380
	/**
381
	 * @SubsonicAPI
382
	 */
383
	private function download() {
384
		$id = $this->getRequiredParam('id');
385
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
386
387
		$track = $this->trackBusinessLayer->find($trackId, $this->userId);
388
		$file = $this->getFilesystemNode($track->getFileId());
389
390
		if ($file instanceof File) {
391
			return new FileResponse($file);
392
		} else {
393
			return $this->subsonicErrorResponse(70, 'file not found');
394
		}
395
	}
396
397
	/**
398
	 * @SubsonicAPI
399
	 */
400
	private function search2() {
401
		$results = $this->doSearch();
402
		return $this->searchResponse('searchResult2', $results, /*$useNewApi=*/false);
403
	}
404
405
	/**
406
	 * @SubsonicAPI
407
	 */
408
	private function search3() {
409
		$results = $this->doSearch();
410
		return $this->searchResponse('searchResult3', $results, /*$useNewApi=*/true);
411
	}
412
413
	/**
414
	 * @SubsonicAPI
415
	 */
416
	private function getGenres() {
417
		$genres = $this->genreBusinessLayer->findAllWithCounts($this->userId);
418
419
		return $this->subsonicResponse(['genres' =>
420
			[
421
				'genre' => \array_map(function($genre) {
422
					return [
423
						'songCount' => $genre->getTrackCount(),
424
						'albumCount' => $genre->getAlbumCount(),
425
						'value' => $genre->getNameString($this->l10n)
426
					];
427
				},
428
				$genres)
429
			]
430
		]);
431
	}
432
433
	/**
434
	 * @SubsonicAPI
435
	 */
436
	private function getSongsByGenre() {
437
		$genre = $this->getRequiredParam('genre');
438
		$count = $this->request->getParam('count', 10);
439
		$offset = $this->request->getParam('offset', 0);
440
441
		$tracks = $this->findTracksByGenre($genre, $count, $offset);
442
443
		return $this->subsonicResponse(['songsByGenre' =>
444
			['song' => \array_map([$this, 'trackToApi'], $tracks)]
445
		]);
446
	}
447
448
	/**
449
	 * @SubsonicAPI
450
	 */
451
	private function getPlaylists() {
452
		$playlists = $this->playlistBusinessLayer->findAll($this->userId);
453
454
		return $this->subsonicResponse(['playlists' =>
455
			['playlist' => \array_map([$this, 'playlistToApi'], $playlists)]
456
		]);
457
	}
458
459
	/**
460
	 * @SubsonicAPI
461
	 */
462
	private function getPlaylist() {
463
		$id = $this->getRequiredParam('id');
464
		$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
465
		$tracks = $this->playlistBusinessLayer->getPlaylistTracks($id, $this->userId);
466
467
		$playlistNode = $this->playlistToApi($playlist);
468
		$playlistNode['entry'] = \array_map([$this, 'trackToApi'], $tracks);
469
470
		return $this->subsonicResponse(['playlist' => $playlistNode]);
471
	}
472
473
	/**
474
	 * @SubsonicAPI
475
	 */
476
	private function createPlaylist() {
477
		$name = $this->getRequiredParam('name');
478
		$songIds = $this->getRepeatedParam('songId');
479
		$songIds = \array_map('self::ripIdPrefix', $songIds);
480
481
		$playlist = $this->playlistBusinessLayer->create($name, $this->userId);
482
		$this->playlistBusinessLayer->addTracks($songIds, $playlist->getId(), $this->userId);
483
484
		return $this->subsonicResponse([]);
485
	}
486
487
	/**
488
	 * @SubsonicAPI
489
	 */
490
	private function updatePlaylist() {
491
		$listId = $this->getRequiredParam('playlistId');
492
		$newName = $this->request->getParam('name');
493
		$songIdsToAdd = $this->getRepeatedParam('songIdToAdd');
494
		$songIdsToAdd = \array_map('self::ripIdPrefix', $songIdsToAdd);
495
		$songIndicesToRemove = $this->getRepeatedParam('songIndexToRemove');
496
497
		if (!empty($newName)) {
498
			$this->playlistBusinessLayer->rename($newName, $listId, $this->userId);
499
		}
500
501
		if (!empty($songIndicesToRemove)) {
502
			$this->playlistBusinessLayer->removeTracks($songIndicesToRemove, $listId, $this->userId);
503
		}
504
505
		if (!empty($songIdsToAdd)) {
506
			$this->playlistBusinessLayer->addTracks($songIdsToAdd, $listId, $this->userId);
507
		}
508
509
		return $this->subsonicResponse([]);
510
	}
511
512
	/**
513
	 * @SubsonicAPI
514
	 */
515
	private function deletePlaylist() {
516
		$id = $this->getRequiredParam('id');
517
		$this->playlistBusinessLayer->delete($id, $this->userId);
518
		return $this->subsonicResponse([]);
519
	}
520
521
	/**
522
	 * @SubsonicAPI
523
	 */
524
	private function getUser() {
525
		$username = $this->getRequiredParam('username');
526
527
		if ($username != $this->userId) {
528
			throw new SubsonicException("{$this->userId} is not authorized to get details for other users.", 50);
529
		}
530
531
		return $this->subsonicResponse([
532
			'user' => [
533
				'username' => $username,
534
				'email' => '',
535
				'scrobblingEnabled' => false,
536
				'adminRole' => false,
537
				'settingsRole' => false,
538
				'downloadRole' => true,
539
				'uploadRole' => false,
540
				'playlistRole' => true,
541
				'coverArtRole' => false,
542
				'commentRole' => false,
543
				'podcastRole' => false,
544
				'streamRole' => true,
545
				'jukeboxRole' => false,
546
				'shareRole' => false,
547
				'videoConversionRole' => false,
548
				'folder' => ['artists', 'folders'],
549
			]
550
		]);
551
	}
552
553
	/**
554
	 * @SubsonicAPI
555
	 */
556
	private function getUsers() {
557
		throw new SubsonicException("{$this->userId} is not authorized to get details for other users.", 50);
558
	}
559
560
	/**
561
	 * @SubsonicAPI
562
	 */
563
	private function getAvatar() {
564
		// TODO: Use 'username' parameter to fetch user-specific avatar from the OC core.
565
		// Remember to check the permission.
566
		// For now, use the Music app logo for all users.
567
		$fileName = \join(DIRECTORY_SEPARATOR, [\dirname(__DIR__), 'img', 'logo', 'music_logo.png']);
568
		$content = \file_get_contents($fileName);
569
		return new FileResponse(['content' => $content, 'mimetype' => 'image/png']);
570
	}
571
572
	/**
573
	 * @SubsonicAPI
574
	 */
575
	private function star() {
576
		$targetIds = $this->getStarringParameters();
577
578
		$this->trackBusinessLayer->setStarred($targetIds['tracks'], $this->userId);
0 ignored issues
show
Bug introduced by
It seems like $targetIds['tracks'] can also be of type string[]; however, parameter $ids of OCA\Music\AppFramework\B...nessLayer::setStarred() does only seem to accept integer[], maybe add an additional type check? ( Ignorable by Annotation )

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

578
		$this->trackBusinessLayer->setStarred(/** @scrutinizer ignore-type */ $targetIds['tracks'], $this->userId);
Loading history...
579
		$this->albumBusinessLayer->setStarred($targetIds['albums'], $this->userId);
580
		$this->artistBusinessLayer->setStarred($targetIds['artists'], $this->userId);
581
582
		return $this->subsonicResponse([]);
583
	}
584
585
	/**
586
	 * @SubsonicAPI
587
	 */
588
	private function unstar() {
589
		$targetIds = $this->getStarringParameters();
590
591
		$this->trackBusinessLayer->unsetStarred($targetIds['tracks'], $this->userId);
0 ignored issues
show
Bug introduced by
It seems like $targetIds['tracks'] can also be of type string[]; however, parameter $ids of OCA\Music\AppFramework\B...ssLayer::unsetStarred() does only seem to accept integer[], maybe add an additional type check? ( Ignorable by Annotation )

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

591
		$this->trackBusinessLayer->unsetStarred(/** @scrutinizer ignore-type */ $targetIds['tracks'], $this->userId);
Loading history...
592
		$this->albumBusinessLayer->unsetStarred($targetIds['albums'], $this->userId);
593
		$this->artistBusinessLayer->unsetStarred($targetIds['artists'], $this->userId);
594
595
		return $this->subsonicResponse([]);
596
	}
597
598
	/**
599
	 * @SubsonicAPI
600
	 */
601
	private function getStarred() {
602
		$starred = $this->doGetStarred();
603
		return $this->searchResponse('starred', $starred, /*$useNewApi=*/false);
604
	}
605
606
	/**
607
	 * @SubsonicAPI
608
	 */
609
	private function getStarred2() {
610
		$starred = $this->doGetStarred();
611
		return $this->searchResponse('starred2', $starred, /*$useNewApi=*/true);
612
	}
613
614
	/**
615
	 * @SubsonicAPI
616
	 */
617
	private function getVideos() {
618
		// TODO: dummy implementation
619
		return $this->subsonicResponse([
620
			'videos' => [
621
				'video' => []
622
			]
623
		]);
624
	}
625
626
	/* -------------------------------------------------------------------------
627
	 * Helper methods
628
	 *------------------------------------------------------------------------*/
629
630
	private function getRequiredParam($paramName) {
631
		$param = $this->request->getParam($paramName);
632
633
		if ($param === null) {
634
			throw new SubsonicException("Required parameter '$paramName' missing", 10);
635
		}
636
637
		return $param;
638
	}
639
640
	/**
641
	 * Get parameters used in the `star` and `unstar` API methods
642
	 */
643
	private function getStarringParameters() {
644
		// album IDs from newer clients
645
		$albumIds = $this->getRepeatedParam('albumId');
646
		$albumIds = \array_map('self::ripIdPrefix', $albumIds);
647
648
		// artist IDs from newer clients
649
		$artistIds = $this->getRepeatedParam('artistId');
650
		$artistIds = \array_map('self::ripIdPrefix', $artistIds);
651
652
		// song IDs from newer clients and song/folder/album/artist IDs from older clients
653
		$ids = $this->getRepeatedParam('id');
654
655
		$trackIds = [];
656
657
		foreach ($ids as $prefixedId) {
658
			$parts = \explode('-', $prefixedId);
659
			$type = $parts[0];
660
			$id = $parts[1];
661
662
			if ($type == 'track') {
663
				$trackIds[] = $id;
664
			} elseif ($type == 'album') {
665
				$albumIds[] = $id;
666
			} elseif ($type == 'artist') {
667
				$artistIds[] = $id;
668
			} elseif ($type == 'folder') {
669
				throw new SubsonicException('Starring folders is not supported', 0);
670
			} else {
671
				throw new SubsonicException("Unexpected ID format: $prefixedId", 0);
672
			}
673
		}
674
675
		return [
676
			'tracks' => $trackIds,
677
			'albums' => $albumIds,
678
			'artists' => $artistIds
679
		];
680
	}
681
682
	/** 
683
	 * Get values for parameter which may be present multiple times in the query
684
	 * string or POST data.
685
	 * @param string $paramName
686
	 * @return string[]
687
	 */
688
	private function getRepeatedParam($paramName) {
689
		// We can't use the IRequest object nor $_GET and $_POST to get the data
690
		// because all of these are based to the idea of unique parameter names.
691
		// If the same name is repeated, only the last value is saved. Hence, we
692
		// need to parse the raw data manually.
693
694
		// query string is always present (although it could be empty)
695
		$values = $this->parseRepeatedKeyValues($paramName, $_SERVER['QUERY_STRING']);
696
697
		// POST data is available if the method is POST
698
		if ($this->request->getMethod() == 'POST') {
699
			$values = \array_merge($values,
700
					$this->parseRepeatedKeyValues($paramName, file_get_contents('php://input')));
701
		}
702
703
		return $values;
704
	}
705
706
	/**
707
	 * Parse a string like "someKey=value1&someKey=value2&anotherKey=valueA&someKey=value3"
708
	 * and return an array of values for the given key
709
	 * @param string $key
710
	 * @param string $data
711
	 */
712
	private function parseRepeatedKeyValues($key, $data) {
713
		$result = [];
714
715
		$keyValuePairs = \explode('&', $data);
716
717
		foreach ($keyValuePairs as $pair) {
718
			$keyAndValue = \explode('=', $pair);
719
720
			if ($keyAndValue[0] == $key) {
721
				$result[] = $keyAndValue[1];
722
			}
723
		}
724
725
		return $result;
726
	}
727
728
	private function getFilesystemNode($id) {
729
		$rootFolder = $this->userMusicFolder->getFolder($this->userId);
730
		$nodes = $rootFolder->getById($id);
731
732
		if (\count($nodes) != 1) {
733
			throw new SubsonicException('file not found', 70);
734
		}
735
736
		return $nodes[0];
737
	}
738
739
	private function getIndexesForFolders() {
740
		$rootFolder = $this->userMusicFolder->getFolder($this->userId);
741
742
		return $this->subsonicResponse(['indexes' => ['index' => [
743
			['name' => '*',
744
			'artist' => [['id' => 'folder-' . $rootFolder->getId(), 'name' => $rootFolder->getName()]]]
745
		]]]);
746
	}
747
748
	private function getMusicDirectoryForFolder($id) {
749
		$folderId = self::ripIdPrefix($id); // get rid of 'folder-' prefix
750
		$folder = $this->getFilesystemNode($folderId);
751
752
		if (!($folder instanceof Folder)) {
753
			throw new SubsonicException("$id is not a valid folder", 70);
754
		}
755
756
		$nodes = $folder->getDirectoryListing();
757
		$subFolders = \array_filter($nodes, function ($n) {
758
			return $n instanceof Folder;
759
		});
760
		$tracks = $this->trackBusinessLayer->findAllByFolder($folderId, $this->userId);
761
762
		// A folder may contain thousands of audio files, and getting artist and album data
763
		// for each of those individually would take a lot of time and great many DB queries.
764
		// To prevent having to do this in `trackToApi`, we fetch all the albums and artists
765
		// in one go.
766
		$this->injectAlbumsArtistsAndGenresToTracks($tracks);
767
768
		$children = \array_merge(
769
			\array_map([$this, 'folderToApi'], $subFolders),
770
			\array_map([$this, 'trackToApi'], $tracks)
771
		);
772
773
		$content = [
774
			'directory' => [
775
				'id' => $id,
776
				'name' => $folder->getName(),
777
				'child' => $children
778
			]
779
		];
780
781
		// Parent folder ID is included if and only if the parent folder is not the top level
782
		$rootFolderId = $this->userMusicFolder->getFolder($this->userId)->getId();
783
		$parentFolderId = $folder->getParent()->getId();
784
		if ($rootFolderId != $parentFolderId) {
785
			$content['parent'] = 'folder-' . $parentFolderId;
786
		}
787
788
		return $this->subsonicResponse($content);
789
	}
790
791
	private function injectAlbumsArtistsAndGenresToTracks(&$tracks) {
792
		$albumIds = [];
793
		$artistIds = [];
794
795
		// get unique album and artist IDs
796
		foreach ($tracks as $track) {
797
			$albumIds[$track->getAlbumId()] = 1;
798
			$artistIds[$track->getArtistId()] = 1;
799
		}
800
		$albumIds = \array_keys($albumIds);
801
		$artistIds = \array_keys($artistIds);
802
803
		// get the corresponding entities from the business layer
804
		$albums = $this->albumBusinessLayer->findById($albumIds, $this->userId);
805
		$artists = $this->artistBusinessLayer->findById($artistIds, $this->userId);
806
		$genres = $this->genreBusinessLayer->findAll($this->userId);
807
808
		// create hash tables "id => entity" for the albums and artists for fast access
809
		$albumMap = Util::createIdLookupTable($albums);
810
		$artistMap = Util::createIdLookupTable($artists);
811
		$genreMap = Util::createIdLookupTable($genres);
812
813
		// finally, set the references on the tracks
814
		foreach ($tracks as &$track) {
815
			$track->setAlbum($albumMap[$track->getAlbumId()]);
816
			$track->setArtist($artistMap[$track->getArtistId()]);
817
			$track->setGenre($genreMap[$track->getGenreId()]);
818
		}
819
	}
820
821
	private function getIndexesForArtists($rootElementName = 'indexes') {
822
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, SortBy::Name);
823
	
824
		$indexes = [];
825
		foreach ($artists as $artist) {
826
			$indexes[$artist->getIndexingChar()][] = $this->artistToApi($artist);
827
		}
828
	
829
		$result = [];
830
		foreach ($indexes as $indexChar => $bucketArtists) {
831
			$result[] = ['name' => $indexChar, 'artist' => $bucketArtists];
832
		}
833
	
834
		return $this->subsonicResponse([$rootElementName => ['index' => $result]]);
835
	}
836
837
	private function getMusicDirectoryForArtist($id) {
838
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
839
840
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
841
		$artistName = $artist->getNameString($this->l10n);
842
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
843
844
		$children = [];
845
		foreach ($albums as $album) {
846
			$children[] = $this->albumToOldApi($album, $artistName);
847
		}
848
849
		return $this->subsonicResponse([
850
			'directory' => [
851
				'id' => $id,
852
				'name' => $artistName,
853
				'child' => $children
854
			]
855
		]);
856
	}
857
858
	private function getMusicDirectoryForAlbum($id) {
859
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
860
861
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
862
		$albumName = $album->getNameString($this->l10n);
863
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
864
865
		return $this->subsonicResponse([
866
			'directory' => [
867
				'id' => $id,
868
				'parent' => 'artist-' . $album->getAlbumArtistId(),
869
				'name' => $albumName,
870
				'child' => \array_map(function($track) use ($album) {
871
					$track->setAlbum($album);
872
					return $this->trackToApi($track);
873
				}, $tracks)
874
			]
875
		]);
876
	}
877
878
	/**
879
	 * @param Folder $folder
880
	 * @return array
881
	 */
882
	private function folderToApi($folder) {
883
		return [
884
			'id' => 'folder-' . $folder->getId(),
885
			'title' => $folder->getName(),
886
			'isDir' => true
887
		];
888
	}
889
890
	/**
891
	 * @param Artist $artist
892
	 * @return array
893
	 */
894
	private function artistToApi($artist) {
895
		$result = [
896
			'name' => $artist->getNameString($this->l10n),
897
			'id' => 'artist-' . $artist->getId(),
898
			'albumCount' => $this->albumBusinessLayer->countByArtist($artist->getId())
899
		];
900
901
		if ($artist->getStarred() != null) {
902
			$result['starred'] = $this->formatDateTime($artist->getStarred());
903
		}
904
905
		return $result;
906
	}
907
908
	/**
909
	 * The "old API" format is used e.g. in getMusicDirectory and getAlbumList
910
	 * @param Album $album
911
	 * @param string|null $artistName
912
	 * @return array
913
	 */
914
	private function albumToOldApi($album, $artistName = null) {
915
		$result = $this->albumCommonApiFields($album, $artistName);
916
917
		$result['parent'] = 'artist-' . $album->getAlbumArtistId();
918
		$result['title'] = $album->getNameString($this->l10n);
919
		$result['isDir'] = true;
920
921
		return $result;
922
	}
923
924
	/**
925
	 * The "new API" format is used e.g. in getAlbum and getAlbumList2
926
	 * @param Album $album
927
	 * @param string|null $artistName
928
	 * @return array
929
	 */
930
	private function albumToNewApi($album, $artistName = null) {
931
		$result = $this->albumCommonApiFields($album, $artistName);
932
933
		$result['artistId'] = 'artist-' . $album->getAlbumArtistId();
934
		$result['name'] = $album->getNameString($this->l10n);
935
		$result['songCount'] = $this->trackBusinessLayer->countByAlbum($album->getId());
936
		//$result['duration'] = 0;
937
938
		return $result;
939
	}
940
941
	private function albumCommonApiFields($album, $artistName = null) {
942
		if (empty($artistName)) {
943
			$artist = $this->artistBusinessLayer->find($album->getAlbumArtistId(), $this->userId);
944
			$artistName = $artist->getNameString($this->l10n);
945
		}
946
947
		$result = [
948
			'id' => 'album-' . $album->getId(),
949
			'artist' => $artistName
950
		];
951
952
		if (!empty($album->getCoverFileId())) {
953
			$result['coverArt'] = $album->getId();
954
		}
955
956
		if ($album->getStarred() != null) {
957
			$result['starred'] = $this->formatDateTime($album->getStarred());
958
		}
959
960
		if (!empty($album->getGenres())) {
961
			$result['genre'] = \implode(', ', \array_map(function($genreId) {
962
				return $this->genreBusinessLayer->find($genreId, $this->userId)->getNameString($this->l10n);
963
			}, $album->getGenres()));
964
		}
965
966
		if (!empty($album->getYears())) {
967
			$result['year'] = $album->yearToAPI();
968
		}
969
970
		return $result;
971
	}
972
973
	/**
974
	 * The same API format is used both on "old" and "new" API methods. The "new" API adds some
975
	 * new fields for the songs, but providing some extra fields shouldn't be a problem for the
976
	 * older clients. 
977
	 * @param Track $track If the track entity has no album, artist and/or genre references set, then
978
	 *                     those are automatically fetched from the respective BusinessLayer modules.
979
	 * @return array
980
	 */
981
	private function trackToApi($track) {
982
		$albumId = $track->getAlbumId();
983
984
		$album = $track->getAlbum();
985
		if (empty($album)) {
986
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
987
			$track->setAlbum($album);
988
		}
989
990
		$artist = $track->getArtist();
991
		if (empty($artist)) {
992
			$artist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
993
			$track->setArtist($artist);
994
		}
995
996
		$result = [
997
			'id' => 'track-' . $track->getId(),
998
			'parent' => 'album-' . $albumId,
999
			//'discNumber' => $track->getDisk(), // not supported on any of the tested clients => adjust track number instead
1000
			'title' => $track->getTitle(),
1001
			'artist' => $artist->getNameString($this->l10n),
1002
			'isDir' => false,
1003
			'album' => $album->getNameString($this->l10n),
1004
			'year' => $track->getYear(),
1005
			'size' => $track->getSize(),
1006
			'contentType' => $track->getMimetype(),
1007
			'suffix' => $track->getFileExtension(),
1008
			'duration' => $track->getLength() ?: 0,
1009
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
1010
			//'path' => '',
1011
			'isVideo' => false,
1012
			'albumId' => 'album-' . $albumId,
1013
			'artistId' => 'artist-' . $track->getArtistId(),
1014
			'type' => 'music'
1015
		];
1016
1017
		if (!empty($album->getCoverFileId())) {
1018
			$result['coverArt'] = $album->getId();
1019
		}
1020
1021
		$trackNumber = $track->getDiskAdjustedTrackNumber();
1022
		if ($trackNumber !== null) {
0 ignored issues
show
introduced by
The condition $trackNumber !== null is always true.
Loading history...
1023
			$result['track'] = $trackNumber;
1024
		}
1025
1026
		if ($track->getStarred() != null) {
1027
			$result['starred'] = $this->formatDateTime($track->getStarred());
1028
		}
1029
1030
		if (!empty($track->getGenreId())) {
1031
			$genre = $track->getGenre() ?: $this->genreBusinessLayer->find($track->getGenreId(), $this->userId);
1032
			$result['genre'] = $genre->getNameString($this->l10n);
1033
		}
1034
1035
		return $result;
1036
	}
1037
1038
	/**
1039
	 * @param Playlist $playlist
1040
	 * @return array
1041
	 */
1042
	private function playlistToApi($playlist) {
1043
		return [
1044
			'id' => $playlist->getId(),
1045
			'name' => $playlist->getName(),
1046
			'owner' => $this->userId,
1047
			'public' => false,
1048
			'songCount' => $playlist->getTrackCount(),
1049
			// comment => '',
1050
			// duration => '',
1051
			// created => '',
1052
			// coverArt => ''
1053
		];
1054
	}
1055
1056
	/**
1057
	 * Common logic for getAlbumList and getAlbumList2
1058
	 * @return Album[]
1059
	 */
1060
	private function albumsForGetAlbumList() {
1061
		$type = $this->getRequiredParam('type');
1062
		$size = $this->request->getParam('size', 10);
1063
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
1064
		$offset = $this->request->getParam('offset', 0);
1065
1066
		$albums = [];
1067
1068
		switch ($type) {
1069
			case 'random':
1070
				$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
1071
				$indices = $this->random->getIndices(\count($allAlbums), $offset, $size, $this->userId, 'subsonic_albums');
1072
				$albums = Util::arrayMultiGet($allAlbums, $indices);
1073
				break;
1074
			case 'starred':
1075
				$albums = $this->albumBusinessLayer->findAllStarred($this->userId, $size, $offset);
1076
				break;
1077
			case 'alphabeticalByName':
1078
				$albums = $this->albumBusinessLayer->findAll($this->userId, SortBy::Name, $size, $offset);
1079
				break;
1080
			case 'byGenre':
1081
				$genre = $this->getRequiredParam('genre');
1082
				$albums = $this->findAlbumsByGenre($genre, $size, $offset);
1083
				break;
1084
			case 'alphabeticalByArtist':
1085
			case 'newest':
1086
			case 'highest':
1087
			case 'frequent':
1088
			case 'recent':
1089
			case 'byYear':
1090
			default:
1091
				$this->logger->log("Album list type '$type' is not supported", 'debug');
1092
				break;
1093
		}
1094
1095
		return $albums;
1096
	}
1097
1098
	/**
1099
	 * Common logic for search2 and search3
1100
	 * @return array with keys 'artists', 'albums', and 'tracks'
1101
	 */
1102
	private function doSearch() {
1103
		$query = $this->getRequiredParam('query');
1104
		$artistCount = $this->request->getParam('artistCount', 20);
1105
		$artistOffset = $this->request->getParam('artistOffset', 0);
1106
		$albumCount = $this->request->getParam('albumCount', 20);
1107
		$albumOffset = $this->request->getParam('albumOffset', 0);
1108
		$songCount = $this->request->getParam('songCount', 20);
1109
		$songOffset = $this->request->getParam('songOffset', 0);
1110
1111
		if (empty($query)) {
1112
			throw new SubsonicException("The 'query' argument is mandatory", 10);
1113
		}
1114
1115
		return [
1116
			'artists' => $this->artistBusinessLayer->findAllByName($query, $this->userId, true, $artistCount, $artistOffset),
1117
			'albums' => $this->albumBusinessLayer->findAllByName($query, $this->userId, true, $albumCount, $albumOffset),
1118
			'tracks' => $this->trackBusinessLayer->findAllByName($query, $this->userId, true, $songCount, $songOffset)
1119
		];
1120
	}
1121
1122
	/**
1123
	 * Common logic for getStarred and getStarred2
1124
	 * @return array
1125
	 */
1126
	private function doGetStarred() {
1127
		return [
1128
			'artists' => $this->artistBusinessLayer->findAllStarred($this->userId),
1129
			'albums' => $this->albumBusinessLayer->findAllStarred($this->userId),
1130
			'tracks' => $this->trackBusinessLayer->findAllStarred($this->userId)
1131
		];
1132
	}
1133
1134
	/**
1135
	 * Common response building logic for search2, search3, getStarred, and getStarred2
1136
	 * @param string $title Name of the main node in the response message
1137
	 * @param array $results Search results with keys 'artists', 'albums', and 'tracks'
1138
	 * @param boolean $useNewApi Set to true for search3 and getStarred3. There is a difference
1139
	 *                           in the formatting of the album nodes.
1140
	 * @return \OCP\AppFramework\Http\Response
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\Response 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...
1141
	 */
1142
	private function searchResponse($title, $results, $useNewApi) {
1143
		$albumMapFunc = $useNewApi ? 'albumToNewApi' : 'albumToOldApi';
1144
1145
		return $this->subsonicResponse([$title => [
1146
			'artist' => \array_map([$this, 'artistToApi'], $results['artists']),
1147
			'album' => \array_map([$this, $albumMapFunc], $results['albums']),
1148
			'song' => \array_map([$this, 'trackToApi'], $results['tracks'])
1149
		]]);
1150
	}
1151
1152
	/**
1153
	 * Find tracks by genre name
1154
	 * @param string $genreName
1155
	 * @param int|null $limit
1156
	 * @param int|null $offset
1157
	 * @return Track[]
1158
	 */
1159
	private function findTracksByGenre($genreName, $limit=null, $offset=null) {
1160
		$genre = $this->findGenreByName($genreName);
1161
1162
		if ($genre) {
1163
			return $this->trackBusinessLayer->findAllByGenre($genre->getId(), $this->userId, $limit, $offset);
1164
		} else {
1165
			return [];
1166
		}
1167
	}
1168
1169
	/**
1170
	 * Find albums by genre name
1171
	 * @param string $genreName
1172
	 * @param int|null $limit
1173
	 * @param int|null $offset
1174
	 * @return Album[]
1175
	 */
1176
	private function findAlbumsByGenre($genreName, $limit=null, $offset=null) {
1177
		$genre = $this->findGenreByName($genreName);
1178
1179
		if ($genre) {
1180
			return $this->albumBusinessLayer->findAllByGenre($genre->getId(), $this->userId, $limit, $offset);
1181
		} else {
1182
			return [];
1183
		}
1184
	}
1185
1186
	private function findGenreByName($name) {
1187
		$genreArr = $this->genreBusinessLayer->findAllByName($name, $this->userId);
1188
		if (\count($genreArr) == 0 && $genreName == Genre::unknownGenreName($this->l10n)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $genreName seems to be never defined.
Loading history...
1189
			$genreArr = $this->genreBusinessLayer->findAllByName('', $this->userId);
1190
		}
1191
		return \count($genreArr) ? $genreArr[0] : null;
1192
	}
1193
1194
	/**
1195
	 * Given a prefixed ID like 'artist-123' or 'track-45', return just the numeric part.
1196
	 * @param string $id
1197
	 * @return integer
1198
	 */
1199
	private static function ripIdPrefix($id) {
1200
		return (int)(\explode('-', $id)[1]);
1201
	}
1202
1203
	private function formatDateTime($dateString) {
1204
		$dateTime = new \DateTime($dateString);
1205
		$dateTime->setTimezone($this->timezone);
1206
		return $dateTime->format('Y-m-d\TH:i:s');
1207
	}
1208
1209
	private function subsonicResponse($content, $status = 'ok') {
1210
		$content['status'] = $status; 
1211
		$content['version'] = self::API_VERSION;
1212
		$responseData = ['subsonic-response' => $content];
1213
1214
		if ($this->format == 'json') {
1215
			$response = new JSONResponse($responseData);
1216
		} else if ($this->format == 'jsonp') {
1217
			$responseData = \json_encode($responseData);
1218
			$response = new DataDisplayResponse("{$this->callback}($responseData);");
1219
			$response->addHeader('Content-Type', 'text/javascript; charset=UTF-8');
1220
		} else {
1221
			$response = new XMLResponse($responseData);
1222
		}
1223
1224
		return $response;
1225
	}
1226
1227
	public function subsonicErrorResponse($errorCode, $errorMessage) {
1228
		return $this->subsonicResponse([
1229
				'error' => [
1230
					'code' => $errorCode,
1231
					'message' => $errorMessage
1232
				]
1233
			], 'failed');
1234
	}
1235
}
1236