Passed
Push — feature/329_Subsonic_API ( 87cb9d...0c15c8 )
by Pauli
12:53
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, 'albumToOldApi'], $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, 'trackToApi'], $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, 'trackToApi'], $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, 'artistToApi'], $artists);
324
		}
325
		if (!empty($albums)) {
326
			$results['album'] = \array_map([$this, 'albumToOldApi'], $albums);
327
		}
328
		if (!empty($tracks)) {
329
			$results['song'] = \array_map([$this, 'trackToApi'], $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, 'playlistToApi'], $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->playlistToApi($playlist);
355
		$playlistNode['entry'] = \array_map([$this, 'trackToApi'], $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, 'folderToApi'], $subFolders),
469
			\array_map([$this, 'trackToApi'], $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->artistToApi($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->albumToOldApi($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, 'trackToApi'], $tracks)
533
			]
534
		]);
535
	}
536
537
	private function folderToApi($folder) {
538
		return [
539
			'id' => 'folder-' . $folder->getId(),
540
			'title' => $folder->getName(),
541
			'isDir' => true
542
		];
543
	}
544
545
	private function artistToApi($artist) {
546
		return [
547
			'name' => $artist->getNameString($this->l10n),
548
			'id' => 'artist-' . $artist->getId()
549
		];
550
	}
551
552
	/**
553
	 * The "old API" format is used e.g. in getMusicDirectory and getAlbumList
554
	 * @param Album $album
0 ignored issues
show
Bug introduced by
The type OCA\Music\Controller\Album 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...
555
	 * @param string $artistName
556
	 * @return array
557
	 */
558
	private function albumToOldApi($album, $artistName = null) {
559
		$artistId = $album->getAlbumArtistId();
560
561
		if (empty($artistName)) {
562
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
563
			$artistName = $artist->getNameString($this->l10n);
564
		}
565
566
		$result = [
567
			'id' => 'album-' . $album->getId(),
568
			'parent' => 'artist-' . $artistId,
569
			'title' => $album->getNameString($this->l10n),
570
			'artist' => $artistName,
571
			'isDir' => true
572
		];
573
574
		if (!empty($album->getCoverFileId())) {
575
			$result['coverArt'] = $album->getId();
576
		}
577
578
		return $result;
579
	}
580
581
	/**
582
	 * The "new API" format is used e.g. in getAlbum and getAlbumList2
583
	 * @param Album $album
584
	 * @param string|null $artistName
585
	 * @return array
586
	 */
587
	private function albumToNewApi($album, $artistName = null) {
588
		$artistId = $album->getAlbumArtistId();
589
590
		if (empty($artistName)) {
591
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
592
			$artistName = $artist->getNameString($this->l10n);
593
		}
594
595
		$result = [
596
			'id' => 'album-' . $album->getId(),
597
			'artistId' => 'artist-' . $artistId,
598
			'name' => $album->getNameString($this->l10n),
599
			'artist' => $artistName,
600
			'songCount' => $this->trackBusinessLayer->countByAlbum($album->getId()),
601
			//'duration' => 0
602
		];
603
	
604
		if (!empty($album->getCoverFileId())) {
605
			$result['coverArt'] = $album->getId();
606
		}
607
	
608
		return $result;
609
	}
610
611
	/**
612
	 * The same API format is used both on "old" and "new" API methods. The "new" API adds some
613
	 * new fields for the songs, but providing some extra fields shouldn't be a problem for the
614
	 * older clients. 
615
	 * @param Track $track
0 ignored issues
show
Bug introduced by
The type OCA\Music\Controller\Track 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...
616
	 * @param Album|null $album
617
	 * @param string|null $albumName
618
	 * @return array
619
	 */
