Passed
Push — master ( 6f900b...ab8614 )
by Pauli
01:54
created

SubsonicController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 12
nc 1
nop 13
dl 0
loc 26
ccs 0
cts 13
cp 0
crap 2
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\Album;
35
use \OCA\Music\Db\Artist;
36
use \OCA\Music\Db\Playlist;
37
use \OCA\Music\Db\SortBy;
38
use \OCA\Music\Db\Track;
39
40
use \OCA\Music\Http\FileResponse;
41
use \OCA\Music\Http\XMLResponse;
42
43
use \OCA\Music\Middleware\SubsonicException;
44
45
use \OCA\Music\Utility\CoverHelper;
46
use \OCA\Music\Utility\UserMusicFolder;
47
use \OCA\Music\Utility\Util;
48
49
class SubsonicController extends Controller {
50
	const API_VERSION = '1.8.0';
51
52
	private $albumBusinessLayer;
53
	private $artistBusinessLayer;
54
	private $playlistBusinessLayer;
55
	private $trackBusinessLayer;
56
	private $library;
57
	private $urlGenerator;
58
	private $rootFolder;
59
	private $userMusicFolder;
60
	private $l10n;
61
	private $coverHelper;
62
	private $logger;
63
	private $userId;
64
	private $format;
65
	private $callback;
66
67
	public function __construct($appname,
68
								IRequest $request,
69
								$l10n,
70
								IURLGenerator $urlGenerator,
71
								AlbumBusinessLayer $albumBusinessLayer,
72
								ArtistBusinessLayer $artistBusinessLayer,
73
								PlaylistBusinessLayer $playlistBusinessLayer,
74
								TrackBusinessLayer $trackBusinessLayer,
75
								Library $library,
76
								IRootFolder $rootFolder,
77
								UserMusicFolder $userMusicFolder,
78
								CoverHelper $coverHelper,
79
								Logger $logger) {
80
		parent::__construct($appname, $request);
81
82
		$this->albumBusinessLayer = $albumBusinessLayer;
83
		$this->artistBusinessLayer = $artistBusinessLayer;
84
		$this->playlistBusinessLayer = $playlistBusinessLayer;
85
		$this->trackBusinessLayer = $trackBusinessLayer;
86
		$this->library = $library;
87
		$this->urlGenerator = $urlGenerator;
88
		$this->l10n = $l10n;
89
		$this->rootFolder = $rootFolder;
90
		$this->userMusicFolder = $userMusicFolder;
91
		$this->coverHelper = $coverHelper;
92
		$this->logger = $logger;
93
	}
94
95
	/**
96
	 * Called by the middleware once the user credentials have been checked
97
	 * @param string $userId
98
	 */
99
	public function setAuthenticatedUser($userId) {
100
		$this->userId = $userId;
101
	}
102
103
	/**
104
	 * @NoAdminRequired
105
	 * @PublicPage
106
	 * @NoCSRFRequired
107
	 */
108
	public function handleRequest($method) {
109
		$this->format = $this->request->getParam('f', 'xml');
110
		$this->callback = $this->request->getParam('callback');
111
112
		if (!\in_array($this->format, ['json', 'xml', 'jsonp'])) {
113
			throw new SubsonicException("Unsupported format {$this->format}", 0);
114
		}
115
116
		if ($this->format === 'jsonp' && empty($this->callback)) {
117
			$this->format = 'json';
118
			throw new SubsonicException("Argument 'callback' is required with jsonp format", 10);
119
		}
120
121
		// Allow calling all methods with or without the postfix ".view"
122
		if (Util::endsWith($method, ".view")) {
123
			$method = \substr($method, 0, -\strlen(".view"));
124
		}
125
126
		// Allow calling any functions annotated to be part of the API
127
		if (\method_exists($this, $method)) {
128
			$annotationReader = new MethodAnnotationReader($this, $method);
129
			if ($annotationReader->hasAnnotation('SubsonicAPI')) {
130
				return $this->$method();
131
			}
132
		}
133
134
		$this->logger->log("Request $method not supported", 'warn');
135
		return $this->subsonicErrorResponse(70, "Requested action $method is not supported");
136
	}
137
138
	/* -------------------------------------------------------------------------
139
	 * REST API methods
140
	 *------------------------------------------------------------------------*/
141
142
	/**
143
	 * @SubsonicAPI
144
	 */
145
	private function ping() {
146
		return $this->subsonicResponse([]);
147
	}
148
149
	/**
150
	 * @SubsonicAPI
151
	 */
152
	private function getLicense() {
153
		return $this->subsonicResponse([
154
			'license' => [
155
				'valid' => 'true',
156
				'email' => '',
157
				'licenseExpires' => 'never'
158
			]
159
		]);
160
	}
161
162
	/**
163
	 * @SubsonicAPI
164
	 */
165
	private function getMusicFolders() {
166
		// Only single root folder is supported
167
		return $this->subsonicResponse([
168
			'musicFolders' => ['musicFolder' => [
169
				['id' => 'artists', 'name' => $this->l10n->t('Artists')],
170
				['id' => 'folders', 'name' => $this->l10n->t('Folders')]
171
			]]
172
		]);
173
	}
174
175
	/**
176
	 * @SubsonicAPI
177
	 */
178
	private function getIndexes() {
179
		$id = $this->request->getParam('musicFolderId');
180
181
		if ($id === 'folders') {
182
			return $this->getIndexesForFolders();
183
		} else {
184
			return $this->getIndexesForArtists();
185
		}
186
	}
187
188
	/**
189
	 * @SubsonicAPI
190
	 */
191
	private function getMusicDirectory() {
192
		$id = $this->getRequiredParam('id');
193
194
		if (Util::startsWith($id, 'folder-')) {
195
			return $this->getMusicDirectoryForFolder($id);
196
		} elseif (Util::startsWith($id, 'artist-')) {
197
			return $this->getMusicDirectoryForArtist($id);
198
		} else {
199
			return $this->getMusicDirectoryForAlbum($id);
200
		}
201
	}
202
203
	/**
204
	 * @SubsonicAPI
205
	 */
206
	private function getAlbumList() {
207
		$albums = $this->albumsForGetAlbumList();
208
		return $this->subsonicResponse(['albumList' =>
209
				['album' => \array_map([$this, 'albumToOldApi'], $albums)]
210
		]);
211
	}
212
213
	/**
214
	 * @SubsonicAPI
215
	 */
216
	private function getAlbumList2() {
217
		/*
218
		 * According to the API specification, the difference between this and getAlbumList
219
		 * should be that this function would organize albums according the metadata while
220
		 * getAlbumList would organize them by folders. However, we organize by metadata
221
		 * also in getAlbumList, because that's more natural for the Music app and many/most
222
		 * clients do not support getAlbumList2.
223
		 */
224
		$albums = $this->albumsForGetAlbumList();
225
		return $this->subsonicResponse(['albumList2' =>
226
				['album' => \array_map([$this, 'albumToNewApi'], $albums)]
227
		]);
228
	}
229
230
	/**
231
	 * @SubsonicAPI
232
	 */
233
	private function getArtists() {
234
		return $this->getIndexesForArtists('artists');
235
	}
236
237
	/**
238
	 * @SubsonicAPI
239
	 */
240
	private function getArtist() {
241
		$id = $this->getRequiredParam('id');
242
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
243
244
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
245
		$artistName = $artist->getNameString($this->l10n);
246
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
247
248
		$artistNode = $this->artistToApi($artist);
249
		$artistNode['album'] = \array_map(function($album) use ($artistName) {
250
			return $this->albumToNewApi($album, $artistName);
251
		}, $albums);
252
253
		return $this->subsonicResponse(['artist' => $artistNode]);
254
	}
255
256
	/**
257
	 * @SubsonicAPI
258
	 */
259
	private function getAlbum() {
260
		$id = $this->getRequiredParam('id');
261
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
262
263
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
264
		$albumName = $album->getNameString($this->l10n);
265
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
266
267
		$albumNode = $this->albumToNewApi($album);
268
		$albumNode['song'] = \array_map(function($track) use ($album, $albumName) {
269
			return $this->trackToApi($track, $album, $albumName);
270
		}, $tracks);
271
272
		return $this->subsonicResponse(['album' => $albumNode]);
273
	}
274
275
	/**
276
	 * @SubsonicAPI
277
	 */
278
	private function getSong() {
279
		$id = $this->getRequiredParam('id');
280
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
281
		$track = $this->trackBusinessLayer->find($trackId, $this->userId);
282
283
		return $this->subsonicResponse(['song' => $this->trackToApi($track)]);
284
	}
285
286
	/**
287
	 * @SubsonicAPI
288
	 */
289
	private function getRandomSongs() {
290
		$size = $this->request->getParam('size', 10);
291
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
292
		// $genre = $this->request->getParam('genre'); not supported
293
		// $fromYear = $this->request->getParam('fromYear'); not supported
294
		// $toYear = $this->request->getParam('genre'); not supported
295
296
		$allTracks = $this->trackBusinessLayer->findAll($this->userId);
297
		$tracks = self::randomItems($allTracks, $size);
298
299
		return $this->subsonicResponse(['randomSongs' =>
300
				['song' => \array_map([$this, 'trackToApi'], $tracks)]
301
		]);
302
	}
303
304
	/**
305
	 * @SubsonicAPI
306
	 */
307
	private function getCoverArt() {
308
		$id = $this->getRequiredParam('id');
309
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
310
		$coverData = $this->coverHelper->getCover($id, $this->userId, $userFolder);
311
312
		if ($coverData !== null) {
0 ignored issues
show
introduced by
The condition $coverData !== null is always true.
Loading history...
313
			return new FileResponse($coverData);
314
		}
315
316
		return $this->subsonicErrorResponse(70, 'album has no cover');
317
	}
318
319
	/**
320
	 * @SubsonicAPI
321
	 */
322
	private function stream() {
323
		// We don't support transcaoding, so 'stream' and 'download' act identically
324
		return $this->download();
325
	}
326
327
	/**
328
	 * @SubsonicAPI
329
	 */
330
	private function download() {
331
		$id = $this->getRequiredParam('id');
332
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
333
334
		$track = $this->trackBusinessLayer->find($trackId, $this->userId);
335
		$file = $this->getFilesystemNode($track->getFileId());
336
337
		if ($file instanceof File) {
338
			return new FileResponse($file);
339
		} else {
340
			return $this->subsonicErrorResponse(70, 'file not found');
341
		}
342
	}
343
344
	/**
345
	 * @SubsonicAPI
346
	 */
347
	private function search2() {
348
		$results = $this->doSearch();
349
350
		return $this->subsonicResponse(['searchResult2' => [
351
			'artist' => \array_map([$this, 'artistToApi'], $results['artists']),
352
			'album' => \array_map([$this, 'albumToOldApi'], $results['albums']),
353
			'song' => \array_map([$this, 'trackToApi'], $results['tracks'])
354
		]]);
355
	}
356
357
	/**
358
	 * @SubsonicAPI
359
	 */
360
	private function search3() {
361
		$results = $this->doSearch();
362
363
		return $this->subsonicResponse(['searchResult3' => [
364
			'artist' => \array_map([$this, 'artistToApi'], $results['artists']),
365
			'album' => \array_map([$this, 'albumToNewApi'], $results['albums']),
366
			'song' => \array_map([$this, 'trackToApi'], $results['tracks'])
367
		]]);
368
	}
369
370
	/**
371
	 * @SubsonicAPI
372
	 */
373
	private function getPlaylists() {
374
		$playlists = $this->playlistBusinessLayer->findAll($this->userId);
375
376
		return $this->subsonicResponse(['playlists' =>
377
			['playlist' => \array_map([$this, 'playlistToApi'], $playlists)]
378
		]);
379
	}
380
381
	/**
382
	 * @SubsonicAPI
383
	 */
384
	private function getPlaylist() {
385
		$id = $this->getRequiredParam('id');
386
		$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
387
		$tracks = $this->playlistBusinessLayer->getPlaylistTracks($id, $this->userId);
388
389
		$playlistNode = $this->playlistToApi($playlist);
390
		$playlistNode['entry'] = \array_map([$this, 'trackToApi'], $tracks);
391
392
		return $this->subsonicResponse(['playlist' => $playlistNode]);
393
	}
394
395
	/**
396
	 * @SubsonicAPI
397
	 */
398
	private function createPlaylist() {
399
		$name = $this->getRequiredParam('name');
400
		$songIds = $this->getRepeatedParam('songId');
401
		$songIds = \array_map('self::ripIdPrefix', $songIds);
402
403
		$playlist = $this->playlistBusinessLayer->create($name, $this->userId);
404
		$this->playlistBusinessLayer->addTracks($songIds, $playlist->getId(), $this->userId);
405
406
		return $this->subsonicResponse([]);
407
	}
408
409
	/**
410
	 * @SubsonicAPI
411
	 */
412
	private function updatePlaylist() {
413
		$listId = $this->getRequiredParam('playlistId');
414
		$newName = $this->request->getParam('name');
415
		$songIdsToAdd = $this->getRepeatedParam('songIdToAdd');
416
		$songIdsToAdd = \array_map('self::ripIdPrefix', $songIdsToAdd);
417
		$songIndicesToRemove = $this->getRepeatedParam('songIndexToRemove');
418
419
		if (!empty($newName)) {
420
			$this->playlistBusinessLayer->rename($newName, $listId, $this->userId);
421
		}
422
423
		if (!empty($songIndicesToRemove)) {
424
			$this->playlistBusinessLayer->removeTracks($songIndicesToRemove, $listId, $this->userId);
425
		}
426
427
		if (!empty($songIdsToAdd)) {
428
			$this->playlistBusinessLayer->addTracks($songIdsToAdd, $listId, $this->userId);
429
		}
430
431
		return $this->subsonicResponse([]);
432
	}
433
434
	/**
435
	 * @SubsonicAPI
436
	 */
437
	private function deletePlaylist() {
438
		$id = $this->getRequiredParam('id');
439
		$this->playlistBusinessLayer->delete($id, $this->userId);
440
		return $this->subsonicResponse([]);
441
	}
442
443
	/**
444
	 * @SubsonicAPI
445
	 */
446
	private function getUser() {
447
		$username = $this->getRequiredParam('username');
448
449
		if ($username != $this->userId) {
450
			throw new SubsonicException("{$this->userId} is not authorized to get details for other users.", 50);
451
		}
452
453
		return $this->subsonicResponse([
454
			'user' => [
455
				'username' => $username,
456
				'email' => '',
457
				'scrobblingEnabled' => false,
458
				'adminRole' => false,
459
				'settingsRole' => false,
460
				'downloadRole' => true,
461
				'uploadRole' => false,
462
				'playlistRole' => true,
463
				'coverArtRole' => false,
464
				'commentRole' => false,
465
				'podcastRole' => false,
466
				'streamRole' => true,
467
				'jukeboxRole' => false,
468
				'shareRole' => false,
469
				'videoConversionRole' => false,
470
				'folder' => ['artists', 'folders'],
471
			]
472
		]);
473
	}
474
475
	/**
476
	 * @SubsonicAPI
477
	 */
478
	private function getUsers() {
479
		throw new SubsonicException("{$this->userId} is not authorized to get details for other users.", 50);
480
	}
481
482
	/**
483
	 * @SubsonicAPI
484
	 */
485
	private function getAvatar() {
486
		// TODO: Use 'username' parameter to fetch user-specific avatar from the OC core.
487
		// Remember to check the permission.
488
		// For now, use the Music app logo for all users.
489
		$fileName = \join(DIRECTORY_SEPARATOR, [\dirname(__DIR__), 'img', 'logo', 'music_logo.png']);
490
		$content = \file_get_contents($fileName);
491
		return new FileResponse(['content' => $content, 'mimetype' => 'image/png']);
492
	}
493
494
	/**
495
	 * @SubsonicAPI
496
	 */
497
	private function getStarred() {
498
		// TODO: dummy implementation
499
		return $this->subsonicResponse([
500
			'starred' => [
501
				'artist' => [],
502
				'album' => [],
503
				'song' => []
504
			]
505
		]);
506
	}
507
508
	/**
509
	 * @SubsonicAPI
510
	 */
511
	private function getStarred2() {
512
		// TODO: dummy implementation
513
		return $this->subsonicResponse([
514
			'starred2' => [
515
				'artist' => [],
516
				'album' => [],
517
				'song' => []
518
			]
519
		]);
520
	}
521
522
	/**
523
	 * @SubsonicAPI
524
	 */
525
	private function getVideos() {
526
		// TODO: dummy implementation
527
		return $this->subsonicResponse([
528
			'videos' => [
529
				'video' => []
530
			]
531
		]);
532
	}
533
534
	/* -------------------------------------------------------------------------
535
	 * Helper methods
536
	 *------------------------------------------------------------------------*/
537
538
	private function getRequiredParam($paramName) {
539
		$param = $this->request->getParam($paramName);
540
541
		if ($param === null) {
542
			throw new SubsonicException("Required parameter '$paramName' missing", 10);
543
		}
544
545
		return $param;
546
	}
547
548
	/** 
549
	 * Get values for parameter which may be present multiple times in the query
550
	 * string or POST data.
551
	 * @param string $paramName
552
	 * @return string[]
553
	 */
554
	private function getRepeatedParam($paramName) {
555
		// We can't use the IRequest object nor $_GET and $_POST to get the data
556
		// because all of these are based to the idea of unique parameter names.
557
		// If the same name is repeated, only the last value is saved. Hence, we
558
		// need to parse the raw data manually.
559
560
		// query string is always present (although it could be empty)
561
		$values = $this->parseRepeatedKeyValues($paramName, $_SERVER['QUERY_STRING']);
562
563
		// POST data is available if the method is POST
564
		if ($this->request->getMethod() == 'POST') {
565
			$values = \array_merge($values,
566
					$this->parseRepeatedKeyValues($paramName, file_get_contents('php://input')));
567
		}
568
569
		return $values;
570
	}
571
572
	/**
573
	 * Parse a string like "someKey=value1&someKey=value2&anotherKey=valueA&someKey=value3"
574
	 * and return an array of values for the given key
575
	 * @param string $key
576
	 * @param string $data
577
	 */
578
	private function parseRepeatedKeyValues($key, $data) {
579
		$result = [];
580
581
		$keyValuePairs = \explode('&', $data);
582
583
		foreach ($keyValuePairs as $pair) {
584
			$keyAndValue = \explode('=', $pair);
585
586
			if ($keyAndValue[0] == $key) {
587
				$result[] = $keyAndValue[1];
588
			}
589
		}
590
591
		return $result;
592
	}
593
594
	private function getFilesystemNode($id) {
595
		$nodes = $this->rootFolder->getUserFolder($this->userId)->getById($id);
596
597
		if (\count($nodes) != 1) {
598
			throw new SubsonicException('file not found', 70);
599
		}
600
601
		return $nodes[0];
602
	}
603
604
	private function getIndexesForFolders() {
605
		$rootFolder = $this->userMusicFolder->getFolder($this->userId);
606
	
607
		return $this->subsonicResponse(['indexes' => ['index' => [
608
				['name' => '*',
609
				'artist' => [['id' => 'folder-' . $rootFolder->getId(), 'name' => $rootFolder->getName()]]]
610
				]]]);
611
	}
612
	
613
	private function getMusicDirectoryForFolder($id) {
614
		$folderId = self::ripIdPrefix($id); // get rid of 'folder-' prefix
615
		$folder = $this->getFilesystemNode($folderId);
616
617
		if (!($folder instanceof Folder)) {
618
			throw new SubsonicException("$id is not a valid folder", 70);
619
		}
620
621
		$nodes = $folder->getDirectoryListing();
622
		$subFolders = \array_filter($nodes, function ($n) {
623
			return $n instanceof Folder;
624
		});
625
		$tracks = $this->trackBusinessLayer->findAllByFolder($folderId, $this->userId);
626
627
		$children = \array_merge(
628
			\array_map([$this, 'folderToApi'], $subFolders),
629
			\array_map([$this, 'trackToApi'], $tracks)
630
		);
631
632
		return $this->subsonicResponse([
633
			'directory' => [
634
				'id' => $id,
635
				'parent' => 'folder-' . $folder->getParent()->getId(),
636
				'name' => $folder->getName(),
637
				'child' => $children
638
			]
639
		]);
640
	}
641
642
	private function getIndexesForArtists($rootElementName = 'indexes') {
643
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, SortBy::Name);
644
	
645
		$indexes = [];
646
		foreach ($artists as $artist) {
647
			$indexes[$artist->getIndexingChar()][] = $this->artistToApi($artist);
648
		}
649
	
650
		$result = [];
651
		foreach ($indexes as $indexChar => $bucketArtists) {
652
			$result[] = ['name' => $indexChar, 'artist' => $bucketArtists];
653
		}
654
	
655
		return $this->subsonicResponse([$rootElementName => ['index' => $result]]);
656
	}
