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

613
		$this->trackBusinessLayer->setStarred(/** @scrutinizer ignore-type */ $targetIds['tracks'], $this->userId);
Loading history...
614
		$this->albumBusinessLayer->setStarred($targetIds['albums'], $this->userId);
615
		$this->artistBusinessLayer->setStarred($targetIds['artists'], $this->userId);
616
617
		return $this->subsonicResponse([]);
618
	}
619
620
	/**
621
	 * @SubsonicAPI
622
	 */
623
	private function unstar() {
624
		$targetIds = $this->getStarringParameters();
625
626
		$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

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