Passed
Push — feature/329_Subsonic_API ( 9783d2...87cb9d )
by Pauli
11:09
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
		$albums = $this->albumsForGetAlbumList();
204
		return $this->subsonicResponse(['albumList' =>
205
				['album' => \array_map([$this, 'albumAsChild'], $albums)]
206
		]);
207
	}
208
209
	/**
210
	 * @SubsonicAPI
211
	 */
212
	private function getAlbumList2() {
213
		/*
214
		 *  According to the API specification, the difference between this and getAlbumList
215
		 * should be that this function would organize albums according the metadata while
216
		 * getAlbumList would organize them by folders. However, we organize by metadata
217
		 * also in getAlbumList, because that's more natural for the Music app and many/most
218
		 * clients do not support getAlbumList2.
219
		 */
220
		$albums = $this->albumsForGetAlbumList();
221
		return $this->subsonicResponse(['albumList2' =>
222
				['album' => \array_map([$this, 'albumToNewApi'], $albums)]
223
		]);
224
	}
225
226
	/**
227
	 * @SubsonicAPI
228
	 */
229
	private function getAlbum() {
230
		$id = $this->getRequiredParam('id');
231
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
232
233
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
234
		$albumName = $album->getNameString($this->l10n);
0 ignored issues
show
Unused Code introduced by
The assignment to $albumName is dead and can be removed.
Loading history...
235
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
236
237
		$albumNode = $this->albumToNewApi($album);
238
		$albumNode['song'] = \array_map([$this, 'trackToNewApi'], $tracks);
239
240
		return $this->subsonicResponse(['album' => $albumNode]);
241
	}
242
243
	/**
244
	 * @SubsonicAPI
245
	 */
246
	private function getRandomSongs() {
247
		$size = $this->request->getParam('size', 10);
248
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
249
		// $genre = $this->request->getParam('genre'); not supported
250
		// $fromYear = $this->request->getParam('fromYear'); not supported
251
		// $toYear = $this->request->getParam('genre'); not supported
252
253
		$allTracks = $this->trackBusinessLayer->findAll($this->userId);
254
		$tracks = self::randomItems($allTracks, $size);
255
256
		return $this->subsonicResponse(['randomSongs' =>
257
				['song' => \array_map([$this, 'trackAsChild'], $tracks)]
258
		]);
259
	}
260
261
	/**
262
	 * @SubsonicAPI
263
	 */
264
	private function getCoverArt() {
265
		$id = $this->getRequiredParam('id');
266
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
267
		$coverData = $this->coverHelper->getCover($id, $this->userId, $userFolder);
268
269
		if ($coverData !== null) {
0 ignored issues
show
introduced by
The condition $coverData !== null is always true.
Loading history...
270
			return new FileResponse($coverData);
271
		}
272
273
		return $this->subsonicErrorResponse(70, 'album has no cover');
274
	}
275
276
	/**
277
	 * @SubsonicAPI
278
	 */
279
	private function stream() {
280
		// We don't support transcaoding, so 'stream' and 'download' act identically
281
		return $this->download();
282
	}
283
284
	/**
285
	 * @SubsonicAPI
286
	 */
287
	private function download() {
288
		$id = $this->getRequiredParam('id');
289
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
290
291
		$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

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

465
		$tracks = $this->trackBusinessLayer->findAllByFolder(/** @scrutinizer ignore-type */ $folderId, $this->userId);
Loading history...
466
467
		$children = \array_merge(
468
			\array_map([$this, 'folderAsChild'], $subFolders),
469
			\array_map([$this, 'trackAsChild'], $tracks)
470
		);
471
472
		return $this->subsonicResponse([
473
			'directory' => [
474
				'id' => $id,
475
				'parent' => 'folder-' . $folder->getParent()->getId(),
476
				'name' => $folder->getName(),
477
				'child' => $children
478
			]
479
		]);
480
	}
481
482
	private function getIndexesForArtists() {
483
		$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

483
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, /** @scrutinizer ignore-type */ SortBy::Name);
Loading history...
484
	
485
		$indexes = [];
486
		foreach ($artists as $artist) {
487
			$indexes[$artist->getIndexingChar()][] = $this->artistAsChild($artist);
488
		}
489
	
490
		$result = [];
491
		foreach ($indexes as $indexChar => $bucketArtists) {
492
			$result[] = ['name' => $indexChar, 'artist' => $bucketArtists];
493
		}
494
	
495
		return $this->subsonicResponse(['indexes' => ['index' => $result]]);
