Passed
Push — feature/329_Subsonic_API ( a9dee6...9d1353 )
by Pauli
14:33
created

SubsonicController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 12
nc 1
nop 13
dl 0
loc 26
rs 9.8666
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\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\Files\IRootFolder;
0 ignored issues
show
Bug introduced by
The type OCP\Files\IRootFolder 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\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...
22
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...
23
24
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
25
use \OCA\Music\AppFramework\Core\Logger;
26
use \OCA\Music\AppFramework\Utility\MethodAnnotationReader;
27
28
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
29
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
30
use \OCA\Music\BusinessLayer\Library;
31
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
32
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
33
34
use \OCA\Music\Db\SortBy;
35
36
use \OCA\Music\Http\FileResponse;
37
use \OCA\Music\Http\XMLResponse;
38
39
use \OCA\Music\Middleware\SubsonicException;
40
41
use \OCA\Music\Utility\CoverHelper;
42
use \OCA\Music\Utility\UserMusicFolder;
43
use \OCA\Music\Utility\Util;
44
45
class SubsonicController extends Controller {
46
	const API_VERSION = '1.4.0';
47
48
	private $albumBusinessLayer;
49
	private $artistBusinessLayer;
50
	private $playlistBusinessLayer;
51
	private $trackBusinessLayer;
52
	private $library;
53
	private $urlGenerator;
54
	private $rootFolder;
55
	private $userMusicFolder;
56
	private $l10n;
57
	private $coverHelper;
58
	private $logger;
59
	private $userId;
60
	private $format;
61
	private $callback;
62
63
	public function __construct($appname,
64
								IRequest $request,
65
								$l10n,
66
								IURLGenerator $urlGenerator,
67
								AlbumBusinessLayer $albumBusinessLayer,
68
								ArtistBusinessLayer $artistBusinessLayer,
69
								PlaylistBusinessLayer $playlistBusinessLayer,
70
								TrackBusinessLayer $trackBusinessLayer,
71
								Library $library,
72
								IRootFolder $rootFolder,
73
								UserMusicFolder $userMusicFolder,
74
								CoverHelper $coverHelper,
75
								Logger $logger) {
76
		parent::__construct($appname, $request);
77
78
		$this->albumBusinessLayer = $albumBusinessLayer;
79
		$this->artistBusinessLayer = $artistBusinessLayer;
80
		$this->playlistBusinessLayer = $playlistBusinessLayer;
81
		$this->trackBusinessLayer = $trackBusinessLayer;
82
		$this->library = $library;
83
		$this->urlGenerator = $urlGenerator;
84
		$this->l10n = $l10n;
85
		$this->rootFolder = $rootFolder;
86
		$this->userMusicFolder = $userMusicFolder;
87
		$this->coverHelper = $coverHelper;
88
		$this->logger = $logger;
89
	}
90
91
	/**
92
	 * Called by the middleware once the user credentials have been checked
93
	 * @param string $userId
94
	 */
95
	public function setAuthenticatedUser($userId) {
96
		$this->userId = $userId;
97
	}
98
99
	/**
100
	 * @NoAdminRequired
101
	 * @PublicPage
102
	 * @NoCSRFRequired
103
	 */
104
	public function handleRequest($method) {
105
		$this->format = $this->request->getParam('f', 'xml');
106
		$this->callback = $this->request->getParam('callback');
107
108
		if (!\in_array($this->format, ['json', 'xml', 'jsonp'])) {
109
			throw new SubsonicException("Unsupported format {$this->format}", 0);
110
		}
111
112
		if ($this->format === 'jsonp' && empty($this->callback)) {
113
			$this->format = 'json';
114
			throw new SubsonicException("Argument 'callback' is required with jsonp format", 10);
115
		}
116
117
		// Allow calling all methods with or without the postfix ".view"
118
		if (Util::endsWith($method, ".view")) {
119
			$method = \substr($method, 0, -\strlen(".view"));
120
		}
121
122
		// Allow calling any functions annotated to be part of the API
123
		if (\method_exists($this, $method)) {
124
			$annotationReader = new MethodAnnotationReader($this, $method);
125
			if ($annotationReader->hasAnnotation('SubsonicAPI')) {
126
				return $this->$method();
127
			}
128
		}
129
130
		$this->logger->log("Request $method not supported", 'warn');
131
		return $this->subsonicErrorResponse(70, "Requested action $method is not supported");
132
	}
133
134
	/* -------------------------------------------------------------------------
135
	 * REST API methods
136
	 *------------------------------------------------------------------------*/
137
138
	/**
139
	 * @SubsonicAPI
140
	 */
141
	private function ping() {
142
		return $this->subsonicResponse([]);
143
	}
144
145
	/**
146
	 * @SubsonicAPI
147
	 */
148
	private function getLicense() {
149
		return $this->subsonicResponse([
150
			'license' => [
151
				'valid' => 'true',
152
				'email' => '',
153
				'licenseExpires' => 'never'
154
			]
155
		]);
156
	}
157
158
	/**
159
	 * @SubsonicAPI
160
	 */
161
	private function getMusicFolders() {
162
		// Only single root folder is supported
163
		return $this->subsonicResponse([
164
			'musicFolders' => ['musicFolder' => [
165
				['id' => 'artists', 'name' => $this->l10n->t('Artists')],
166
				['id' => 'folders', 'name' => $this->l10n->t('Folders')]
167
			]]
168
		]);
169
	}
170
171
	/**
172
	 * @SubsonicAPI
173
	 */
174
	private function getIndexes() {
175
		$id = $this->request->getParam('musicFolderId');
176
177
		if ($id === 'folders') {
178
			return $this->getIndexesForFolders();
179
		} else {
180
			return $this->getIndexesForArtists();
181
		}
182
	}
183
184
	/**
185
	 * @SubsonicAPI
186
	 */
187
	private function getMusicDirectory() {
188
		$id = $this->getRequiredParam('id');
189
190
		if (Util::startsWith($id, 'folder-')) {
191
			return $this->getMusicDirectoryForFolder($id);
192
		} elseif (Util::startsWith($id, 'artist-')) {
193
			return $this->getMusicDirectoryForArtist($id);
194
		} else {
195
			return $this->getMusicDirectoryForAlbum($id);
196
		}
197
	}
198
199
	/**
200
	 * @SubsonicAPI
201
	 */
202
	private function getAlbumList() {
203
		$type = $this->getRequiredParam('type');
204
		$size = $this->request->getParam('size', 10);
205
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
206
		// $offset = $this->request->getParam('offset', 0); parameter not supported for now
207
208
		$albums = [];
209
		if ($type == 'random') {
210
			$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
211
			$albums = self::randomItems($allAlbums, $size);
212
		}
213
		// TODO: support 'newest', 'highest', 'frequent', 'recent'
214
215
		return $this->subsonicResponse(['albumList' =>
216
				['album' => \array_map([$this, 'albumAsChild'], $albums)]
217
		]);
218
	}
219
220
	/**
221
	 * @SubsonicAPI
222
	 */
223
	private function getRandomSongs() {
224
		$size = $this->request->getParam('size', 10);
225
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
226
		// $genre = $this->request->getParam('genre'); not supported
227
		// $fromYear = $this->request->getParam('fromYear'); not supported
228
		// $toYear = $this->request->getParam('genre'); not supported
229
230
		$allTracks = $this->trackBusinessLayer->findAll($this->userId);
231
		$tracks = self::randomItems($allTracks, $size);
232
233
		return $this->subsonicResponse(['randomSongs' =>
234
				['song' => \array_map([$this, 'trackAsChild'], $tracks)]
235
		]);
236
	}
237
238
	/**
239
	 * @SubsonicAPI
240
	 */
241
	private function getCoverArt() {
242
		$id = $this->getRequiredParam('id');
243
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
244
		$coverData = $this->coverHelper->getCover($id, $this->userId, $userFolder);
245
246
		if ($coverData !== null) {
0 ignored issues
show
introduced by
The condition $coverData !== null is always true.
Loading history...
247
			return new FileResponse($coverData);
248
		}
249
250
		return $this->subsonicErrorResponse(70, 'album has no cover');
251
	}
252
253
	/**
254
	 * @SubsonicAPI
255
	 */
256
	private function stream() {
257
		// We don't support transcaoding, so 'stream' and 'download' act identically
258
		return $this->download();
259
	}
260
261
	/**
262
	 * @SubsonicAPI
263
	 */
264
	private function download() {
265
		$id = $this->getRequiredParam('id');
266
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
267
268
		$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

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

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

442
		$tracks = $this->trackBusinessLayer->findAllByFolder(/** @scrutinizer ignore-type */ $folderId, $this->userId);
Loading history...
443
444
		$children = \array_merge(
445
			\array_map([$this, 'folderAsChild'], $subFolders),
446
			\array_map([$this, 'trackAsChild'], $tracks)
447
		);
448
449
		return $this->subsonicResponse([
450
			'directory' => [
451
				'id' => $id,
452
				'parent' => 'folder-' . $folder->getParent()->getId(),
453
				'name' => $folder->getName(),
454
				'child' => $children
455
			]
456
		]);
457
	}
458
459
	private function getIndexesForArtists() {
460
		$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

460
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, /** @scrutinizer ignore-type */ SortBy::Name);
Loading history...
461
	
462
		$indexes = [];
463
		foreach ($artists as $artist) {
464
			$indexes[$artist->getIndexingChar()][] = $this->artistAsChild($artist);
465
		}
466
	
467
		$result = [];
468
		foreach ($indexes as $indexChar => $bucketArtists) {
469
			$result[] = ['name' => $indexChar, 'artist' => $bucketArtists];
470
		}
471
	
472
		return $this->subsonicResponse(['indexes' => ['index' => $result]]);
473
	}
