Passed
Push — feature/329_Subsonic_API ( d9d298 )
by Pauli
10:35
created

SubsonicController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 27
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
nc 1
nop 12
dl 0
loc 27
rs 9.9
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\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...
17
use \OCP\AppFramework\Http\TemplateResponse;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\TemplateResponse 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\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...
19
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...
20
21
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
22
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
23
use \OCA\Music\AppFramework\Core\Logger;
24
use \OCA\Music\AppFramework\Utility\MethodAnnotationReader;
25
26
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
27
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
28
use \OCA\Music\BusinessLayer\Library;
29
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
30
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
31
32
use \OCA\Music\Db\SortBy;
33
34
use \OCA\Music\Http\ErrorResponse;
35
use \OCA\Music\Http\FileResponse;
36
use \OCA\Music\Http\XMLResponse;
37
38
use \OCA\Music\Utility\CoverHelper;
39
use \OCA\Music\Utility\Util;
40
use OCA\Music\Middleware\SubsonicException;
41
42
class SubsonicController extends Controller {
43
	const API_VERSION = '1.4.0';
44
45
	private $albumBusinessLayer;
46
	private $artistBusinessLayer;
47
	private $playlistBusinessLayer;
48
	private $trackBusinessLayer;
49
	private $library;
50
	private $urlGenerator;
51
	private $rootFolder;
52
	private $l10n;
53
	private $coverHelper;
54
	private $logger;
55
	private $userId;
56
57
	public function __construct($appname,
58
								IRequest $request,
59
								$l10n,
60
								IURLGenerator $urlGenerator,
61
								AlbumBusinessLayer $albumBusinessLayer,
62
								ArtistBusinessLayer $artistBusinessLayer,
63
								PlaylistBusinessLayer $playlistBusinessLayer,
64
								TrackBusinessLayer $trackBusinessLayer,
65
								Library $library,
66
								$rootFolder,
67
								CoverHelper $coverHelper,
68
								Logger $logger) {
69
		parent::__construct($appname, $request);
70
71
		$this->albumBusinessLayer = $albumBusinessLayer;
72
		$this->artistBusinessLayer = $artistBusinessLayer;
73
		$this->playlistBusinessLayer = $playlistBusinessLayer;
74
		$this->trackBusinessLayer = $trackBusinessLayer;
75
		$this->library = $library;
76
		$this->urlGenerator = $urlGenerator;
77
		$this->l10n = $l10n;
78
79
		// used to deliver actual media file
80
		$this->rootFolder = $rootFolder;
81
82
		$this->coverHelper = $coverHelper;
83
		$this->logger = $logger;
84
	}
85
86
	/**
87
	 * Called by the middleware once the user credentials have been checked
88
	 * @param string $userId
89
	 */
90
	public function setAuthenticatedUser($userId) {
91
		$this->userId = $userId;
92
	}
93
94
	/**
95
	 * @NoAdminRequired
96
	 * @PublicPage
97
	 * @NoCSRFRequired
98
	 * @SubsonicAPI
99
	 */
100
	public function handleRequest($method) {
101
		$this->format = $this->request->getParam('f', 'xml');
0 ignored issues
show
Bug Best Practice introduced by
The property format does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
102
		if ($this->format != 'json' && $this->format != 'xml') {
103
			throw new SubsonicException("Unsupported format {$this->format}", 0);
104
		}
105
106
		// Allow calling all methods with or without the postfix ".view"
107
		if (Util::endsWith($method, ".view")) {
108
			$method = \substr($method, 0, -\strlen(".view"));
109
		}
110
111
		// Allow calling any functions annotated to be part of the API, except for
112
		// recursive call back to this dispatcher function.
113
		if ($method !== 'handleRequest' && \method_exists($this, $method)) {
114
			$annotationReader = new MethodAnnotationReader($this, $method);
115
			if ($annotationReader->hasAnnotation('SubsonicAPI')) {
116
				return $this->$method();
117
			}
118
		}
119
120
		$this->logger->log("Request $method not supported", 'warn');
121
		return $this->subsonicErrorResponse(70, "Requested action $method is not supported");
122
	}
123
124
	/* -------------------------------------------------------------------------
125
	 * REST API methods
126
	 *------------------------------------------------------------------------*/
127
128
	/**
129
	 * @SubsonicAPI
130
	 */
131
	private function ping() {
132
		return $this->subsonicResponse([]);
133
	}
134
135
	/**
136
	 * @SubsonicAPI
137
	 */
138
	private function getLicense() {
139
		return $this->subsonicResponse([
140
			'license' => [
141
				'valid' => 'true',
142
				'email' => '',
143
				'licenseExpires' => 'never'
144
			]
145
		]);
146
	}
147
148
	/**
149
	 * @SubsonicAPI
150
	 */
151
	private function getMusicFolders() {
152
		// Only single root folder is supported
153
		return $this->subsonicResponse([
154
			'musicFolders' => ['musicFolder' => [
155
				['id' => 'root', 
156
				'name' => $this->l10n->t('Music')]
157
			]]
158
		]);
159
	}
160
161
	/**
162
	 * @SubsonicAPI
163
	 */
164
	private function getIndexes() {
165
		$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

165
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, /** @scrutinizer ignore-type */ SortBy::Name);
Loading history...
166
167
		$indexes = [];
168
		foreach ($artists as $artist) {
169
			$indexes[$artist->getIndexingChar()][] = $this->artistAsChild($artist);
170
		}
171
172
		$result = [];
173
		foreach ($indexes as $indexChar => $bucketArtists) {
174
			$result[] = ['name' => $indexChar, 'artist' => $bucketArtists];
175
		}
176
177
		return $this->subsonicResponse(['indexes' => ['index' => $result]]);
178
	}
