Passed
Push — master ( 7fe04d...543ba8 )
by Pauli
04:49
created

SubsonicController::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 38
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

635
		$this->trackBusinessLayer->setStarred(/** @scrutinizer ignore-type */ $targetIds['tracks'], $this->userId);
Loading history...
636
		$this->albumBusinessLayer->setStarred($targetIds['albums'], $this->userId);
637
		$this->artistBusinessLayer->setStarred($targetIds['artists'], $this->userId);
638
639
		return $this->subsonicResponse([]);
640
	}
641
642
	/**
643
	 * @SubsonicAPI
644
	 */
645
	private function unstar() {
646
		$targetIds = $this->getStarringParameters();
647
648
		$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

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