Passed
Push — master ( 0837cd...cb2cf5 )
by Pauli
02:00
created

SubsonicController::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 40
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 18
c 2
b 0
f 0
nc 1
nop 17
dl 0
loc 40
ccs 0
cts 19
cp 0
crap 6
rs 9.6666

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

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

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