Completed
Pull Request — master (#718)
by Pauli
08:54 queued 07:34
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\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.4.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 getAlbum() {
234
		$id = $this->getRequiredParam('id');
235
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
236
237
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
238
		$albumName = $album->getNameString($this->l10n);
239
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
240
241
		$albumNode = $this->albumToNewApi($album);
242
		$albumNode['song'] = \array_map(function($track) use ($album, $albumName) {
243
			return $this->trackToApi($track, $album, $albumName);
244
		}, $tracks);
245
246
		return $this->subsonicResponse(['album' => $albumNode]);
247
	}
248
249
	/**
250
	 * @SubsonicAPI
251
	 */
252
	private function getRandomSongs() {
253
		$size = $this->request->getParam('size', 10);
254
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
255
		// $genre = $this->request->getParam('genre'); not supported
256
		// $fromYear = $this->request->getParam('fromYear'); not supported
257
		// $toYear = $this->request->getParam('genre'); not supported
258
259
		$allTracks = $this->trackBusinessLayer->findAll($this->userId);
260
		$tracks = self::randomItems($allTracks, $size);
261
262
		return $this->subsonicResponse(['randomSongs' =>
263
				['song' => \array_map([$this, 'trackToApi'], $tracks)]
264
		]);
265
	}
266
267
	/**
268
	 * @SubsonicAPI
269
	 */
270
	private function getCoverArt() {
271
		$id = $this->getRequiredParam('id');
272
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
273
		$coverData = $this->coverHelper->getCover($id, $this->userId, $userFolder);
274
275
		if ($coverData !== null) {
0 ignored issues
show
introduced by
The condition $coverData !== null is always true.
Loading history...
276
			return new FileResponse($coverData);
277
		}
278
279
		return $this->subsonicErrorResponse(70, 'album has no cover');
280
	}
281
282
	/**
283
	 * @SubsonicAPI
284
	 */
285
	private function stream() {
286
		// We don't support transcaoding, so 'stream' and 'download' act identically
287
		return $this->download();
288
	}
289
290
	/**
291
	 * @SubsonicAPI
292
	 */
293
	private function download() {
294
		$id = $this->getRequiredParam('id');
295
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
296
297
		$track = $this->trackBusinessLayer->find($trackId, $this->userId);
298
		$file = $this->getFilesystemNode($track->getFileId());
299
300
		if ($file instanceof File) {
301
			return new FileResponse($file);
302
		} else {
303
			return $this->subsonicErrorResponse(70, 'file not found');
304
		}
305
	}
306
307
	/**
308
	 * @SubsonicAPI
309
	 */
310
	private function search2() {
311
		$query = $this->getRequiredParam('query');
312
		$artistCount = $this->request->getParam('artistCount', 20);
313
		$artistOffset = $this->request->getParam('artistOffset', 0);
314
		$albumCount = $this->request->getParam('albumCount', 20);
315
		$albumOffset = $this->request->getParam('albumOffset', 0);
316
		$songCount = $this->request->getParam('songCount', 20);
317
		$songOffset = $this->request->getParam('songOffset', 0);
318
319
		if (empty($query)) {
320
			throw new SubsonicException("The 'query' argument is mandatory", 10);
321
		}
322
323
		$artists = $this->artistBusinessLayer->findAllByName($query, $this->userId, true, $artistCount, $artistOffset);
324
		$albums = $this->albumBusinessLayer->findAllByName($query, $this->userId, true, $albumCount, $albumOffset);
325
		$tracks = $this->trackBusinessLayer->findAllByName($query, $this->userId, true, $songCount, $songOffset);
326
327
		$results = [];
328
		if (!empty($artists)) {
329
			$results['artist'] = \array_map([$this, 'artistToApi'], $artists);
330
		}
331
		if (!empty($albums)) {
332
			$results['album'] = \array_map([$this, 'albumToOldApi'], $albums);
333
		}
334
		if (!empty($tracks)) {
335
			$results['song'] = \array_map([$this, 'trackToApi'], $tracks);
336
		}
337
338
		return $this->subsonicResponse(['searchResult2' => $results]);
339
	}
340
341
	/**
342
	 * @SubsonicAPI
343
	 */
344
	private function getPlaylists() {
345
		$playlists = $this->playlistBusinessLayer->findAll($this->userId);
346
347
		return $this->subsonicResponse(['playlists' =>
348
			['playlist' => \array_map([$this, 'playlistToApi'], $playlists)]
349
		]);
350
	}
351
352
	/**
353
	 * @SubsonicAPI
354
	 */
355
	private function getPlaylist() {
356
		$id = $this->getRequiredParam('id');
357
		$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
358
		$tracks = $this->playlistBusinessLayer->getPlaylistTracks($id, $this->userId);
359
360
		$playlistNode = $this->playlistToApi($playlist);
361
		$playlistNode['entry'] = \array_map([$this, 'trackToApi'], $tracks);
362
363
		return $this->subsonicResponse(['playlist' => $playlistNode]);
364
	}
365
366
	/**
367
	 * @SubsonicAPI
368
	 */
369
	private function createPlaylist() {
370
		$name = $this->getRequiredParam('name');
371
		$songIds = $this->getRepeatedParam('songId');
372
		$songIds = \array_map('self::ripIdPrefix', $songIds);
373
374
		$playlist = $this->playlistBusinessLayer->create($name, $this->userId);
375
		$this->playlistBusinessLayer->addTracks($songIds, $playlist->getId(), $this->userId);
376
377
		return $this->subsonicResponse([]);
378
	}
379
380
	/* -------------------------------------------------------------------------
381
	 * Helper methods
382
	 *------------------------------------------------------------------------*/
383
384
	private function getRequiredParam($paramName) {
385
		$param = $this->request->getParam($paramName);
386
387
		if ($param === null) {
388
			throw new SubsonicException("Required parameter '$paramName' missing", 10);
389
		}
390
391
		return $param;
392
	}
393
394
	/** 
395
	 * Get values for parameter which may be present multiple times in the query
396
	 * string or POST data.
397
	 * @param string $paramName
398
	 * @return string[]
399
	 */
400
	private function getRepeatedParam($paramName) {
401
		// We can't use the IRequest object nor $_GET and $_POST to get the data
402
		// because all of these are based to the idea of unique parameter names.
403
		// If the same name is repeated, only the last value is saved. Hence, we
404
		// need to parse the raw data manually.
405
406
		// query string is always present (although it could be empty)
407
		$values = $this->parseRepeatedKeyValues($paramName, $_SERVER['QUERY_STRING']);
408
409
		// POST data is available if the method is POST
410
		if ($this->request->getMethod() == 'POST') {
411
			$values = \array_merge($values,
412
					$this->parseRepeatedKeyValues($paramName, file_get_contents('php://input')));
413
		}
414
415
		return $values;
416
	}
417
418
	/**
419
	 * Parse a string like "someKey=value1&someKey=value2&anotherKey=valueA&someKey=value3"
420
	 * and return an array of values for the given key
421
	 * @param string $key
422
	 * @param string $data
423
	 */
424
	private function parseRepeatedKeyValues($key, $data) {
425
		$result = [];
426
427
		$keyValuePairs = \explode('&', $data);
428
429
		foreach ($keyValuePairs as $pair) {
430
			$keyAndValue = \explode('=', $pair);
431
432
			if ($keyAndValue[0] == $key) {
433
				$result[] = $keyAndValue[1];
434
			}
435
		}
436
437
		return $result;
438
	}
439
440
	private function getFilesystemNode($id) {
441
		$nodes = $this->rootFolder->getUserFolder($this->userId)->getById($id);
442
443
		if (\count($nodes) != 1) {
444
			throw new SubsonicException('file not found', 70);
445
		}
446
447
		return $nodes[0];
448
	}
449
450
	private function getIndexesForFolders() {
451
		$rootFolder = $this->userMusicFolder->getFolder($this->userId);
452
	
453
		return $this->subsonicResponse(['indexes' => ['index' => [
454
				['name' => '*',
455
				'artist' => [['id' => 'folder-' . $rootFolder->getId(), 'name' => $rootFolder->getName()]]]
456
				]]]);
457
	}
458
	
459
	private function getMusicDirectoryForFolder($id) {
460
		$folderId = self::ripIdPrefix($id); // get rid of 'folder-' prefix
461
		$folder = $this->getFilesystemNode($folderId);
462
463
		if (!($folder instanceof Folder)) {
464
			throw new SubsonicException("$id is not a valid folder", 70);
465
		}
466
467
		$nodes = $folder->getDirectoryListing();
468
		$subFolders = \array_filter($nodes, function ($n) {
469
			return $n instanceof Folder;
470
		});
471
		$tracks = $this->trackBusinessLayer->findAllByFolder($folderId, $this->userId);
472
473
		$children = \array_merge(
474
			\array_map([$this, 'folderToApi'], $subFolders),
475
			\array_map([$this, 'trackToApi'], $tracks)
476
		);
477
478
		return $this->subsonicResponse([
479
			'directory' => [
480
				'id' => $id,
481
				'parent' => 'folder-' . $folder->getParent()->getId(),
482
				'name' => $folder->getName(),
483
				'child' => $children
484
			]
485
		]);
486
	}
487
488
	private function getIndexesForArtists() {
489
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, SortBy::Name);
490
	
491
		$indexes = [];
492
		foreach ($artists as $artist) {
493
			$indexes[$artist->getIndexingChar()][] = $this->artistToApi($artist);
494
		}
495
	
496
		$result = [];
497
		foreach ($indexes as $indexChar => $bucketArtists) {
498
			$result[] = ['name' => $indexChar, 'artist' => $bucketArtists];
499
		}
500
	
501
		return $this->subsonicResponse(['indexes' => ['index' => $result]]);
502
	}