657
658
	private function getMusicDirectoryForArtist($id) {
659
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
660
661
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
662
		$artistName = $artist->getNameString($this->l10n);
663
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
664
665
		$children = [];
666
		foreach ($albums as $album) {
667
			$children[] = $this->albumToOldApi($album, $artistName);
668
		}
669
670
		return $this->subsonicResponse([
671
			'directory' => [
672
				'id' => $id,
673
				'parent' => 'artists',
674
				'name' => $artistName,
675
				'child' => $children
676
			]
677
		]);
678
	}
679
680
	private function getMusicDirectoryForAlbum($id) {
681
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
682
683
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
684
		$albumName = $album->getNameString($this->l10n);
685
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
686
687
		return $this->subsonicResponse([
688
			'directory' => [
689
				'id' => $id,
690
				'parent' => 'artist-' . $album->getAlbumArtistId(),
691
				'name' => $albumName,
692
				'child' => \array_map([$this, 'trackToApi'], $tracks)
693
			]
694
		]);
695
	}
696
697
	/**
698
	 * @param Folder $folder
699
	 * @return array
700
	 */
701
	private function folderToApi($folder) {
702
		return [
703
			'id' => 'folder-' . $folder->getId(),
704
			'title' => $folder->getName(),
705
			'isDir' => true
706
		];
707
	}