474
475
	private function getMusicDirectoryForArtist($id) {
476
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
477
478
		$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

478
		$artist = $this->artistBusinessLayer->find(/** @scrutinizer ignore-type */ $artistId, $this->userId);
Loading history...
479
		$artistName = $artist->getNameString($this->l10n);
480
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
481
482
		$children = [];
483
		foreach ($albums as $album) {
484
			$children[] = $this->albumAsChild($album, $artistName);
485
		}
486
487
		return $this->subsonicResponse([
488
			'directory' => [
489
				'id' => $id,
490
				'parent' => 'artists',
491
				'name' => $artistName,
492
				'child' => $children
493
			]
494
		]);
495
	}
496
497
	private function getMusicDirectoryForAlbum($id) {
498
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
499
500
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
501
		$albumName = $album->getNameString($this->l10n);
502
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
503
504
		return $this->subsonicResponse([
505
			'directory' => [
506
				'id' => $id,
507
				'parent' => 'artist-' . $album->getAlbumArtistId(),
508
				'name' => $albumName,
509
				'child' => \array_map([$this, 'trackAsChild'], $tracks)
510
			]
511
		]);
512
	}
513
514
	private function folderAsChild($folder) {
515
		return [
516
			'id' => 'folder-' . $folder->getId(),
517
			'title' => $folder->getName(),
518
			'isDir' => true
519
		];
520
	}