503
504
	private function getMusicDirectoryForArtist($id) {
505
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
506
507
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
508
		$artistName = $artist->getNameString($this->l10n);
509
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
510
511
		$children = [];
512
		foreach ($albums as $album) {
513
			$children[] = $this->albumToOldApi($album, $artistName);
514
		}
515
516
		return $this->subsonicResponse([
517
			'directory' => [
518
				'id' => $id,
519
				'parent' => 'artists',
520
				'name' => $artistName,
521
				'child' => $children
522
			]
523
		]);
524
	}
525
526
	private function getMusicDirectoryForAlbum($id) {
527
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
528
529
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
530
		$albumName = $album->getNameString($this->l10n);
531
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
532
533
		return $this->subsonicResponse([
534
			'directory' => [
535
				'id' => $id,
536
				'parent' => 'artist-' . $album->getAlbumArtistId(),
537
				'name' => $albumName,
538
				'child' => \array_map([$this, 'trackToApi'], $tracks)
539
			]
540
		]);
541
	}
542
543
	/**
544
	 * @param Folder $folder
545
	 * @return array
546
	 */
547
	private function folderToApi($folder) {
548
		return [
549
			'id' => 'folder-' . $folder->getId(),
550
			'title' => $folder->getName(),
551
			'isDir' => true
552
		];
553
	}