708
709
	/**
710
	 * @param Artist $artist
711
	 * @return array
712
	 */
713
	private function artistToApi($artist) {
714
		return [
715
			'name' => $artist->getNameString($this->l10n),
716
			'id' => 'artist-' . $artist->getId(),
717
			'albumCount' => $this->albumBusinessLayer->countByArtist($artist->getId())
718
		];
719
	}
720
721
	/**
722
	 * The "old API" format is used e.g. in getMusicDirectory and getAlbumList
723
	 * @param Album $album
724
	 * @param string|null $artistName
725
	 * @return array
726
	 */
727
	private function albumToOldApi($album, $artistName = null) {
728
		$artistId = $album->getAlbumArtistId();
729
730
		if (empty($artistName)) {
731
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
732
			$artistName = $artist->getNameString($this->l10n);
733
		}
734
735
		$result = [
736
			'id' => 'album-' . $album->getId(),
737
			'parent' => 'artist-' . $artistId,
738
			'title' => $album->getNameString($this->l10n),
739
			'artist' => $artistName,
740
			'isDir' => true
741
		];
742
743
		if (!empty($album->getCoverFileId())) {
744
			$result['coverArt'] = $album->getId();
745
		}
746
747
		return $result;
748
	}
749
750
	/**
751
	 * The "new API" format is used e.g. in getAlbum and getAlbumList2
752
	 * @param Album $album
753
	 * @param string|null $artistName
754
	 * @return array
755
	 */