179
180
	/**
181
	 * @SubsonicAPI
182
	 */
183
	private function getMusicDirectory() {
184
		$id = $this->getRequiredParam('id');
185
186
		if (Util::startsWith($id, 'artist-')) {
187
			return $this->doGetMusicDirectoryForArtist($id);
188
		} else {
189
			return $this->doGetMusicDirectoryForAlbum($id);
190
		}
191
	}
192
193
	/**
194
	 * @SubsonicAPI
195
	 */
196
	private function getAlbumList() {
197
		$type = $this->getRequiredParam('type');
198
		$size = $this->request->getParam('size', 10);
199
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
200
		// $offset = $this->request->getParam('offset', 0); parameter not supported for now
201
202
		$albums = [];
203
		if ($type == 'random') {
204
			$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
205
			$albums = self::randomItems($allAlbums, $size);
206
		}
207
		// TODO: support 'newest', 'highest', 'frequent', 'recent'
208
209
		return $this->subsonicResponse(['albumList' =>
210
				['album' => \array_map([$this, 'albumAsChild'], $albums)]
211
		]);
212
	}
213
214
	/**
215
	 * @SubsonicAPI
216
	 */
217
	private function getRandomSongs() {
218
		$size = $this->request->getParam('size', 10);
219
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
220
		// $genre = $this->request->getParam('genre'); not supported
221
		// $fromYear = $this->request->getParam('fromYear'); not supported
222
		// $toYear = $this->request->getParam('genre'); not supported
223
224
		$allTracks = $this->trackBusinessLayer->findAll($this->userId);
225
		$tracks = self::randomItems($allTracks, $size);
226
227
		return $this->subsonicResponse(['randomSongs' =>
228
				['song' => \array_map([$this, 'trackAsChild'], $tracks)]
229
		]);
230
	}
231
232
	/**
233
	 * @SubsonicAPI
234
	 */
235
	private function getCoverArt() {
236
		$id = $this->getRequiredParam('id');
237
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
238
239
		try {
240
			$coverData = $this->coverHelper->getCover($id, $this->userId, $userFolder);
241
			if ($coverData !== null) {
0 ignored issues
show
introduced by
The condition $coverData !== null is always true.
Loading history...
242
				return new FileResponse($coverData);
243
			}
244
		} catch (BusinessLayerException $e) {
245
			return $this->subsonicErrorResponse(70, 'album not found');
246
		}
247
248
		return $this->subsonicErrorResponse(70, 'album has no cover');
249
	}
250
251
	/**
252
	 * @SubsonicAPI
253
	 */
254
	private function stream() {
255
		// We don't support transcaoding, so 'stream' and 'download' act identically
256
		return $this->download();
257
	}
258
259
	/**
260
	 * @SubsonicAPI
261
	 */
