Passed
Push — master ( 31284f...e373e6 )
by Pauli
01:55
created

SubsonicController::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 36
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 16
nc 1
nop 15
dl 0
loc 36
ccs 0
cts 17
cp 0
crap 6
rs 9.7333
c 2
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, 2020
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\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...
21
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...
22
23
use \OCA\Music\AppFramework\Core\Logger;
24
use \OCA\Music\AppFramework\Utility\MethodAnnotationReader;
25
26
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
27
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
28
use \OCA\Music\BusinessLayer\GenreBusinessLayer;
29
use \OCA\Music\BusinessLayer\Library;
30
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
31
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
32
33
use \OCA\Music\Db\Album;
34
use \OCA\Music\Db\Artist;
35
use \OCA\Music\Db\Genre;
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\DetailsHelper;
47
use \OCA\Music\Utility\Random;
48
use \OCA\Music\Utility\UserMusicFolder;
49
use \OCA\Music\Utility\Util;
50
51
class SubsonicController extends Controller {
52
	const API_VERSION = '1.10.2';
53
54
	private $albumBusinessLayer;
55
	private $artistBusinessLayer;
56
	private $playlistBusinessLayer;
57
	private $genreBusinessLayer;
58
	private $trackBusinessLayer;
59
	private $library;
60
	private $urlGenerator;
61
	private $userMusicFolder;
62
	private $l10n;
63
	private $coverHelper;
64
	private $detailsHelper;
65
	private $random;
66
	private $logger;
67
	private $userId;
68
	private $format;
69
	private $callback;
70
	private $timezone;
71
72
	public function __construct($appname,
73
								IRequest $request,
74
								$l10n,
75
								IURLGenerator $urlGenerator,
76
								AlbumBusinessLayer $albumBusinessLayer,
77
								ArtistBusinessLayer $artistBusinessLayer,
78
								GenreBusinessLayer $genreBusinessLayer,
79
								PlaylistBusinessLayer $playlistBusinessLayer,
80
								TrackBusinessLayer $trackBusinessLayer,
81
								Library $library,
82
								UserMusicFolder $userMusicFolder,
83
								CoverHelper $coverHelper,
84
								DetailsHelper $detailsHelper,
85
								Random $random,
86
								Logger $logger) {
87
		parent::__construct($appname, $request);
88
89
		$this->albumBusinessLayer = $albumBusinessLayer;
90
		$this->artistBusinessLayer = $artistBusinessLayer;
91
		$this->genreBusinessLayer = $genreBusinessLayer;
92
		$this->playlistBusinessLayer = $playlistBusinessLayer;
93
		$this->trackBusinessLayer = $trackBusinessLayer;
94
		$this->library = $library;
95
		$this->urlGenerator = $urlGenerator;
96
		$this->l10n = $l10n;
97
		$this->userMusicFolder = $userMusicFolder;
98
		$this->coverHelper = $coverHelper;
99
		$this->detailsHelper = $detailsHelper;
100
		$this->random = $random;
101
		$this->logger = $logger;
102
103
		// For timestamps in the Subsonic API, we would prefer to use the local timezone,
104
		// but the core has set the default timezone as 'UTC'. Get the timezone from php.ini
105
		// if available, and store it for later use.
106
		$tz = \ini_get('date.timezone') ?: 'UTC';
107
		$this->timezone = new \DateTimeZone($tz);
108
	}
109
110
	/**
111
	 * Called by the middleware to set the reponse format to be used
112
	 * @param string $format Response format: xml/json/jsonp
113
	 * @param string|null $callback Function name to use if the @a $format is 'jsonp'
114
	 */
115
	public function setResponseFormat($format, $callback) {
116
		$this->format = $format;
117
		$this->callback = $callback;
118
	}
119
120
	/**
121
	 * Called by the middleware once the user credentials have been checked
122
	 * @param string $userId
123
	 */
124
	public function setAuthenticatedUser($userId) {
125
		$this->userId = $userId;
126
	}
127
128
	/**
129
	 * @NoAdminRequired
130
	 * @PublicPage
131
	 * @NoCSRFRequired
132
	 */
133
	public function handleRequest($method) {
134
		$this->logger->log("Subsonic request $method", 'debug');
135
136
		// Allow calling all methods with or without the postfix ".view"
137
		if (Util::endsWith($method, ".view")) {
138
			$method = \substr($method, 0, -\strlen(".view"));
139
		}
140
141
		// Allow calling any functions annotated to be part of the API
142
		if (\method_exists($this, $method)) {
143
			$annotationReader = new MethodAnnotationReader($this, $method);
144
			if ($annotationReader->hasAnnotation('SubsonicAPI')) {
145
				return $this->$method();
146
			}
147
		}
148
149
		$this->logger->log("Request $method not supported", 'warn');
150
		return $this->subsonicErrorResponse(70, "Requested action $method is not supported");
151
	}
152
153
	/* -------------------------------------------------------------------------
154
	 * REST API methods
155
	 *------------------------------------------------------------------------*/
156
157
	/**
158
	 * @SubsonicAPI
159
	 */
160
	private function ping() {
161
		return $this->subsonicResponse([]);
162
	}
163
164
	/**
165
	 * @SubsonicAPI
166
	 */
167
	private function getLicense() {
168
		return $this->subsonicResponse([
169
			'license' => [
170
				'valid' => 'true'
171
			]
172
		]);
173
	}
174
175
	/**
176
	 * @SubsonicAPI
177
	 */
178
	private function getMusicFolders() {
179
		// Only single root folder is supported
180
		return $this->subsonicResponse([
181
			'musicFolders' => ['musicFolder' => [
182
				['id' => 'artists', 'name' => $this->l10n->t('Artists')],
183
				['id' => 'folders', 'name' => $this->l10n->t('Folders')]
184
			]]
185
		]);
186
	}
187
188
	/**
189
	 * @SubsonicAPI
190
	 */
191
	private function getIndexes() {
192
		$id = $this->request->getParam('musicFolderId');
193
194
		if ($id === 'folders') {
195
			return $this->getIndexesForFolders();
196
		} else {
197
			return $this->getIndexesForArtists();
198
		}
199
	}
200
201
	/**
202
	 * @SubsonicAPI
203
	 */
204
	private function getMusicDirectory() {
205
		$id = $this->getRequiredParam('id');
206
207
		if (Util::startsWith($id, 'folder-')) {
208
			return $this->getMusicDirectoryForFolder($id);
209
		} elseif (Util::startsWith($id, 'artist-')) {
210
			return $this->getMusicDirectoryForArtist($id);
211
		} else {
212
			return $this->getMusicDirectoryForAlbum($id);
213
		}
214
	}
215
216
	/**
217
	 * @SubsonicAPI
218
	 */
219
	private function getAlbumList() {
220
		$albums = $this->albumsForGetAlbumList();
221
		return $this->subsonicResponse(['albumList' =>
222
				['album' => \array_map([$this, 'albumToOldApi'], $albums)]
223
		]);
224
	}
225
226
	/**
227
	 * @SubsonicAPI
228
	 */
229
	private function getAlbumList2() {
230
		/*
231
		 * According to the API specification, the difference between this and getAlbumList
232
		 * should be that this function would organize albums according the metadata while
233
		 * getAlbumList would organize them by folders. However, we organize by metadata
234
		 * also in getAlbumList, because that's more natural for the Music app and many/most
235
		 * clients do not support getAlbumList2.
236
		 */
237
		$albums = $this->albumsForGetAlbumList();
238
		return $this->subsonicResponse(['albumList2' =>
239
				['album' => \array_map([$this, 'albumToNewApi'], $albums)]
240
		]);
241
	}
242
243
	/**
244
	 * @SubsonicAPI
245
	 */
246
	private function getArtists() {
247
		return $this->getIndexesForArtists('artists');
248
	}
249
250
	/**
251
	 * @SubsonicAPI
252
	 */
253
	private function getArtist() {
254
		$id = $this->getRequiredParam('id');
255
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
256
257
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
258
		$artistName = $artist->getNameString($this->l10n);
259
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
260
261
		$artistNode = $this->artistToApi($artist);
262
		$artistNode['album'] = \array_map(function($album) use ($artistName) {
263
			return $this->albumToNewApi($album, $artistName);
264
		}, $albums);
265
266
		return $this->subsonicResponse(['artist' => $artistNode]);
267
	}
268
269
	/**
270
	 * @SubsonicAPI
271
	 */
272
	private function getAlbum() {
273
		$id = $this->getRequiredParam('id');
274
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
275
276
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
277
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
278
279
		$albumNode = $this->albumToNewApi($album);
280
		$albumNode['song'] = \array_map(function($track) use ($album) {
281
			$track->setAlbum($album);
282
			return $this->trackToApi($track);
283
		}, $tracks);
284
		return $this->subsonicResponse(['album' => $albumNode]);
285
	}
286
287
	/**
288
	 * @SubsonicAPI
289
	 */
290
	private function getSong() {
291
		$id = $this->getRequiredParam('id');
292
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
293
		$track = $this->trackBusinessLayer->find($trackId, $this->userId);
294
295
		return $this->subsonicResponse(['song' => $this->trackToApi($track)]);
296
	}
297
298
	/**
299
	 * @SubsonicAPI
300
	 */
301
	private function getRandomSongs() {
302
		$size = $this->request->getParam('size', 10);
303
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
304
		$genre = $this->request->getParam('genre');
305
		$fromYear = $this->request->getParam('fromYear');
306
		$toYear = $this->request->getParam('toYear');
307
308
		if ($genre !== null) {
309
			$trackPool = $this->findTracksByGenre($genre);
310
		} else {
311
			$trackPool = $this->trackBusinessLayer->findAll($this->userId);
312
		}
313
314
		if ($fromYear !== null) {
315
			$trackPool = \array_filter($trackPool, function($track) use ($fromYear) {
316
				return ($track->getYear() !== null && $track->getYear() >= $fromYear);
317
			});
318
		}
319
320
		if ($toYear !== null) {
321
			$trackPool = \array_filter($trackPool, function($track) use ($toYear) {
322
				return ($track->getYear() !== null && $track->getYear() <= $toYear);
323
			});
324
		}
325
326
		$tracks = Random::pickItems($trackPool, $size);
327
328
		return $this->subsonicResponse(['randomSongs' =>
329
				['song' => \array_map([$this, 'trackToApi'], $tracks)]
330
		]);
331
	}
332
333
	/**
334
	 * @SubsonicAPI
335
	 */
336
	private function getCoverArt() {
337
		$id = $this->getRequiredParam('id');
338
		$size = $this->request->getParam('size');
339
340
		$rootFolder = $this->userMusicFolder->getFolder($this->userId);
341
		$coverData = $this->coverHelper->getCover($id, $this->userId, $rootFolder, $size);
342
343
		if ($coverData !== null) {
344
			return new FileResponse($coverData);
345
		}
346
347
		return $this->subsonicErrorResponse(70, 'album has no cover');
348
	}
349
350
	/**
351
	 * @SubsonicAPI
352
	 */
353
	private function getLyrics() {
354
		$artistPar = $this->request->getParam('artist');
355
		$titlePar = $this->request->getParam('title');
356
357
		$matches = $this->trackBusinessLayer->findAllByNameAndArtistName($titlePar, $artistPar, $this->userId);
358
		$matchingCount = \count($matches);
359
360
		if ($matchingCount === 0) {
361
			$this->logger->log("No matching track for title '$titlePar' and artist '$artistPar'", 'debug');
362
			return $this->subsonicResponse(['lyrics' => new \stdClass]);
363
		}
364
		else {
365
			if ($matchingCount > 1) {
366
				$this->logger->log("Found $matchingCount tracks matching title ".
367
									"'$titlePar' and artist '$artistPar'; using the first", 'debug');
368
			}
369
			$track = $matches[0];
370
371
			$artist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
372
			$rootFolder = $this->userMusicFolder->getFolder($this->userId);
373
			$lyrics = $this->detailsHelper->getLyrics($track->getFileId(), $rootFolder);
374
375
			return $this->subsonicResponse(['lyrics' => [
376
					'artist' => $artist->getNameString($this->l10n),
377
					'title' => $track->getTitle(),
378
					'value' => $lyrics
379
			]]);
380
		}
381
	}
382
383
	/**
384
	 * @SubsonicAPI
385
	 */
386
	private function stream() {
387
		// We don't support transcaoding, so 'stream' and 'download' act identically
388
		return $this->download();
389
	}
390
391
	/**
392
	 * @SubsonicAPI
393
	 */
394
	private function download() {
395
		$id = $this->getRequiredParam('id');
396
		$trackId = self::ripIdPrefix($id); // get rid of 'track-' prefix
397
398
		$track = $this->trackBusinessLayer->find($trackId, $this->userId);
399
		$file = $this->getFilesystemNode($track->getFileId());
400
401
		if ($file instanceof File) {
402
			return new FileResponse($file);
403
		} else {
404
			return $this->subsonicErrorResponse(70, 'file not found');
405
		}
406
	}
407
408
	/**
409
	 * @SubsonicAPI
410
	 */
411
	private function search2() {
412
		$results = $this->doSearch();
413
		return $this->searchResponse('searchResult2', $results, /*$useNewApi=*/false);
414
	}
415
416
	/**
417
	 * @SubsonicAPI
418
	 */
419
	private function search3() {
420
		$results = $this->doSearch();
421
		return $this->searchResponse('searchResult3', $results, /*$useNewApi=*/true);
422
	}
423
424
	/**
425
	 * @SubsonicAPI
426
	 */
427
	private function getGenres() {
428
		$genres = $this->genreBusinessLayer->findAllWithCounts($this->userId);
429
430
		return $this->subsonicResponse(['genres' =>
431
			[
432
				'genre' => \array_map(function($genre) {
433
					return [
434
						'songCount' => $genre->getTrackCount(),
435
						'albumCount' => $genre->getAlbumCount(),
436
						'value' => $genre->getNameString($this->l10n)
437
					];
438
				},
439
				$genres)
440
			]
441
		]);
442
	}
443
444
	/**
445
	 * @SubsonicAPI
446
	 */
447
	private function getSongsByGenre() {
448
		$genre = $this->getRequiredParam('genre');
449
		$count = $this->request->getParam('count', 10);
450
		$offset = $this->request->getParam('offset', 0);
451
452
		$tracks = $this->findTracksByGenre($genre, $count, $offset);
453
454
		return $this->subsonicResponse(['songsByGenre' =>
455
			['song' => \array_map([$this, 'trackToApi'], $tracks)]
456
		]);
457
	}
458
459
	/**
460
	 * @SubsonicAPI
461
	 */
462
	private function getPlaylists() {
463
		$playlists = $this->playlistBusinessLayer->findAll($this->userId);
464
465
		return $this->subsonicResponse(['playlists' =>
466
			['playlist' => \array_map([$this, 'playlistToApi'], $playlists)]
467
		]);
468
	}
469
470
	/**
471
	 * @SubsonicAPI
472
	 */
473
	private function getPlaylist() {
474
		$id = $this->getRequiredParam('id');
475
		$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
476
		$tracks = $this->playlistBusinessLayer->getPlaylistTracks($id, $this->userId);
477
478
		$playlistNode = $this->playlistToApi($playlist);
479
		$playlistNode['entry'] = \array_map([$this, 'trackToApi'], $tracks);
480
481
		return $this->subsonicResponse(['playlist' => $playlistNode]);
482
	}
483
484
	/**
485
	 * @SubsonicAPI
486
	 */
487
	private function createPlaylist() {
488
		$name = $this->getRequiredParam('name');
489
		$songIds = $this->getRepeatedParam('songId');
490
		$songIds = \array_map('self::ripIdPrefix', $songIds);
491
492
		$playlist = $this->playlistBusinessLayer->create($name, $this->userId);
493
		$this->playlistBusinessLayer->addTracks($songIds, $playlist->getId(), $this->userId);
494
495
		return $this->subsonicResponse([]);
496
	}
497
498
	/**
499
	 * @SubsonicAPI
500
	 */
501
	private function updatePlaylist() {
502
		$listId = $this->getRequiredParam('playlistId');
503
		$newName = $this->request->getParam('name');
504
		$newComment = $this->request->getParam('comment');
505
		$songIdsToAdd = $this->getRepeatedParam('songIdToAdd');
506
		$songIdsToAdd = \array_map('self::ripIdPrefix', $songIdsToAdd);
507
		$songIndicesToRemove = $this->getRepeatedParam('songIndexToRemove');
508
509
		if (!empty($newName)) {
510
			$this->playlistBusinessLayer->rename($newName, $listId, $this->userId);
511
		}
512
513
		if ($newComment !== null) {
514
			$this->playlistBusinessLayer->setComment($newComment, $listId, $this->userId);
515
		}
516
517
		if (!empty($songIndicesToRemove)) {
518
			$this->playlistBusinessLayer->removeTracks($songIndicesToRemove, $listId, $this->userId);
519
		}
520
521
		if (!empty($songIdsToAdd)) {
522
			$this->playlistBusinessLayer->addTracks($songIdsToAdd, $listId, $this->userId);
523
		}
524
525
		return $this->subsonicResponse([]);
526
	}
527
528
	/**
529
	 * @SubsonicAPI
530
	 */
531
	private function deletePlaylist() {
532
		$id = $this->getRequiredParam('id');
533
		$this->playlistBusinessLayer->delete($id, $this->userId);
534
		return $this->subsonicResponse([]);
535
	}
536
537
	/**
538
	 * @SubsonicAPI
539
	 */
540
	private function getUser() {
541
		$username = $this->getRequiredParam('username');
542
543
		if ($username != $this->userId) {
544
			throw new SubsonicException("{$this->userId} is not authorized to get details for other users.", 50);
545
		}
546
547
		return $this->subsonicResponse([
548
			'user' => [
549
				'username' => $username,
550
				'email' => '',
551
				'scrobblingEnabled' => false,
552
				'adminRole' => false,
553
				'settingsRole' => false,
554
				'downloadRole' => true,
555
				'uploadRole' => false,
556
				'playlistRole' => true,
557
				'coverArtRole' => false,
558
				'commentRole' => false,
559
				'podcastRole' => false,
560
				'streamRole' => true,
561
				'jukeboxRole' => false,
562
				'shareRole' => false,
563
				'videoConversionRole' => false,
564
				'folder' => ['artists', 'folders'],
565
			]
566
		]);
567
	}
568
569
	/**
570
	 * @SubsonicAPI
571
	 */
572
	private function getUsers() {
573
		throw new SubsonicException("{$this->userId} is not authorized to get details for other users.", 50);
574
	}
575
576
	/**
577
	 * @SubsonicAPI
578
	 */
579
	private function getAvatar() {
580
		// TODO: Use 'username' parameter to fetch user-specific avatar from the OC core.
581
		// Remember to check the permission.
582
		// For now, use the Music app logo for all users.
583
		$fileName = \join(DIRECTORY_SEPARATOR, [\dirname(__DIR__), 'img', 'logo', 'music_logo.png']);
584
		$content = \file_get_contents($fileName);
585
		return new FileResponse(['content' => $content, 'mimetype' => 'image/png']);
586
	}
587
588
	/**
589
	 * @SubsonicAPI
590
	 */
591
	private function star() {
592
		$targetIds = $this->getStarringParameters();
593
594
		$this->trackBusinessLayer->setStarred($targetIds['tracks'], $this->userId);
0 ignored issues
show
Bug introduced by
It seems like $targetIds['tracks'] can also be of type string[]; however, parameter $ids of OCA\Music\AppFramework\B...nessLayer::setStarred() does only seem to accept integer[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

594
		$this->trackBusinessLayer->setStarred(/** @scrutinizer ignore-type */ $targetIds['tracks'], $this->userId);
Loading history...
595
		$this->albumBusinessLayer->setStarred($targetIds['albums'], $this->userId);
596
		$this->artistBusinessLayer->setStarred($targetIds['artists'], $this->userId);
597
598
		return $this->subsonicResponse([]);
599
	}
600
601
	/**
602
	 * @SubsonicAPI
603
	 */
604
	private function unstar() {
605
		$targetIds = $this->getStarringParameters();
606
607
		$this->trackBusinessLayer->unsetStarred($targetIds['tracks'], $this->userId);
0 ignored issues
show
Bug introduced by
It seems like $targetIds['tracks'] can also be of type string[]; however, parameter $ids of OCA\Music\AppFramework\B...ssLayer::unsetStarred() does only seem to accept integer[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

607
		$this->trackBusinessLayer->unsetStarred(/** @scrutinizer ignore-type */ $targetIds['tracks'], $this->userId);
Loading history...
608
		$this->albumBusinessLayer->unsetStarred($targetIds['albums'], $this->userId);
609
		$this->artistBusinessLayer->unsetStarred($targetIds['artists'], $this->userId);
610
611
		return $this->subsonicResponse([]);
612
	}
613
614
	/**
615
	 * @SubsonicAPI
616
	 */
617
	private function getStarred() {
618
		$starred = $this->doGetStarred();
619
		return $this->searchResponse('starred', $starred, /*$useNewApi=*/false);
620
	}
621
622
	/**
623
	 * @SubsonicAPI
624
	 */
625
	private function getStarred2() {
626
		$starred = $this->doGetStarred();
627
		return $this->searchResponse('starred2', $starred, /*$useNewApi=*/true);
628
	}
629
630
	/**
631
	 * @SubsonicAPI
632
	 */
633
	private function getVideos() {
634
		// Feature not supported, return an empty list
635
		return $this->subsonicResponse([
636
			'videos' => [
637
				'video' => []
638
			]
639
		]);
640
	}
641
642
	/**
643
	 * @SubsonicAPI
644
	 */
645
	 private function getPodcasts() {
646
		// Feature not supported, return an empty list
647
		return $this->subsonicResponse([
648
			'podcasts' => [
649
				'channel' => []
650
			]
651
		]);
652
	}
653
654
	/* -------------------------------------------------------------------------
655
	 * Helper methods
656
	 *------------------------------------------------------------------------*/
657
658
	private function getRequiredParam($paramName) {
659
		$param = $this->request->getParam($paramName);
660
661
		if ($param === null) {
662
			throw new SubsonicException("Required parameter '$paramName' missing", 10);
663
		}
664
665
		return $param;
666
	}
667
668
	/**
669
	 * Get parameters used in the `star` and `unstar` API methods
670
	 */
671
	private function getStarringParameters() {
672
		// album IDs from newer clients
673
		$albumIds = $this->getRepeatedParam('albumId');
674
		$albumIds = \array_map('self::ripIdPrefix', $albumIds);
675
676
		// artist IDs from newer clients
677
		$artistIds = $this->getRepeatedParam('artistId');
678
		$artistIds = \array_map('self::ripIdPrefix', $artistIds);
679
680
		// song IDs from newer clients and song/folder/album/artist IDs from older clients
681
		$ids = $this->getRepeatedParam('id');
682
683
		$trackIds = [];
684
685
		foreach ($ids as $prefixedId) {
686
			$parts = \explode('-', $prefixedId);
687
			$type = $parts[0];
688
			$id = $parts[1];
689
690
			if ($type == 'track') {
691
				$trackIds[] = $id;
692
			} elseif ($type == 'album') {
693
				$albumIds[] = $id;
694
			} elseif ($type == 'artist') {
695
				$artistIds[] = $id;
696
			} elseif ($type == 'folder') {
697
				throw new SubsonicException('Starring folders is not supported', 0);
698
			} else {
699
				throw new SubsonicException("Unexpected ID format: $prefixedId", 0);
700
			}
701
		}
702
703
		return [
704
			'tracks' => $trackIds,
705
			'albums' => $albumIds,
706
			'artists' => $artistIds
707
		];
708
	}
709
710
	/** 
711
	 * Get values for parameter which may be present multiple times in the query
712
	 * string or POST data.
713
	 * @param string $paramName
714
	 * @return string[]
715
	 */
716
	private function getRepeatedParam($paramName) {
717
		// We can't use the IRequest object nor $_GET and $_POST to get the data
718
		// because all of these are based to the idea of unique parameter names.
719
		// If the same name is repeated, only the last value is saved. Hence, we
720
		// need to parse the raw data manually.
721
722
		// query string is always present (although it could be empty)
723
		$values = $this->parseRepeatedKeyValues($paramName, $_SERVER['QUERY_STRING']);
724
725
		// POST data is available if the method is POST
726
		if ($this->request->getMethod() == 'POST') {
727
			$values = \array_merge($values,
728
					$this->parseRepeatedKeyValues($paramName, file_get_contents('php://input')));
729
		}
730
731
		return $values;
732
	}
733
734
	/**
735
	 * Parse a string like "someKey=value1&someKey=value2&anotherKey=valueA&someKey=value3"
736
	 * and return an array of values for the given key
737
	 * @param string $key
738
	 * @param string $data
739
	 */
740
	private function parseRepeatedKeyValues($key, $data) {
741
		$result = [];
742
743
		$keyValuePairs = \explode('&', $data);
744
745
		foreach ($keyValuePairs as $pair) {
746
			$keyAndValue = \explode('=', $pair);
747
748
			if ($keyAndValue[0] == $key) {
749
				$result[] = $keyAndValue[1];
750
			}
751
		}
752
753
		return $result;
754
	}
755
756
	private function getFilesystemNode($id) {
757
		$rootFolder = $this->userMusicFolder->getFolder($this->userId);
758
		$nodes = $rootFolder->getById($id);
759
760
		if (\count($nodes) != 1) {
761
			throw new SubsonicException('file not found', 70);
762
		}
763
764
		return $nodes[0];
765
	}
766
767
	private function getIndexesForFolders() {
768
		$rootFolder = $this->userMusicFolder->getFolder($this->userId);
769
770
		return $this->subsonicResponse(['indexes' => ['index' => [
771
			['name' => '*',
772
			'artist' => [['id' => 'folder-' . $rootFolder->getId(), 'name' => $rootFolder->getName()]]]
773
		]]]);
774
	}
775
776
	private function getMusicDirectoryForFolder($id) {
777
		$folderId = self::ripIdPrefix($id); // get rid of 'folder-' prefix
778
		$folder = $this->getFilesystemNode($folderId);
779
780
		if (!($folder instanceof Folder)) {
781
			throw new SubsonicException("$id is not a valid folder", 70);
782
		}
783
784
		$nodes = $folder->getDirectoryListing();
785
		$subFolders = \array_filter($nodes, function ($n) {
786
			return $n instanceof Folder;
787
		});
788
		$tracks = $this->trackBusinessLayer->findAllByFolder($folderId, $this->userId);
789
790
		// A folder may contain thousands of audio files, and getting artist and album data
791
		// for each of those individually would take a lot of time and great many DB queries.
792
		// To prevent having to do this in `trackToApi`, we fetch all the albums and artists
793
		// in one go.
794
		$this->injectAlbumsArtistsAndGenresToTracks($tracks);
795
796
		$children = \array_merge(
797
			\array_map([$this, 'folderToApi'], $subFolders),
798
			\array_map([$this, 'trackToApi'], $tracks)
799
		);
800
801
		$content = [
802
			'directory' => [
803
				'id' => $id,
804
				'name' => $folder->getName(),
805
				'child' => $children
806
			]
807
		];
808
809
		// Parent folder ID is included if and only if the parent folder is not the top level
810
		$rootFolderId = $this->userMusicFolder->getFolder($this->userId)->getId();
811
		$parentFolderId = $folder->getParent()->getId();
812
		if ($rootFolderId != $parentFolderId) {
813
			$content['parent'] = 'folder-' . $parentFolderId;
814
		}
815
816
		return $this->subsonicResponse($content);
817
	}
818
819
	private function injectAlbumsArtistsAndGenresToTracks(&$tracks) {
820
		$albumIds = [];
821
		$artistIds = [];
822
823
		// get unique album and artist IDs
824
		foreach ($tracks as $track) {
825
			$albumIds[$track->getAlbumId()] = 1;
826
			$artistIds[$track->getArtistId()] = 1;
827
		}
828
		$albumIds = \array_keys($albumIds);
829
		$artistIds = \array_keys($artistIds);
830
831
		// get the corresponding entities from the business layer
832
		$albums = $this->albumBusinessLayer->findById($albumIds, $this->userId);
833
		$artists = $this->artistBusinessLayer->findById($artistIds, $this->userId);
834
		$genres = $this->genreBusinessLayer->findAll($this->userId);
835
836
		// create hash tables "id => entity" for the albums and artists for fast access
837
		$albumMap = Util::createIdLookupTable($albums);
838
		$artistMap = Util::createIdLookupTable($artists);
839
		$genreMap = Util::createIdLookupTable($genres);
840
841
		// finally, set the references on the tracks
842
		foreach ($tracks as &$track) {
843
			$track->setAlbum($albumMap[$track->getAlbumId()]);
844
			$track->setArtist($artistMap[$track->getArtistId()]);
845
			$track->setGenre($genreMap[$track->getGenreId()]);
846
		}
847
	}
848
849
	private function getIndexesForArtists($rootElementName = 'indexes') {
850
		$artists = $this->artistBusinessLayer->findAllHavingAlbums($this->userId, SortBy::Name);
851
	
852
		$indexes = [];
853
		foreach ($artists as $artist) {
854
			$indexes[$artist->getIndexingChar()][] = $this->artistToApi($artist);
855
		}
856
	
857
		$result = [];
858
		foreach ($indexes as $indexChar => $bucketArtists) {
859
			$result[] = ['name' => $indexChar, 'artist' => $bucketArtists];
860
		}
861
	
862
		return $this->subsonicResponse([$rootElementName => ['index' => $result]]);
863
	}
864
865
	private function getMusicDirectoryForArtist($id) {
866
		$artistId = self::ripIdPrefix($id); // get rid of 'artist-' prefix
867
868
		$artist = $this->artistBusinessLayer->find($artistId, $this->userId);
869
		$artistName = $artist->getNameString($this->l10n);
870
		$albums = $this->albumBusinessLayer->findAllByAlbumArtist($artistId, $this->userId);
871
872
		$children = [];
873
		foreach ($albums as $album) {
874
			$children[] = $this->albumToOldApi($album, $artistName);
875
		}
876
877
		return $this->subsonicResponse([
878
			'directory' => [
879
				'id' => $id,
880
				'name' => $artistName,
881
				'child' => $children
882
			]
883
		]);
884
	}
885
886
	private function getMusicDirectoryForAlbum($id) {
887
		$albumId = self::ripIdPrefix($id); // get rid of 'album-' prefix
888
889
		$album = $this->albumBusinessLayer->find($albumId, $this->userId);
890
		$albumName = $album->getNameString($this->l10n);
891
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $this->userId);
892
893
		return $this->subsonicResponse([
894
			'directory' => [
895
				'id' => $id,
896
				'parent' => 'artist-' . $album->getAlbumArtistId(),
897
				'name' => $albumName,
898
				'child' => \array_map(function($track) use ($album) {
899
					$track->setAlbum($album);
900
					return $this->trackToApi($track);
901
				}, $tracks)
902
			]
903
		]);
904
	}
905
906
	/**
907
	 * @param Folder $folder
908
	 * @return array
909
	 */
910
	private function folderToApi($folder) {
911
		return [
912
			'id' => 'folder-' . $folder->getId(),
913
			'title' => $folder->getName(),
914
			'isDir' => true
915
		];
916
	}
917
918
	/**
919
	 * @param Artist $artist
920
	 * @return array
921
	 */
922
	private function artistToApi($artist) {
923
		$result = [
924
			'name' => $artist->getNameString($this->l10n),
925
			'id' => 'artist-' . $artist->getId(),
926
			'albumCount' => $this->albumBusinessLayer->countByArtist($artist->getId())
927
		];
928
929
		if ($artist->getStarred() != null) {
930
			$result['starred'] = $this->formatDateTime($artist->getStarred());
931
		}
932
933
		return $result;
934
	}
935
936
	/**
937
	 * The "old API" format is used e.g. in getMusicDirectory and getAlbumList
938
	 * @param Album $album
939
	 * @param string|null $artistName
940
	 * @return array
941
	 */
942
	private function albumToOldApi($album, $artistName = null) {
943
		$result = $this->albumCommonApiFields($album, $artistName);
944
945
		$result['parent'] = 'artist-' . $album->getAlbumArtistId();
946
		$result['title'] = $album->getNameString($this->l10n);
947
		$result['isDir'] = true;
948
949
		return $result;
950
	}
951
952
	/**
953
	 * The "new API" format is used e.g. in getAlbum and getAlbumList2
954
	 * @param Album $album
955
	 * @param string|null $artistName
956
	 * @return array
957
	 */
958
	private function albumToNewApi($album, $artistName = null) {
959
		$result = $this->albumCommonApiFields($album, $artistName);
960
961
		$result['artistId'] = 'artist-' . $album->getAlbumArtistId();
962
		$result['name'] = $album->getNameString($this->l10n);
963
		$result['songCount'] = $this->trackBusinessLayer->countByAlbum($album->getId());
964
		$result['duration'] = $this->trackBusinessLayer->totalDurationOfAlbum($album->getId());
965
966
		return $result;
967
	}
968
969
	private function albumCommonApiFields($album, $artistName = null) {
970
		if (empty($artistName)) {
971
			$artist = $this->artistBusinessLayer->find($album->getAlbumArtistId(), $this->userId);
972
			$artistName = $artist->getNameString($this->l10n);
973
		}
974
975
		$result = [
976
			'id' => 'album-' . $album->getId(),
977
			'artist' => $artistName
978
		];
979
980
		if (!empty($album->getCoverFileId())) {
981
			$result['coverArt'] = $album->getId();
982
		}
983
984
		if ($album->getStarred() != null) {
985
			$result['starred'] = $this->formatDateTime($album->getStarred());
986
		}
987
988
		if (!empty($album->getGenres())) {
989
			$result['genre'] = \implode(', ', \array_map(function($genreId) {
990
				return $this->genreBusinessLayer->find($genreId, $this->userId)->getNameString($this->l10n);
991
			}, $album->getGenres()));
992
		}
993
994
		if (!empty($album->getYears())) {
995
			$result['year'] = $album->yearToAPI();
996
		}
997
998
		return $result;
999
	}
1000
1001
	/**
1002
	 * The same API format is used both on "old" and "new" API methods. The "new" API adds some
1003
	 * new fields for the songs, but providing some extra fields shouldn't be a problem for the
1004
	 * older clients. 
1005
	 * @param Track $track If the track entity has no album, artist and/or genre references set, then
1006
	 *                     those are automatically fetched from the respective BusinessLayer modules.
1007
	 * @return array
1008
	 */
1009
	private function trackToApi($track) {
1010
		$albumId = $track->getAlbumId();
1011
1012
		$album = $track->getAlbum();
1013
		if (empty($album)) {
1014
			$album = $this->albumBusinessLayer->find($albumId, $this->userId);
1015
			$track->setAlbum($album);
1016
		}
1017
1018
		$artist = $track->getArtist();
1019
		if (empty($artist)) {
1020
			$artist = $this->artistBusinessLayer->find($track->getArtistId(), $this->userId);
1021
			$track->setArtist($artist);
1022
		}
1023
1024
		$result = [
1025
			'id' => 'track-' . $track->getId(),
1026
			'parent' => 'album-' . $albumId,
1027
			//'discNumber' => $track->getDisk(), // not supported on any of the tested clients => adjust track number instead
1028
			'title' => $track->getTitle(),
1029
			'artist' => $artist->getNameString($this->l10n),
1030
			'isDir' => false,
1031
			'album' => $album->getNameString($this->l10n),
1032
			'year' => $track->getYear(),
1033
			'size' => $track->getSize(),
1034
			'contentType' => $track->getMimetype(),
1035
			'suffix' => $track->getFileExtension(),
1036
			'duration' => $track->getLength() ?: 0,
1037
			'bitRate' => \round($track->getBitrate()/1000) ?: 0, // convert bps to kbps
1038
			//'path' => '',
1039
			'isVideo' => false,
1040
			'albumId' => 'album-' . $albumId,
1041
			'artistId' => 'artist-' . $track->getArtistId(),
1042
			'type' => 'music'
1043
		];
1044
1045
		if (!empty($album->getCoverFileId())) {
1046
			$result['coverArt'] = $album->getId();
1047
		}
1048
1049
		$trackNumber = $track->getAdjustedTrackNumber();
1050
		if ($trackNumber !== null) {
1051
			$result['track'] = $trackNumber;
1052
		}
1053
1054
		if ($track->getStarred() != null) {
1055
			$result['starred'] = $this->formatDateTime($track->getStarred());
1056
		}
1057
1058
		if (!empty($track->getGenreId())) {
1059
			$genre = $track->getGenre() ?: $this->genreBusinessLayer->find($track->getGenreId(), $this->userId);
1060
			$result['genre'] = $genre->getNameString($this->l10n);
1061
		}
1062
1063
		return $result;
1064
	}
1065
1066
	/**
1067
	 * @param Playlist $playlist
1068
	 * @return array
1069
	 */
1070
	private function playlistToApi($playlist) {
1071
		return [
1072
			'id' => $playlist->getId(),
1073
			'name' => $playlist->getName(),
1074
			'owner' => $this->userId,
1075
			'public' => false,
1076
			'songCount' => $playlist->getTrackCount(),
1077
			'duration' => $this->playlistBusinessLayer->getDuration($playlist->getId(), $this->userId),
1078
			'comment' => $playlist->getComment() ?: '',
1079
			'created' => $this->formatDateTime($playlist->getCreated()),
1080
			//'coverArt' => '' // added in API 1.11.0 but is optional even there
1081
		];
1082
	}
1083
1084
	/**
1085
	 * Common logic for getAlbumList and getAlbumList2
1086
	 * @return Album[]
1087
	 */
1088
	private function albumsForGetAlbumList() {
1089
		$type = $this->getRequiredParam('type');
1090
		$size = $this->request->getParam('size', 10);
1091
		$size = \min($size, 500); // the API spec limits the maximum amount to 500
1092
		$offset = $this->request->getParam('offset', 0);
1093
1094
		$albums = [];
1095
1096
		switch ($type) {
1097
			case 'random':
1098
				$allAlbums = $this->albumBusinessLayer->findAll($this->userId);
1099
				$indices = $this->random->getIndices(\count($allAlbums), $offset, $size, $this->userId, 'subsonic_albums');
1100
				$albums = Util::arrayMultiGet($allAlbums, $indices);
1101
				break;
1102
			case 'starred':
1103
				$albums = $this->albumBusinessLayer->findAllStarred($this->userId, $size, $offset);
1104
				break;
1105
			case 'alphabeticalByName':
1106
				$albums = $this->albumBusinessLayer->findAll($this->userId, SortBy::Name, $size, $offset);
1107
				break;
1108
			case 'alphabeticalByArtist':
1109
				$albums = $this->albumBusinessLayer->findAll($this->userId, SortBy::Parent, $size, $offset);
1110
				break;
1111
			case 'byGenre':
1112
				$genre = $this->getRequiredParam('genre');
1113
				$albums = $this->findAlbumsByGenre($genre, $size, $offset);
1114
				break;
1115
			case 'byYear':
1116
				$fromYear = $this->getRequiredParam('fromYear');
1117
				$toYear = $this->getRequiredParam('toYear');
1118
				$albums = $this->albumBusinessLayer->findAllByYearRange($fromYear, $toYear, $this->userId, $size, $offset);
1119
				break;
1120
			case 'newest':
1121
			case 'highest':
1122
			case 'frequent':
1123
			case 'recent':
1124
			default:
1125
				$this->logger->log("Album list type '$type' is not supported", 'debug');
1126
				break;
1127
		}
1128
1129
		return $albums;
1130
	}
1131
1132
	/**
1133
	 * Common logic for search2 and search3
1134
	 * @return array with keys 'artists', 'albums', and 'tracks'
1135
	 */
1136
	private function doSearch() {
1137
		$query = $this->getRequiredParam('query');
1138
		$artistCount = $this->request->getParam('artistCount', 20);
1139
		$artistOffset = $this->request->getParam('artistOffset', 0);
1140
		$albumCount = $this->request->getParam('albumCount', 20);
1141
		$albumOffset = $this->request->getParam('albumOffset', 0);
1142
		$songCount = $this->request->getParam('songCount', 20);
1143
		$songOffset = $this->request->getParam('songOffset', 0);
1144
1145
		if (empty($query)) {
1146
			throw new SubsonicException("The 'query' argument is mandatory", 10);
1147
		}
1148
1149
		return [
1150
			'artists' => $this->artistBusinessLayer->findAllByName($query, $this->userId, true, $artistCount, $artistOffset),
1151
			'albums' => $this->albumBusinessLayer->findAllByName($query, $this->userId, true, $albumCount, $albumOffset),
1152
			'tracks' => $this->trackBusinessLayer->findAllByName($query, $this->userId, true, $songCount, $songOffset)
1153
		];
1154
	}
1155
1156
	/**
1157
	 * Common logic for getStarred and getStarred2
1158
	 * @return array
1159
	 */
1160
	private function doGetStarred() {
1161
		return [
1162
			'artists' => $this->artistBusinessLayer->findAllStarred($this->userId),
1163
			'albums' => $this->albumBusinessLayer->findAllStarred($this->userId),
1164
			'tracks' => $this->trackBusinessLayer->findAllStarred($this->userId)
1165
		];
1166
	}
1167
1168
	/**
1169
	 * Common response building logic for search2, search3, getStarred, and getStarred2
1170
	 * @param string $title Name of the main node in the response message
1171
	 * @param array $results Search results with keys 'artists', 'albums', and 'tracks'
1172
	 * @param boolean $useNewApi Set to true for search3 and getStarred3. There is a difference
1173
	 *                           in the formatting of the album nodes.
1174
	 * @return \OCP\AppFramework\Http\Response
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\Response 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...
1175
	 */
1176
	private function searchResponse($title, $results, $useNewApi) {
1177
		$albumMapFunc = $useNewApi ? 'albumToNewApi' : 'albumToOldApi';
1178
1179
		return $this->subsonicResponse([$title => [
1180
			'artist' => \array_map([$this, 'artistToApi'], $results['artists']),
1181
			'album' => \array_map([$this, $albumMapFunc], $results['albums']),
1182
			'song' => \array_map([$this, 'trackToApi'], $results['tracks'])
1183
		]]);
1184
	}
1185
1186
	/**
1187
	 * Find tracks by genre name
1188
	 * @param string $genreName
1189
	 * @param int|null $limit
1190
	 * @param int|null $offset
1191
	 * @return Track[]
1192
	 */
1193
	private function findTracksByGenre($genreName, $limit=null, $offset=null) {
1194
		$genre = $this->findGenreByName($genreName);
1195
1196
		if ($genre) {
1197
			return $this->trackBusinessLayer->findAllByGenre($genre->getId(), $this->userId, $limit, $offset);
1198
		} else {
1199
			return [];
1200
		}
1201
	}
1202
1203
	/**
1204
	 * Find albums by genre name
1205
	 * @param string $genreName
1206
	 * @param int|null $limit
1207
	 * @param int|null $offset
1208
	 * @return Album[]
1209
	 */
1210
	private function findAlbumsByGenre($genreName, $limit=null, $offset=null) {
1211
		$genre = $this->findGenreByName($genreName);
1212
1213
		if ($genre) {
1214
			return $this->albumBusinessLayer->findAllByGenre($genre->getId(), $this->userId, $limit, $offset);
1215
		} else {
1216
			return [];
1217
		}
1218
	}
1219
1220
	private function findGenreByName($name) {
1221
		$genreArr = $this->genreBusinessLayer->findAllByName($name, $this->userId);
1222
		if (\count($genreArr) == 0 && $name == Genre::unknownGenreName($this->l10n)) {
1223
			$genreArr = $this->genreBusinessLayer->findAllByName('', $this->userId);
1224
		}
1225
		return \count($genreArr) ? $genreArr[0] : null;
1226
	}
1227
1228
	/**
1229
	 * Given a prefixed ID like 'artist-123' or 'track-45', return just the numeric part.
1230
	 * @param string $id
1231
	 * @return integer
1232
	 */
1233
	private static function ripIdPrefix($id) {
1234
		return (int)(\explode('-', $id)[1]);
1235
	}
1236
1237
	private function formatDateTime($dateString) {
1238
		$dateTime = new \DateTime($dateString);
1239
		$dateTime->setTimezone($this->timezone);
1240
		return $dateTime->format('Y-m-d\TH:i:s');
1241
	}
1242
1243
	private function subsonicResponse($content, $status = 'ok') {
1244
		$content['status'] = $status; 
1245
		$content['version'] = self::API_VERSION;
1246
		$responseData = ['subsonic-response' => $content];
1247
1248
		if ($this->format == 'json') {
1249
			$response = new JSONResponse($responseData);
1250
		} else if ($this->format == 'jsonp') {
1251
			$responseData = \json_encode($responseData);
1252
			$response = new DataDisplayResponse("{$this->callback}($responseData);");
1253
			$response->addHeader('Content-Type', 'text/javascript; charset=UTF-8');
1254
		} else {
1255
			$response = new XMLResponse($responseData);
1256
		}
1257
1258
		return $response;
1259
	}
1260
1261
	public function subsonicErrorResponse($errorCode, $errorMessage) {
1262
		return $this->subsonicResponse([
1263
				'error' => [
1264
					'code' => $errorCode,
1265
					'message' => $errorMessage
1266
				]
1267
			], 'failed');
1268
	}
1269
}
1270