756
	private function albumToNewApi($album, $artistName = null) {
757
		$artistId = $album->getAlbumArtistId();
758
759
		if (empty($artistName)) {
760
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
761
			$artistName = $artist->getNameString($this->l10n);
762
		}
763
764
		$result = [
765
			'id' => 'album-' . $album->getId(),
766
			'artistId' => 'artist-' . $artistId,
767
			'name' => $album->getNameString($this->l10n),
768
			'artist' => $artistName,
769
			'songCount' => $this->trackBusinessLayer->countByAlbum($album->getId()),
770
			//'duration' => 0
771
		];
772
773
		if (!empty($album->getCoverFileId())) {
774
			$result['coverArt'] = $album->getId();
775
		}
776
777
		return $result;
778
	}
779
780
	/**
781
	 * The same API format is used both on "old" and "new" API methods. The "new" API adds some
782
	 * new fields for the songs, but providing some extra fields shouldn't be a problem for the
783
	 * older clients. 
784
	 * @param Track $track
785
	 * @param Album|null $album
786
	 * @param string|null $albumName
787
	 * @return array
788
	 */
789
	private function trackToApi($track, $album = null, $albumName = null) {
790
		$albumId = $track->getAlbumId();
791
		if ($album == null) {
792
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
793
		}
794
		if (empty($albumName)) {
795
			$albumName = $album->getNameString($this->l10n);
796
		}
797
798
		$trackArtist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
799
		$result = [
800
			'id' => 'track-' . $track->getId(),
801
			'parent' => 'album-' . $albumId,
802
			'title' => $track->getTitle(),
803
			'artist' => $trackArtist->getNameString($this->l10n),
804
			'isDir' => false,
805
			'album' => $albumName,
806
			//'genre' => '',
807
			'year' => $track->getYear(),
808
			'size' => $track->getSize(),
809
			'contentType' => $track->getMimetype(),
810
			'suffix' => $track->getFileExtension(),
811
			'duration' => $track->getLength() ?: 0,
812
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
813
			//'path' => '',
814
			'isVideo' => false,
815
			'albumId' => 'album-' . $albumId,
816
			'artistId' => 'artist-' . $track->getArtistId(),
817
			'type' => 'music'
818
		];
819
820
		if (!empty($album->getCoverFileId())) {
821
			$result['coverArt'] = $album->getId();
822
		}
823
824
		if ($track->getNumber() !== null) {
0 ignored issues
show
introduced by
The condition $track->getNumber() !== null is always true.
Loading history...
825
			$result['track'] = $track->getNumber();
826
		}
827
828
		return $result;
829
	}