262
	private function download() {
263
		$id = $this->getRequiredParam('id');
264
		$trackId = \explode('-', $id)[1]; // get rid of 'track-' prefix
265
266
		try {
267
			$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

267
			$track = $this->trackBusinessLayer->find(/** @scrutinizer ignore-type */ $trackId, $this->userId);
Loading history...
268
		} catch (BusinessLayerException $e) {
269
			return $this->subsonicErrorResponse(70, $e->getMessage());
270
		}
271
272
		$files = $this->rootFolder->getUserFolder($this->userId)->getById($track->getFileId());
273
274
		if (\count($files) === 1) {
275
			return new FileResponse($files[0]);
276
		} else {
277
			return $this->subsonicErrorResponse(70, 'file not found');
278
		}
279
	}
280
281
	/**
282
	 * @SubsonicAPI
283
	 */
284
	private function search2() {
285
		$query = $this->getRequiredParam('query');
286
		$artistCount = $this->request->getParam('artistCount', 20);
287
		$artistOffset = $this->request->getParam('artistOffset', 0);
288
		$albumCount = $this->request->getParam('albumCount', 20);
289
		$albumOffset = $this->request->getParam('albumOffset', 0);
290
		$songCount = $this->request->getParam('songCount', 20);
291
		$songOffset = $this->request->getParam('songOffset', 0);
292
293
		if (empty($query)) {
294
			throw new SubsonicException("The 'query' argument is mandatory", 10);
295
		}
296
297
		$artists = $this->artistBusinessLayer->findAllByName($query, $this->userId, true, $artistCount, $artistOffset);
298
		$albums = $this->albumBusinessLayer->findAllByName($query, $this->userId, true, $albumCount, $albumOffset);
299
		$tracks = $this->trackBusinessLayer->findAllByName($query, $this->userId, true, $songCount, $songOffset);
300
301
		$results = [];
302
		if (!empty($artists)) {
303
			$results['artist'] = \array_map([$this, 'artistAsChild'], $artists);
304
		}
305
		if (!empty($albums)) {
306
			$results['album'] = \array_map([$this, 'albumAsChild'], $albums);
307
		}
308
		if (!empty($tracks)) {
309
			$results['song'] = \array_map([$this, 'trackAsChild'], $tracks);
310
		}
311
312
		return $this->subsonicResponse(['searchResult2' => $results]);
313
	}
314
315
	/**
316
	 * @SubsonicAPI
317
	 */
318
	private function getPlaylists() {
319
		$playlists = $this->playlistBusinessLayer->findAll($this->userId);
320
321
		return $this->subsonicResponse(['playlists' =>
322
			['playlist' => \array_map([$this, 'playlistAsChild'], $playlists)]
323
		]);
324
	}
325
326
	/**
327
	 * @SubsonicAPI
328
	 */
329
	private function getPlaylist() {
330
		$id = $this->getRequiredParam('id');
331
		$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
332
		$tracks = $this->playlistBusinessLayer->getPlaylistTracks($id, $this->userId);
333
334
		$playlistNode = $this->playlistAsChild($playlist);
335
		$playlistNode['entry'] = \array_map([$this, 'trackAsChild'], $tracks);
336
337
		return $this->subsonicResponse(['playlist' => $playlistNode]);
338
	}
339
340
	/* -------------------------------------------------------------------------
341
	 * Helper methods
342
	 *------------------------------------------------------------------------*/
343
344
	private function getRequiredParam($paramName) {
345
		$param = $this->request->getParam($paramName);
346
347
		if ($param === null) {
348
			throw new SubsonicException("Required parameter '$paramName' missing", 10);
349
		}
350
351
		return $param;
352
	}
353
354
	private function doGetMusicDirectoryForArtist($id) {
355
		$artistId = \explode('-', $id)[1]; // get rid of 'artist-' prefix
356
357
		$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

357
		$artist = $this->artistBusinessLayer->find(/** @scrutinizer ignore-type */ $artistId, $this->userId);
Loading history...
358
		$artistName = $artist->getNameString($this->l10n);
359
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
360
361
		$children = [];
362
		foreach ($albums as $album) {
363
			$children[] = $this->albumAsChild($album, $artistName);
364
		}
365
366
		return $this->subsonicResponse([
367
			'directory' => [
368
				'id' => $id,
369
				'parent' => 'root',
370
				'name' => $artistName,
371
				'child' => $children
372
			]
373
		]);
374
	}
