Passed
Pull Request — master (#752)
by
unknown
02:07
created

SubsonicController::savePlayQueue()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 3
nop 0
dl 0
loc 16
ccs 0
cts 6
cp 0
crap 20
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2019, 2020
11
 */
12
13
namespace OCA\Music\Controller;
14
15
use \OCP\AppFramework\Controller;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Controller was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use \OCP\AppFramework\Http\DataDisplayResponse;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\DataDisplayResponse was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use \OCP\AppFramework\Http\JSONResponse;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\JSONResponse was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use \OCP\Files\File;
0 ignored issues
show
Bug introduced by
The type OCP\Files\File was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use \OCP\Files\Folder;
0 ignored issues
show
Bug introduced by
The type OCP\Files\Folder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use \OCP\IRequest;
0 ignored issues
show
Bug introduced by
The type OCP\IRequest was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use \OCP\IURLGenerator;
0 ignored issues
show
Bug introduced by
The type OCP\IURLGenerator was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
23
use \OCA\Music\AppFramework\Core\Logger;
24
use \OCA\Music\AppFramework\Utility\MethodAnnotationReader;
25
26
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
27
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
28
use \OCA\Music\BusinessLayer\GenreBusinessLayer;
29
use \OCA\Music\BusinessLayer\Library;
30
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
31
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
32
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