Passed
Push — feature/329_Subsonic_API ( 87cb9d...0c15c8 )
by Pauli
12:53
created

SubsonicController::trackToApi()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 40
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 28
c 1
b 0
f 0
nc 16
nop 3
dl 0
loc 40
rs 8.5386
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