Completed
Pull Request — master (#718)
by Pauli
08:54 queued 07:34
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\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