496
	}
497
498
	private function getMusicDirectoryForArtist($id) {
499
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
500
501
		$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

501
		$artist = $this->artistBusinessLayer->find(/** @scrutinizer ignore-type */ $artistId, $this->userId);
Loading history...
502
		$artistName = $artist->getNameString($this->l10n);
503
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
504
505
		$children = [];
506
		foreach ($albums as $album) {
507
			$children[] = $this->albumAsChild($album, $artistName);
508
		}
509
510
		return $this->subsonicResponse([
511
			'directory' => [
512
				'id' => $id,
513
				'parent' => 'artists',
514
				'name' => $artistName,
515
				'child' => $children
516
			]
517
		]);
518
	}
519
520
	private function getMusicDirectoryForAlbum($id) {
521
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
522
523
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
524
		$albumName = $album->getNameString($this->l10n);
525
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
526
527
		return $this->subsonicResponse([
528
			'directory' => [
529
				'id' => $id,
530
				'parent' => 'artist-' . $album->getAlbumArtistId(),
531
				'name' => $albumName,
532
				'child' => \array_map([$this, 'trackAsChild'], $tracks)
533
			]
534
		]);
535
	}
536
537
	private function folderAsChild($folder) {
538
		return [
539
			'id' => 'folder-' . $folder->getId(),
540
			'title' => $folder->getName(),
541
			'isDir' => true
542
		];
543
	}
544
	private function artistAsChild($artist) {
545
		return [
546
			'name' => $artist->getNameString($this->l10n),
547
			'id' => 'artist-' . $artist->getId()
548
		];
549
	}
550
551
	private function albumAsChild($album, $artistName = null) {
552
		$artistId = $album->getAlbumArtistId();
553
554
		if (empty($artistName)) {
555
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
556
			$artistName = $artist->getNameString($this->l10n);
557
		}
558
559
		$result = [
560
			'id' => 'album-' . $album->getId(),
561
			'parent' => 'artist-' . $artistId,
562
			'title' => $album->getNameString($this->l10n),
563
			'artist' => $artistName,
564
			'isDir' => true
565
		];
566
567
		if (!empty($album->getCoverFileId())) {
568
			$result['coverArt'] = $album->getId();
569
		}
570
571
		return $result;
572
	}
573
574
	private function albumToNewApi($album, $artistName = null) {
575
		$artistId = $album->getAlbumArtistId();
576
577
		if (empty($artistName)) {
578
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
579
			$artistName = $artist->getNameString($this->l10n);
580
		}
581
582
		$result = [
583
			'id' => 'album-' . $album->getId(),
584
			'artistId' => 'artist-' . $artistId,
585
			'name' => $album->getNameString($this->l10n),
586
			'artist' => $artistName,
587
			'songCount' => $this->trackBusinessLayer->countByAlbum($album->getId()),
588
			//'duration' => 0
589
		];
590
	
591
		if (!empty($album->getCoverFileId())) {
592
			$result['coverArt'] = $album->getId();
593
		}
594
	
595
		return $result;
596
	}
597
598
	private function trackAsChild($track, $album = null, $albumName = null) {
599
		$albumId = $track->getAlbumId();
600
		if ($album == null) {
601
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
602
		}
603
		if (empty($albumName)) {
604
			$albumName = $album->getNameString($this->l10n);
605
		}
606
607
		$trackArtist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
608
		$result = [
609
			'id' => 'track-' . $track->getId(),
610
			'parent' => 'album-' . $albumId,
611
			'title' => $track->getTitle(),
612
			'artist' => $trackArtist->getNameString($this->l10n),
613
			'isDir' => false,
614
			'album' => $albumName,
615
			//'genre' => '',
616
			'year' => $track->getYear(),
617
			'size' => $track->getSize(),
618
			'contentType' => $track->getMimetype(),
619
			'suffix' => \end(\explode('.', $track->getFilename())),
620
			'duration' => $track->getLength() ?: 0,
621
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
622
			//'path' => ''
623
		];
624
625
		if (!empty($album->getCoverFileId())) {
626
			$result['coverArt'] = $album->getId();
627
		}
628
629
		if ($track->getNumber() !== null) {
630
			$result['track'] = $track->getNumber();
631
		}
632
633
		return $result;
634
	}