830
831
	/**
832
	 * @param Playlist $playlist
833
	 * @return array
834
	 */
835
	private function playlistToApi($playlist) {
836
		return [
837
			'id' => $playlist->getId(),
838
			'name' => $playlist->getName(),
839
			'owner' => $this->userId,
840
			'public' => false,
841
			'songCount' => $playlist->getTrackCount(),
842
			// comment => '',
843
			// duration => '',
844
			// created => '',
845
			// coverArt => ''
846
		];
847
	}
848
849
	/**
850
	 * Common logic for getAlbumList and getAlbumList2
851
	 * @return Album[]
852
	 */
853
	private function albumsForGetAlbumList() {
854
		$type = $this->getRequiredParam('type');
855
		$size = $this->request->getParam('size', 10);
856
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
857
		$offset = $this->request->getParam('offset', 0);
858
859
		$albums = [];
860
861
		switch ($type) {
862
			case 'random':
863
				$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
864
				$albums = self::randomItems($allAlbums, $size);
865
				// Note: offset is not supported on this type
866
				break;
867
			case 'alphabeticalByName':
868
				$albums = $this->albumBusinessLayer->findAll($this->userId, SortBy::Name, $size, $offset);
869
				break;
870
			case 'alphabeticalByArtist':
871
			case 'newest':
872
			case 'highest':
873
			case 'frequent':
874
			case 'recent':
875
			case 'starred':
876
			case 'byYear':
877
			case 'byGenre':
878
			default:
879
				$this->logger->log("Album list type '$type' is not supported", 'debug');
880
				break;
881
		}
882
883
		return $albums;
884
	}
