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

SubsonicController::getIndexesForArtists()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 4
nop 0
dl 0
loc 14
rs 10
c 0
b 0
f 0
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