375
376
	private function doGetMusicDirectoryForAlbum($id) {
377
		$albumId = \explode('-', $id)[1]; // get rid of 'album-' prefix
378
379
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
380
		$albumName = $album->getNameString($this->l10n);
381
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
382
383
		return $this->subsonicResponse([
384
			'directory' => [
385
				'id' => $id,
386
				'parent' => 'artist-' . $album->getAlbumArtistId(),
387
				'name' => $albumName,
388
				'child' => \array_map([$this, 'trackAsChild'], $tracks)
389
			]
390
		]);
391
	}
392
393
	private function artistAsChild($artist) {
394
		return [
395
			'name' => $artist->getNameString($this->l10n),
396
			'id' => 'artist-' . $artist->getId()
397
		];
398
	}
399
400
	private function albumAsChild($album, $artistName = null) {
401
		$artistId = $album->getAlbumArtistId();
402
403
		if (empty($artistName)) {
404
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
405
			$artistName = $artist->getNameString($this->l10n);
406
		}
407
408
		$result = [
409
			'id' => 'album-' . $album->getId(),
410
			'parent' => 'artist-' . $artistId,
411
			'title' => $album->getNameString($this->l10n),
412
			'artist' => $artistName,
413
			'isDir' => true
414
		];
415
416
		if (!empty($album->getCoverFileId())) {
417
			$result['coverArt'] = $album->getId();
418
		}
419
420
		return $result;
421
	}
422
423
	private function trackAsChild($track, $album = null, $albumName = null) {
424
		$albumId = $track->getAlbumId();
425
		if ($album == null) {
426
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
427
		}
428
		if (empty($albumName)) {
429
			$albumName = $album->getNameString($this->l10n);
430
		}
431
432
		$trackArtist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
433
		$result = [
434
			'id' => 'track-' . $track->getId(),
435
			'parent' => 'album-' . $albumId,
436
			'title' => $track->getTitle(),
437
			'artist' => $trackArtist->getNameString($this->l10n),
438
			'isDir' => false,
439
			'album' => $albumName,
440
			//'genre' => '',
441
			'year' => $track->getYear(),
442
			//'size' => 0,
443
			'contentType' => $track->getMimetype(),
444
			//'suffix' => '',
445
			'duration' => $track->getLength() ?: 0,
446
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
447
			//'path' => ''
448
		];
449
450
		if (!empty($album->getCoverFileId())) {
451
			$result['coverArt'] = $album->getId();
452
		}
453
454
		if ($track->getNumber() !== null) {
455
			$result['track'] = $track->getNumber();
456
		}
457
458
		return $result;
459
	}
460
461
	private function playlistAsChild($playlist) {
462
		return [
463
			'id' => $playlist->getId(),
464
			'name' => $playlist->getName(),
465
			'owner' => $this->userId,
466
			'public' => false,
467
			'songCount' => $playlist->getTrackCount(),
468
			// comment => '',
469
			// duration => '',
470
			// created => '',
471
			// coverArt => ''
472
		];
473
	}
474
475
	private static function randomItems($itemArray, $count) {
476
		$count = \min($count, \count($itemArray)); // can't return more than all items
477
		$indices = \array_rand($itemArray, $count);
478
		if ($count == 1) { // return type is not array when randomizing a single index
479
			$indices = [$indices];
480
		}
481
482
		$result = [];
483
		foreach ($indices as $index) {
484
			$result[] = $itemArray[$index];
485
		}
486
487
		return $result;
488
	}
489
490
	private function subsonicResponse($content, $status = 'ok') {
491
		$content['status'] = $status; 
492
		$content['version'] = self::API_VERSION;
493
		$responseData = ['subsonic-response' => $content];
494
		
495
		if ($this->format == 'json') {
496
			return new JSONResponse($responseData);
497
		} else {
498
			return new XMLResponse($responseData);
499
		}
500
	}
501
502
	public function subsonicErrorResponse($errorCode, $errorMessage) {
503
		return $this->subsonicResponse([
504
				'error' => [
505
					'code' => $errorCode,
506
					'message' => $errorMessage
507
				]
508
			], 'failed');
509
	}
510
}
511