885
886
	/**
887
	 * Common logic for search2 and search3
888
	 * @return array with keys 'artists', 'albums', and 'tracks'
889
	 */
890
	private function doSearch() {
891
		$query = $this->getRequiredParam('query');
892
		$artistCount = $this->request->getParam('artistCount', 20);
893
		$artistOffset = $this->request->getParam('artistOffset', 0);
894
		$albumCount = $this->request->getParam('albumCount', 20);
895
		$albumOffset = $this->request->getParam('albumOffset', 0);
896
		$songCount = $this->request->getParam('songCount', 20);
897
		$songOffset = $this->request->getParam('songOffset', 0);
898
899
		if (empty($query)) {
900
			throw new SubsonicException("The 'query' argument is mandatory", 10);
901
		}
902
903
		return [
904
			'artists' => $this->artistBusinessLayer->findAllByName($query, $this->userId, true, $artistCount, $artistOffset),
905
			'albums' => $this->albumBusinessLayer->findAllByName($query, $this->userId, true, $albumCount, $albumOffset),
906
			'tracks' => $this->trackBusinessLayer->findAllByName($query, $this->userId, true, $songCount, $songOffset)
907
		];
908
	}
909
910
	/**
911
	 * Given a prefixed ID like 'artist-123' or 'track-45', return just the numeric part.
912
	 * @param string $id
913
	 * @return integer
914
	 */