521
	private function artistAsChild($artist) {
522
		return [
523
			'name' => $artist->getNameString($this->l10n),
524
			'id' => 'artist-' . $artist->getId()
525
		];
526
	}
527
528
	private function albumAsChild($album, $artistName = null) {
529
		$artistId = $album->getAlbumArtistId();
530
531
		if (empty($artistName)) {
532
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
533
			$artistName = $artist->getNameString($this->l10n);
534
		}
535
536
		$result = [
537
			'id' => 'album-' . $album->getId(),
538
			'parent' => 'artist-' . $artistId,
539
			'title' => $album->getNameString($this->l10n),
540
			'artist' => $artistName,
541
			'isDir' => true
542
		];
543
544
		if (!empty($album->getCoverFileId())) {
545
			$result['coverArt'] = $album->getId();
546
		}
547
548
		return $result;
549
	}
550
551
	private function trackAsChild($track, $album = null, $albumName = null) {
552
		$albumId = $track->getAlbumId();
553
		if ($album == null) {
554
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
555
		}
556
		if (empty($albumName)) {
557
			$albumName = $album->getNameString($this->l10n);
558
		}
559
560
		$trackArtist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
561
		$result = [
562
			'id' => 'track-' . $track->getId(),
563
			'parent' => 'album-' . $albumId,
564
			'title' => $track->getTitle(),
565
			'artist' => $trackArtist->getNameString($this->l10n),
566
			'isDir' => false,
567
			'album' => $albumName,
568
			//'genre' => '',
569
			'year' => $track->getYear(),
570
			'size' => $track->getSize(),
571
			'contentType' => $track->getMimetype(),
572
			'suffix' => \end(\explode('.', $track->getFilename())),
573
			'duration' => $track->getLength() ?: 0,
574
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
575
			//'path' => ''
576
		];
577
578
		if (!empty($album->getCoverFileId())) {
579
			$result['coverArt'] = $album->getId();
580
		}
581
582
		if ($track->getNumber() !== null) {
583
			$result['track'] = $track->getNumber();
584
		}
585
586
		return $result;
587
	}
