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

SubsonicController::getPodcasts()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
c 1
b 0
f 0
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