915
	private static function ripIdPrefix($id) {
916
		return (int)(\explode('-', $id)[1]);
917
	}
918
919
	private static function randomItems($itemArray, $count) {
920
		$count = \min($count, \count($itemArray)); // can't return more than all items
921
		$indices = \array_rand($itemArray, $count);
922
		if ($count == 1) { // return type is not array when randomizing a single index
923
			$indices = [$indices];
924
		}
925
926
		$result = [];
927
		foreach ($indices as $index) {
928
			$result[] = $itemArray[$index];
929
		}
930
931
		return $result;
932
	}
933
934
	private function subsonicResponse($content, $status = 'ok') {
935
		$content['status'] = $status; 
936
		$content['version'] = self::API_VERSION;
937
		$responseData = ['subsonic-response' => $content];
938
939
		if ($this->format == 'json') {
940
			$response = new JSONResponse($responseData);
941
		} else if ($this->format == 'jsonp') {
942
			$responseData = \json_encode($responseData);
943
			$response = new DataDisplayResponse("{$this->callback}($responseData);");
944
			$response->addHeader('Content-Type', 'text/javascript; charset=UTF-8');
945
		} else {
946
			$response = new XMLResponse($responseData);
947
		}
948
949
		return $response;
950
	}
951
952
	public function subsonicErrorResponse($errorCode, $errorMessage) {
953
		return $this->subsonicResponse([
954
				'error' => [
955
					'code' => $errorCode,
956
					'message' => $errorMessage
957
				]
958
			], 'failed');
959
	}
960
}
961