620
	private function trackToApi($track, $album = null, $albumName = null) {
621
		$albumId = $track->getAlbumId();
622
		if ($album == null) {
623
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
624
		}
625
		if (empty($albumName)) {
626
			$albumName = $album->getNameString($this->l10n);
627
		}
628
629
		$trackArtist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
630
		$result = [
631
			'id' => 'track-' . $track->getId(),
632
			'parent' => 'album-' . $albumId,
633
			'title' => $track->getTitle(),
634
			'artist' => $trackArtist->getNameString($this->l10n),
635
			'isDir' => false,
636
			'album' => $albumName,
637
			//'genre' => '',
638
			'year' => $track->getYear(),
639
			'size' => $track->getSize(),
640
			'contentType' => $track->getMimetype(),
641
			'suffix' => \end(\explode('.', $track->getFilename())),
642
			'duration' => $track->getLength() ?: 0,
643
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
644
			//'path' => '',
645
			'isVideo' => false,
646
			'albumId' => 'album-' . $albumId,
647
			'artistId' => 'artist-' . $track->getArtistId(),
648
			'type' => 'music'
649
		];
650
651
		if (!empty($album->getCoverFileId())) {
652
			$result['coverArt'] = $album->getId();
653
		}
654
655
		if ($track->getNumber() !== null) {
656
			$result['track'] = $track->getNumber();
657
		}
658
659
		return $result;
660
	}
661
662
	private function playlistToApi($playlist) {
663
		return [
664
			'id' => $playlist->getId(),
665
			'name' => $playlist->getName(),
666
			'owner' => $this->userId,
667
			'public' => false,
668
			'songCount' => $playlist->getTrackCount(),
669
			// comment => '',
670
			// duration => '',
671
			// created => '',
672
			// coverArt => ''
673
		];
674
	}
675
676
	/**
677
	 * Common logic for getAlbumList and getAlbumList2
678
	 * @return Album[]
679
	 */
680
	private function albumsForGetAlbumList() {
681
		$type = $this->getRequiredParam('type');
682
		$size = $this->request->getParam('size', 10);
683
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
684
		$offset = $this->request->getParam('offset', 0);
685
686
		$albums = [];
687
688
		switch ($type) {
689
			case 'random':
690
				$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
691
				$albums = self::randomItems($allAlbums, $size);
692
				// Note: offset is not supported on this type
693
				break;
694
			case 'alphabeticalByName':
695
				$albums = $this->albumBusinessLayer->findAll($this->userId, SortBy::Name, $size, $offset);
696
				break;
697
			case 'alphabeticalByArtist':
698
			case 'newest':
699
			case 'highest':
700
			case 'frequent':
701
			case 'recent':
702
			case 'starred':
703
			case 'byYear':
704
			case 'byGenre':
705
			default:
706
				$this->logger->log("Album list type '$type' is not supported", 'warn');
707
				break;
708
		}
709
710
		return $albums;
711
	}
712
713
	/**
714
	 * Given a prefixed ID like 'artist-123' or 'track-45', return just the numeric part.
715
	 * @param string $id
716
	 * @return string
717
	 */
718
	private static function ripIdPrefix($id) {
719
		return \explode('-', $id)[1];
720
	}
721
722
	private static function randomItems($itemArray, $count) {
723
		$count = \min($count, \count($itemArray)); // can't return more than all items
724
		$indices = \array_rand($itemArray, $count);
725
		if ($count == 1) { // return type is not array when randomizing a single index
726
			$indices = [$indices];
727
		}
728
729
		$result = [];
730
		foreach ($indices as $index) {
731
			$result[] = $itemArray[$index];
732
		}
733
734
		return $result;
735
	}
736
737
	private function subsonicResponse($content, $status = 'ok') {
738
		$content['status'] = $status; 
739
		$content['version'] = self::API_VERSION;
740
		$responseData = ['subsonic-response' => $content];
741
742
		if ($this->format == 'json') {
743
			$response = new JSONResponse($responseData);
744
		} else if ($this->format == 'jsonp') {
745
			$responseData = \json_encode($responseData);
746
			$response = new DataDisplayResponse("{$this->callback}($responseData);");
747
			$response->addHeader('Content-Type', 'text/javascript; charset=UTF-8');
748
		} else {
749
			$response = new XMLResponse($responseData);
750
		}
751
752
		return $response;
753
	}
754
755
	public function subsonicErrorResponse($errorCode, $errorMessage) {
756
		return $this->subsonicResponse([
757
				'error' => [
758
					'code' => $errorCode,
759
					'message' => $errorMessage
760
				]
761
			], 'failed');
762
	}
763
}
764