Passed
Push — feature/playlist_improvements ( 2a690a...faf9ee )
by Pauli
14:31
created

SubsonicController::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 36
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 16
nc 1
nop 15
dl 0
loc 36
ccs 0
cts 17
cp 0
crap 6
rs 9.7333
c 2
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

603
		$this->trackBusinessLayer->setStarred(/** @scrutinizer ignore-type */ $targetIds['tracks'], $this->userId);
Loading history...
604
		$this->albumBusinessLayer->setStarred($targetIds['albums'], $this->userId);
605
		$this->artistBusinessLayer->setStarred($targetIds['artists'], $this->userId);
606
607
		return $this->subsonicResponse([]);
608
	}
609
610
	/**
611
	 * @SubsonicAPI
612
	 */
613
	private function unstar() {
614
		$targetIds = $this->getStarringParameters();
615
616
		$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

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