554
555
	/**
556
	 * @param Artist $artist
557
	 * @return array
558
	 */
559
	private function artistToApi($artist) {
560
		return [
561
			'name' => $artist->getNameString($this->l10n),
562
			'id' => 'artist-' . $artist->getId()
563
		];
564
	}
565
566
	/**
567
	 * The "old API" format is used e.g. in getMusicDirectory and getAlbumList
568
	 * @param Album $album
569
	 * @param string|null $artistName
570
	 * @return array
571
	 */
572
	private function albumToOldApi($album, $artistName = null) {
573
		$artistId = $album->getAlbumArtistId();
574
575
		if (empty($artistName)) {
576
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
577
			$artistName = $artist->getNameString($this->l10n);
578
		}
579
580
		$result = [
581
			'id' => 'album-' . $album->getId(),
582
			'parent' => 'artist-' . $artistId,
583
			'title' => $album->getNameString($this->l10n),
584
			'artist' => $artistName,
585
			'isDir' => true
586
		];
587
588
		if (!empty($album->getCoverFileId())) {
589
			$result['coverArt'] = $album->getId();
590
		}
591
592
		return $result;
593
	}
594
595
	/**
596
	 * The "new API" format is used e.g. in getAlbum and getAlbumList2
597
	 * @param Album $album
598
	 * @param string|null $artistName
599
	 * @return array
600
	 */
601
	private function albumToNewApi($album, $artistName = null) {
602
		$artistId = $album->getAlbumArtistId();
603
604
		if (empty($artistName)) {
605
			$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
606
			$artistName = $artist->getNameString($this->l10n);
607
		}
608
609
		$result = [
610
			'id' => 'album-' . $album->getId(),
611
			'artistId' => 'artist-' . $artistId,
612
			'name' => $album->getNameString($this->l10n),
613
			'artist' => $artistName,
614
			'songCount' => $this->trackBusinessLayer->countByAlbum($album->getId()),
615
			//'duration' => 0
616
		];
617
618
		if (!empty($album->getCoverFileId())) {
619
			$result['coverArt'] = $album->getId();
620
		}
621
622
		return $result;
623
	}
624
625
	/**
626
	 * The same API format is used both on "old" and "new" API methods. The "new" API adds some
627
	 * new fields for the songs, but providing some extra fields shouldn't be a problem for the
628
	 * older clients. 
629
	 * @param Track $track
630
	 * @param Album|null $album
631
	 * @param string|null $albumName
632
	 * @return array
633
	 */