588
589
	private function playlistAsChild($playlist) {
590
		return [
591
			'id' => $playlist->getId(),
592
			'name' => $playlist->getName(),
593
			'owner' => $this->userId,
594
			'public' => false,
595
			'songCount' => $playlist->getTrackCount(),
596
			// comment => '',
597
			// duration => '',
598
			// created => '',
599
			// coverArt => ''
600
		];
601
	}
602
603
	/**
604
	 * Given a prefixed ID like 'artist-123' or 'track-45', return just the numeric part.
605
	 * @param string $id
606
	 * @return string
607
	 */
608
	private static function ripIdPrefix($id) {
609
		return \explode('-', $id)[1];
610
	}
611
612
	private static function randomItems($itemArray, $count) {
613
		$count = \min($count, \count($itemArray)); // can't return more than all items
614
		$indices = \array_rand($itemArray, $count);
615
		if ($count == 1) { // return type is not array when randomizing a single index
616
			$indices = [$indices];
617
		}
618
619
		$result = [];
620
		foreach ($indices as $index) {
621
			$result[] = $itemArray[$index];
622
		}
623
624
		return $result;
625
	}
626
627
	private function subsonicResponse($content, $status = 'ok') {
628
		$content['status'] = $status; 
629
		$content['version'] = self::API_VERSION;
630
		$responseData = ['subsonic-response' => $content];
631
632
		if ($this->format == 'json') {
633
			$response = new JSONResponse($responseData);
634
		} else if ($this->format == 'jsonp') {
635
			$responseData = \json_encode($responseData);
636
			$response = new DataDisplayResponse("{$this->callback}($responseData);");
637
			$response->addHeader('Content-Type', 'text/javascript; charset=UTF-8');
638
		} else {
639
			$response = new XMLResponse($responseData);
640
		}
641
642
		return $response;
643
	}
644
645
	public function subsonicErrorResponse($errorCode, $errorMessage) {
646
		return $this->subsonicResponse([
647
				'error' => [
648
					'code' => $errorCode,
649
					'message' => $errorMessage
650
				]
651
			], 'failed');
652
	}
653
}
654