635
636
	private function trackToNewApi($track, $album = null, $albumName = null) {
637
		$albumId = $track->getAlbumId();
638
		if ($album == null) {
639
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
640
		}
641
		if (empty($albumName)) {
642
			$albumName = $album->getNameString($this->l10n);
643
		}
644
645
		$trackArtist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
646
		$result = [
647
			'id' => 'track-' . $track->getId(),
648
			'parent' => 'album-' . $albumId,
649
			'title' => $track->getTitle(),
650
			'artist' => $trackArtist->getNameString($this->l10n),
651
			'isDir' => false,
652
			'album' => $albumName,
653
			//'genre' => '',
654
			'year' => $track->getYear(),
655
			'size' => $track->getSize(),
656
			'contentType' => $track->getMimetype(),
657
			'suffix' => \end(\explode('.', $track->getFilename())),
658
			'duration' => $track->getLength() ?: 0,
659
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
660
			//'path' => '',
661
			'isVideo' => false,
662
			'albumId' => 'album-' . $albumId,
663
			'artistId' => 'artist-' . $track->getArtistId(),
664
			'type' => 'music',
665
		];
666
667
		if (!empty($album->getCoverFileId())) {
668
			$result['coverArt'] = $album->getId();
669
		}
670
671
		if ($track->getNumber() !== null) {
672
			$result['track'] = $track->getNumber();
673
		}
674
675
		return $result;
676
	}
677
678
	private function playlistAsChild($playlist) {
679
		return [
680
			'id' => $playlist->getId(),
681
			'name' => $playlist->getName(),
682
			'owner' => $this->userId,
683
			'public' => false,
684
			'songCount' => $playlist->getTrackCount(),
685
			// comment => '',
686
			// duration => '',
687
			// created => '',
688
			// coverArt => ''
689
		];
690
	}
691
692
	/**
693
	 * Common logic for getAlbumList and getAlbumList2
694
	 * @return Album[]
695
	 */
696
	private function albumsForGetAlbumList() {
697
		$type = $this->getRequiredParam('type');
698
		$size = $this->request->getParam('size', 10);
699
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
700
		$offset = $this->request->getParam('offset', 0);
701
702
		$albums = [];
703
704
		switch ($type) {
705
			case 'random':
706
				$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
707
				$albums = self::randomItems($allAlbums, $size);
708
				// Note: offset is not supported on this type
709
				break;
710
			case 'alphabeticalByName':
711
				$albums = $this->albumBusinessLayer->findAll($this->userId, SortBy::Name, $size, $offset);
712
				break;
713
			case 'alphabeticalByArtist':
714
			case 'newest':
715
			case 'highest':
716
			case 'frequent':
717
			case 'recent':
718
			case 'starred':
719
			case 'byYear':
720
			case 'byGenre':
721
			default:
722
				$this->logger->log("Album list type '$type' is not supported", 'warn');
723
				break;
724
		}
725
726
		return $albums;
727
	}
728
729
	/**
730
	 * Given a prefixed ID like 'artist-123' or 'track-45', return just the numeric part.
731
	 * @param string $id
732
	 * @return string
733
	 */
734
	private static function ripIdPrefix($id) {
735
		return \explode('-', $id)[1];
736
	}
737
738
	private static function randomItems($itemArray, $count) {
739
		$count = \min($count, \count($itemArray)); // can't return more than all items
740
		$indices = \array_rand($itemArray, $count);
741
		if ($count == 1) { // return type is not array when randomizing a single index
742
			$indices = [$indices];
743
		}
744
745
		$result = [];
746
		foreach ($indices as $index) {
747
			$result[] = $itemArray[$index];
748
		}
749
750
		return $result;
751
	}
752
753
	private function subsonicResponse($content, $status = 'ok') {
754
		$content['status'] = $status; 
755
		$content['version'] = self::API_VERSION;
756
		$responseData = ['subsonic-response' => $content];
757
758
		if ($this->format == 'json') {
759
			$response = new JSONResponse($responseData);
760
		} else if ($this->format == 'jsonp') {
761
			$responseData = \json_encode($responseData);
762
			$response = new DataDisplayResponse("{$this->callback}($responseData);");
763
			$response->addHeader('Content-Type', 'text/javascript; charset=UTF-8');
764
		} else {
765
			$response = new XMLResponse($responseData);
766
		}
767
768
		return $response;
769
	}
770
771
	public function subsonicErrorResponse($errorCode, $errorMessage) {
772
		return $this->subsonicResponse([
773
				'error' => [
774
					'code' => $errorCode,
775
					'message' => $errorMessage
776
				]
777
			], 'failed');
778
	}
779
}
780