634
	private function trackToApi($track, $album = null, $albumName = null) {
635
		$albumId = $track->getAlbumId();
636
		if ($album == null) {
637
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
638
		}
639
		if (empty($albumName)) {
640
			$albumName = $album->getNameString($this->l10n);
641
		}
642
643
		$trackArtist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
644
		$result = [
645
			'id' => 'track-' . $track->getId(),
646
			'parent' => 'album-' . $albumId,
647
			'title' => $track->getTitle(),
648
			'artist' => $trackArtist->getNameString($this->l10n),
649
			'isDir' => false,
650
			'album' => $albumName,
651
			//'genre' => '',
652
			'year' => $track->getYear(),
653
			'size' => $track->getSize(),
654
			'contentType' => $track->getMimetype(),
655
			'suffix' => \end(\explode('.', $track->getFilename())),
656
			'duration' => $track->getLength() ?: 0,
657
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
658
			//'path' => '',
659
			'isVideo' => false,
660
			'albumId' => 'album-' . $albumId,
661
			'artistId' => 'artist-' . $track->getArtistId(),
662
			'type' => 'music'
663
		];
664
665
		if (!empty($album->getCoverFileId())) {
666
			$result['coverArt'] = $album->getId();
667
		}
668
669
		if ($track->getNumber() !== null) {
0 ignored issues
show
introduced by
The condition $track->getNumber() !== null is always true.
Loading history...
670
			$result['track'] = $track->getNumber();
671
		}
672
673
		return $result;
674
	}
675
676
	/**
677
	 * @param Playlist $playlist
678
	 * @return array
679
	 */
680
	private function playlistToApi($playlist) {
681
		return [
682
			'id' => $playlist->getId(),
683
			'name' => $playlist->getName(),
684
			'owner' => $this->userId,
685
			'public' => false,
686
			'songCount' => $playlist->getTrackCount(),
687
			// comment => '',
688
			// duration => '',
689
			// created => '',
690
			// coverArt => ''
691
		];
692
	}
693
694
	/**
695
	 * Common logic for getAlbumList and getAlbumList2
696
	 * @return Album[]
697
	 */
698
	private function albumsForGetAlbumList() {
699
		$type = $this->getRequiredParam('type');
700
		$size = $this->request->getParam('size', 10);
701
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
702
		$offset = $this->request->getParam('offset', 0);
703
704
		$albums = [];
705
706
		switch ($type) {
707
			case 'random':
708
				$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
709
				$albums = self::randomItems($allAlbums, $size);
710
				// Note: offset is not supported on this type
711
				break;
712
			case 'alphabeticalByName':
713
				$albums = $this->albumBusinessLayer->findAll($this->userId, SortBy::Name, $size, $offset);
714
				break;
715
			case 'alphabeticalByArtist':
716
			case 'newest':
717
			case 'highest':
718
			case 'frequent':
719
			case 'recent':
720
			case 'starred':
721
			case 'byYear':
722
			case 'byGenre':
723
			default:
724
				$this->logger->log("Album list type '$type' is not supported", 'warn');
725
				break;
726
		}
727
728
		return $albums;
729
	}
730
731
	/**
732
	 * Given a prefixed ID like 'artist-123' or 'track-45', return just the numeric part.
733
	 * @param string $id
734
	 * @return integer
735
	 */
736
	private static function ripIdPrefix($id) {
737
		return (int)(\explode('-', $id)[1]);
738
	}
739
740
	private static function randomItems($itemArray, $count) {
741
		$count = \min($count, \count($itemArray)); // can't return more than all items
742
		$indices = \array_rand($itemArray, $count);
743
		if ($count == 1) { // return type is not array when randomizing a single index
744
			$indices = [$indices];
745
		}
746
747
		$result = [];
748
		foreach ($indices as $index) {
749
			$result[] = $itemArray[$index];
750
		}
751
752
		return $result;
753
	}
754
755
	private function subsonicResponse($content, $status = 'ok') {
756
		$content['status'] = $status; 
757
		$content['version'] = self::API_VERSION;
758
		$responseData = ['subsonic-response' => $content];
759
760
		if ($this->format == 'json') {
761
			$response = new JSONResponse($responseData);
762
		} else if ($this->format == 'jsonp') {
763
			$responseData = \json_encode($responseData);
764
			$response = new DataDisplayResponse("{$this->callback}($responseData);");
765
			$response->addHeader('Content-Type', 'text/javascript; charset=UTF-8');
766
		} else {
767
			$response = new XMLResponse($responseData);
768
		}
769
770
		return $response;
771
	}
772
773
	public function subsonicErrorResponse($errorCode, $errorMessage) {
774
		return $this->subsonicResponse([
775
				'error' => [
776
					'code' => $errorCode,
777
					'message' => $errorMessage
778
				]
779
			], 'failed');
780
	}
781
}
782