Passed
Push — master ( d79fc9...d41de6 )
by Pauli
02:07
created

AmpacheController::xmlApi()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 8
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10

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 Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2017 - 2020
13
 */
14
15
namespace OCA\Music\Controller;
16
17
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...
18
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...
19
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...
20
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...
21
22
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
23
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
24
use \OCA\Music\AppFramework\Core\Logger;
25
use \OCA\Music\Middleware\AmpacheException;
26
27
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
28
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
29
use \OCA\Music\BusinessLayer\GenreBusinessLayer;
30
use \OCA\Music\BusinessLayer\Library;
31
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
32
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
33
34
use \OCA\Music\Db\Album;
35
use \OCA\Music\Db\AmpacheUserMapper;
36
use \OCA\Music\Db\AmpacheSession;
37
use \OCA\Music\Db\AmpacheSessionMapper;
38
use \OCA\Music\Db\Artist;
39
use \OCA\Music\Db\SortBy;
40
41
use \OCA\Music\Http\ErrorResponse;
42
use \OCA\Music\Http\FileResponse;
43
use \OCA\Music\Http\XMLResponse;
44
45
use \OCA\Music\Utility\AmpacheUser;
46
use \OCA\Music\Utility\CoverHelper;
47
use \OCA\Music\Utility\Util;
48
49
class AmpacheController extends Controller {
50
	private $ampacheUserMapper;
51
	private $ampacheSessionMapper;
52
	private $albumBusinessLayer;
53
	private $artistBusinessLayer;
54
	private $genreBusinessLayer;
55
	private $playlistBusinessLayer;
56
	private $trackBusinessLayer;
57
	private $library;
58
	private $ampacheUser;
59
	private $urlGenerator;
60
	private $rootFolder;
61
	private $l10n;
62
	private $coverHelper;
63
	private $logger;
64
	private $jsonMode;
65
66
	const SESSION_EXPIRY_TIME = 6000;
67
	const ALL_TRACKS_PLAYLIST_ID = 10000000;
68
	const API_VERSION = 350001;
69
70
	public function __construct($appname,
71
								IRequest $request,
72
								$l10n,
73
								IURLGenerator $urlGenerator,
74
								AmpacheUserMapper $ampacheUserMapper,
75
								AmpacheSessionMapper $ampacheSessionMapper,
76
								AlbumBusinessLayer $albumBusinessLayer,
77
								ArtistBusinessLayer $artistBusinessLayer,
78
								GenreBusinessLayer $genreBusinessLayer,
79
								PlaylistBusinessLayer $playlistBusinessLayer,
80
								TrackBusinessLayer $trackBusinessLayer,
81
								Library $library,
82
								AmpacheUser $ampacheUser,
83
								$rootFolder,
84
								CoverHelper $coverHelper,
85
								Logger $logger) {
86
		parent::__construct($appname, $request);
87
88
		$this->ampacheUserMapper = $ampacheUserMapper;
89
		$this->ampacheSessionMapper = $ampacheSessionMapper;
90
		$this->albumBusinessLayer = $albumBusinessLayer;
91
		$this->artistBusinessLayer = $artistBusinessLayer;
92
		$this->genreBusinessLayer = $genreBusinessLayer;
93
		$this->playlistBusinessLayer = $playlistBusinessLayer;
94
		$this->trackBusinessLayer = $trackBusinessLayer;
95
		$this->library = $library;
96
		$this->urlGenerator = $urlGenerator;
97
		$this->l10n = $l10n;
98
99
		// used to share user info with middleware
100
		$this->ampacheUser = $ampacheUser;
101
102
		// used to deliver actual media file
103
		$this->rootFolder = $rootFolder;
104
105
		$this->coverHelper = $coverHelper;
106
		$this->logger = $logger;
107
	}
108
109
	public function setJsonMode($useJsonMode) {
110
		$this->jsonMode = $useJsonMode;
111
	}
112
113
	public function ampacheResponse($content) {
114
		if ($this->jsonMode) {
115
			return new JSONResponse(self::prepareResultForJsonApi($content));
116
		} else {
117
			return new XMLResponse(['root' => $content], ['id', 'count', 'code']);
118
		}
119
	}
120
121
	/**
122
	 * @NoAdminRequired
123
	 * @PublicPage
124
	 * @NoCSRFRequired
125
	 */
126
	public function xmlApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset) {
127
		// differentation between xmlApi and jsonApi is made already by the middleware
128
		return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset);
129
	}
130
131
	/**
132
	 * @NoAdminRequired
133
	 * @PublicPage
134
	 * @NoCSRFRequired
135
	 */
136
	public function jsonApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset) {
137
		// differentation between xmlApi and jsonApi is made already by the middleware
138
		return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset);
139
	}
140
141
	protected function dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset) {
142
		$this->logger->log("Ampache action '$action' requested", 'debug');
143
144
		$limit = self::validateLimitOrOffset($limit);
145
		$offset = self::validateLimitOrOffset($offset);
146
147
		switch ($action) {
148
			case 'handshake':
149
				return $this->handshake($user, $timestamp, $auth);
150
			case 'ping':
151
				return $this->ping($auth);
152
			case 'artists':
153
				return $this->artists($filter, $exact, $limit, $offset, $auth);
154
			case 'artist':
155
				return $this->artist($filter, $auth);
156
			case 'artist_albums':
157
				return $this->artist_albums($filter, $auth);
158
			case 'album_songs':
159
				return $this->album_songs($filter, $auth);
160
			case 'albums':
161
				return $this->albums($filter, $exact, $limit, $offset, $auth);
162
			case 'album':
163
				return $this->album($filter, $auth);
164
			case 'artist_songs':
165
				return $this->artist_songs($filter, $auth);
166
			case 'songs':
167
				return $this->songs($filter, $exact, $limit, $offset, $auth);
168
			case 'song':
169
				return $this->song($filter, $auth);
170
			case 'search_songs':
171
				return $this->search_songs($filter, $auth);
172
			case 'playlists':
173
				return $this->playlists($filter, $exact, $limit, $offset);
174
			case 'playlist':
175
				return $this->playlist($filter);
176
			case 'playlist_songs':
177
				return $this->playlist_songs($filter, $limit, $offset, $auth);
178
			case 'tags':
179
				return $this->tags($filter, $exact, $limit, $offset);
180
			case 'tag':
181
				return $this->tag($filter);
182
			case 'tag_artists':
183
				return $this->tag_artists($filter, $limit, $offset, $auth);
184
			case 'tag_albums':
185
				return $this->tag_albums($filter, $limit, $offset, $auth);
186
			case 'tag_songs':
187
				return $this->tag_songs($filter, $limit, $offset, $auth);
188
189
			# non Ampache API actions
190
			case '_play':
191
				return $this->_play($filter);
192
			case '_get_album_cover':
193
				return $this->_get_album_cover($filter);
194
			case '_get_artist_cover':
195
				return $this->_get_artist_cover($filter);
196
		}
197
198
		$this->logger->log("Unsupported Ampache action '$action' requested", 'warn');
199
		throw new AmpacheException('Action not supported', 405);
200
	}
201
202
	/***********************
203
	 * Ampahce API methods *
204
	 ***********************/
205
206
	protected function handshake($user, $timestamp, $auth) {
207
		$currentTime = \time();
208
		$expiryDate = $currentTime + self::SESSION_EXPIRY_TIME;
209
210
		$this->checkHandshakeTimestamp($timestamp, $currentTime);
211
		$this->checkHandshakeAuthentication($user, $timestamp, $auth);
212
		$token = $this->startNewSession($user, $expiryDate);
213
214
		$currentTimeFormated = \date('c', $currentTime);
215
		$expiryDateFormated = \date('c', $expiryDate);
216
217
		return $this->ampacheResponse([
218
			'auth' => $token,
219
			'version' => self::API_VERSION,
220
			'update' => $currentTimeFormated,
221
			'add' => $currentTimeFormated,
222
			'clean' => $currentTimeFormated,
223
			'songs' => $this->trackBusinessLayer->count($user),
224
			'artists' => $this->artistBusinessLayer->count($user),
225
			'albums' => $this->albumBusinessLayer->count($user),
226
			'playlists' => $this->playlistBusinessLayer->count($user) + 1, // +1 for "All tracks"
227
			'session_expire' => $expiryDateFormated,
228
			'tags' => $this->genreBusinessLayer->count($user),
229
			'videos' => 0
230
		]);
231
	}
232
233
	protected function ping($auth) {
234
		if ($auth !== null && $auth !== '') {
235
			$this->ampacheSessionMapper->extend($auth, \time() + self::SESSION_EXPIRY_TIME);
236
		}
237
238
		return $this->ampacheResponse([
239
			'version' => self::API_VERSION
240
		]);
241
	}
242
243
	protected function artists($filter, $exact, $limit, $offset, $auth) {
244
		$artists = $this->findEntities($this->artistBusinessLayer, $filter, $exact, $limit, $offset);
245
		return $this->renderArtists($artists, $auth);
246
	}
247
248
	protected function artist($artistId, $auth) {
249
		$userId = $this->ampacheUser->getUserId();
250
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
251
		return $this->renderArtists([$artist], $auth);
252
	}
253
254
	protected function artist_albums($artistId, $auth) {
255
		$userId = $this->ampacheUser->getUserId();
256
		$albums = $this->albumBusinessLayer->findAllByArtist($artistId, $userId);
257
		return $this->renderAlbums($albums, $auth);
258
	}
259
260
	protected function artist_songs($artistId, $auth) {
261
		$userId = $this->ampacheUser->getUserId();
262
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
263
		$tracks = $this->trackBusinessLayer->findAllByArtist($artistId, $userId);
264
		$this->injectArtistAndAlbum($tracks, $artist);
265
		return $this->renderSongs($tracks, $auth);
266
	}
267
268
	protected function album_songs($albumId, $auth) {
269
		$userId = $this->ampacheUser->getUserId();
270
271
		$album = $this->albumBusinessLayer->find($albumId, $userId);
272
		$album->setAlbumArtist($this->artistBusinessLayer->find($album->getAlbumArtistId(), $userId));
273
274
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $userId);
275
		$this->injectArtistAndAlbum($tracks, null, $album);
276
277
		return $this->renderSongs($tracks, $auth);
278
	}
279
280
	protected function song($trackId, $auth) {
281
		$userId = $this->ampacheUser->getUserId();
282
		$track = $this->trackBusinessLayer->find($trackId, $userId);
283
		$trackInArray = [$track];
284
		$this->injectArtistAndAlbum($trackInArray);
285
		return $this->renderSongs($trackInArray, $auth);
286
	}
287
288
	protected function songs($filter, $exact, $limit, $offset, $auth) {
289
290
		// optimized handling for fetching the whole library
291
		// note: the ordering of the songs differs between these two cases
292
		if (empty($filter) && !$limit && !$offset) {
293
			$tracks = $this->getAllTracks();
294
		}
295
		// general case
296
		else {
297
			$tracks = $this->findEntities($this->trackBusinessLayer, $filter, $exact, $limit, $offset);
298
			$this->injectArtistAndAlbum($tracks);
299
		}
300
301
		return $this->renderSongs($tracks, $auth);
302
	}
303
304
	protected function search_songs($filter, $auth) {
305
		$userId = $this->ampacheUser->getUserId();
306
		$tracks = $this->trackBusinessLayer->findAllByNameRecursive($filter, $userId);
307
		$this->injectArtistAndAlbum($tracks);
308
		return $this->renderSongs($tracks, $auth);
309
	}
310
311
	protected function albums($filter, $exact, $limit, $offset, $auth) {
312
		$albums = $this->findEntities($this->albumBusinessLayer, $filter, $exact, $limit, $offset);
313
		return $this->renderAlbums($albums, $auth);
314
	}
315
316
	protected function album($albumId, $auth) {
317
		$userId = $this->ampacheUser->getUserId();
318
		$album = $this->albumBusinessLayer->find($albumId, $userId);
319
		return $this->renderAlbums([$album], $auth);
320
	}
321
322
	protected function playlists($filter, $exact, $limit, $offset) {
323
		$userId = $this->ampacheUser->getUserId();
324
		$playlists = $this->findEntities($this->playlistBusinessLayer, $filter, $exact, $limit, $offset);
325
326
		// append "All tracks" if not searching by name, and it is not off-limit
327
		if (empty($filter) && ($limit === null || \count($playlists) < $limit)) {
328
			$playlists[] = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n);
329
		}
330
331
		return $this->renderPlaylists($playlists);
332
	}
333
334
	protected function playlist($listId) {
335
		$userId = $this->ampacheUser->getUserId();
336
		if ($listId == self::ALL_TRACKS_PLAYLIST_ID) {
337
			$playlist = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n);
338
		} else {
339
			$playlist = $this->playlistBusinessLayer->find($listId, $userId);
340
		}
341
		return $this->renderPlaylists([$playlist]);
342
	}
343
344
	protected function playlist_songs($listId, $limit, $offset, $auth) {
345
		if ($listId == self::ALL_TRACKS_PLAYLIST_ID) {
346
			$playlistTracks = $this->getAllTracks();
347
			$playlistTracks = \array_slice($playlistTracks, $offset, $limit);
348
		}
349
		else {
350
			$userId = $this->ampacheUser->getUserId();
351
			$playlistTracks = $this->playlistBusinessLayer->getPlaylistTracks($listId, $userId, $limit, $offset);
352
			$this->injectArtistAndAlbum($playlistTracks);
353
		}
354
		return $this->renderSongs($playlistTracks, $auth);
355
	}
356
357
	protected function tags($filter, $exact, $limit, $offset) {
358
		$userId = $this->ampacheUser->getUserId();
359
		// TODO: $filter, $exact
360
		$genres = $this->genreBusinessLayer->findAllWithCounts($userId, $limit, $offset);
361
		return $this->renderTags($genres);
362
	}
363
364
	protected function tag($tagId) {
365
		$userId = $this->ampacheUser->getUserId();
366
		$genre = $this->genreBusinessLayer->find($tagId, $userId);
367
		return $this->renderTags([$genre]);
368
	}
369
370
	protected function tag_artists($genreId, $limit, $offset, $auth) {
371
		$userId = $this->ampacheUser->getUserId();
372
		$artists = $this->artistBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
373
		return $this->renderArtists($artists, $auth);
374
	}
375
376
	protected function tag_albums($genreId, $limit, $offset, $auth) {
377
		$userId = $this->ampacheUser->getUserId();
378
		$albums = $this->albumBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
379
		return $this->renderAlbums($albums, $auth);
380
	}
381
382
	protected function tag_songs($genreId, $limit, $offset, $auth) {
383
		$userId = $this->ampacheUser->getUserId();
384
		$tracks = $this->trackBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
385
		$this->injectArtistAndAlbum($tracks, $artist);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $artist seems to be never defined.
Loading history...
386
		return $this->renderSongs($tracks, $auth);
387
	}
388
389
	/***************************************************************
390
	 * API methods which are not part of the Ampache specification *
391
	 ***************************************************************/
392
	protected function _play($trackId) {
393
		$userId = $this->ampacheUser->getUserId();
394
395
		try {
396
			$track = $this->trackBusinessLayer->find($trackId, $userId);
397
		} catch (BusinessLayerException $e) {
398
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $e->getMessage());
0 ignored issues
show
Bug introduced by
The type OCA\Music\Controller\Http 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...
399
		}
400
401
		$files = $this->rootFolder->getUserFolder($userId)->getById($track->getFileId());
402
403
		if (\count($files) === 1) {
404
			return new FileResponse($files[0]);
405
		} else {
406
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
407
		}
408
	}
409
410
	protected function _get_album_cover($albumId) {
411
		return $this->getCover($albumId, $this->albumBusinessLayer);
412
	}
413
414
	protected function _get_artist_cover($artistId) {
415
		return $this->getCover($artistId, $this->artistBusinessLayer);
416
	}
417
418
419
	/********************
420
	 * Helper functions *
421
	 ********************/
422
423
	private function getCover($entityId, BusinessLayer $businessLayer) {
424
		$userId = $this->ampacheUser->getUserId();
425
		$userFolder = $this->rootFolder->getUserFolder($userId);
426
		$entity = $businessLayer->find($entityId, $userId);
427
428
		try {
429
			$coverData = $this->coverHelper->getCover($entity, $userId, $userFolder);
430
			if ($coverData !== null) {
431
				return new FileResponse($coverData);
432
			}
433
		} catch (BusinessLayerException $e) {
434
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity not found');
435
		}
436
437
		return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity has no cover');
438
	}
439
440
	private function checkHandshakeTimestamp($timestamp, $currentTime) {
441
		$providedTime = \intval($timestamp);
442
443
		if ($providedTime === 0) {
444
			throw new AmpacheException('Invalid Login - cannot parse time', 401);
445
		}
446
		if ($providedTime < ($currentTime - self::SESSION_EXPIRY_TIME)) {
447
			throw new AmpacheException('Invalid Login - session is outdated', 401);
448
		}
449
		// Allow the timestamp to be at maximum 10 minutes in the future. The client may use its
450
		// own system clock to generate the timestamp and that may differ from the server's time.
451
		if ($providedTime > $currentTime + 600) {
452
			throw new AmpacheException('Invalid Login - timestamp is in future', 401);
453
		}
454
	}
455
456
	private function checkHandshakeAuthentication($user, $timestamp, $auth) {
457
		$hashes = $this->ampacheUserMapper->getPasswordHashes($user);
458
459
		foreach ($hashes as $hash) {
460
			$expectedHash = \hash('sha256', $timestamp . $hash);
461
462
			if ($expectedHash === $auth) {
463
				return;
464
			}
465
		}
466
467
		throw new AmpacheException('Invalid Login - passphrase does not match', 401);
468
	}
469
470
	private function startNewSession($user, $expiryDate) {
471
		// this can cause collision, but it's just a temporary token
472
		$token = \md5(\uniqid(\rand(), true));
473
474
		// create new session
475
		$session = new AmpacheSession();
476
		$session->setUserId($user);
477
		$session->setToken($token);
478
		$session->setExpiry($expiryDate);
479
480
		// save session
481
		$this->ampacheSessionMapper->insert($session);
482
483
		return $token;
484
	}
485
486
	private function findEntities(BusinessLayer $businessLayer, $filter, $exact, $limit=null, $offset=null) {
487
		$userId = $this->ampacheUser->getUserId();
488
489
		if ($filter) {
490
			$fuzzy = !((boolean) $exact);
491
			return $businessLayer->findAllByName($filter, $userId, $fuzzy, $limit, $offset);
492
		} else {
493
			return $businessLayer->findAll($userId, SortBy::Name, $limit, $offset);
494
		}
495
	}
496
497
	/**
498
	 * Getting all tracks with this helper is more efficient than with `findEntities`
499
	 * followed by `injectArtistAndAlbum`. This is because, under the hood, the albums
500
	 * and artists are fetched with a single DB query instead of fetching each separately.
501
	 * 
502
	 * The result set is ordered first by artist and then by song title.
503
	 */
504
	private function getAllTracks() {
505
		$userId = $this->ampacheUser->getUserId();
506
		$tracks = $this->library->getTracksAlbumsAndArtists($userId)['tracks'];
507
		\usort($tracks, ['\OCA\Music\Db\Track', 'compareArtistAndTitle']);
508
		foreach ($tracks as $index => &$track) {
509
			$track->setNumberOnPlaylist($index + 1);
510
		}
511
		return $tracks;
512
	}
513
514
	private function createAmpacheActionUrl($action, $filter, $auth) {
515
		$api = $this->jsonMode ? 'music.ampache.jsonApi' : 'music.ampache.xmlApi';
516
		return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute($api))
517
				. "?action=$action&filter=$filter&auth=$auth";
518
	}
519
520
	private function createCoverUrl($entity, $auth) {
521
		if ($entity instanceof Album) {
522
			$type = 'album';
523
		} elseif ($entity instanceof Artist) {
524
			$type = 'artist';
525
		} else {
526
			throw new AmpacheException('unexpeted entity type for cover image', 500);
527
		}
528
529
		if ($entity->getCoverFileId()) {
530
			return $this->createAmpacheActionUrl("_get_{$type}_cover", $entity->getId(), $auth);
531
		} else {
532
			return '';
533
		}
534
	}
535
536
	/**
537
	 * Any non-integer values and integer value 0 are converted to null to
538
	 * indicate "no limit" or "no offset".
539
	 * @param string $value
540
	 * @return integer|null
541
	 */
542
	private static function validateLimitOrOffset($value) {
543
		if (\ctype_digit(\strval($value)) && $value !== 0) {
544
			return \intval($value);
545
		} else {
546
			return null;
547
		}
548
	}
549
550
	private function renderArtists($artists, $auth) {
551
		$userId = $this->ampacheUser->getUserId();
552
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
553
554
		return $this->ampacheResponse([
555
			'artist' => \array_map(function($artist) use ($userId, $genreMap, $auth) {
556
				return [
557
					'id' => (string)$artist->getId(),
558
					'name' => $artist->getNameString($this->l10n),
559
					'albums' => $this->albumBusinessLayer->countByArtist($artist->getId()),
560
					'songs' => $this->trackBusinessLayer->countByArtist($artist->getId()),
561
					'art' => $this->createCoverUrl($artist, $auth),
562
					'rating' => 0,
563
					'preciserating' => 0,
564
					'tag' => \array_map(function($genreId) use ($genreMap) {
565
						return [
566
							'id' => (string)$genreId,
567
							'value' => $genreMap[$genreId]->getNameString($this->l10n),
568
							'count' => 1
569
						];
570
					}, $this->trackBusinessLayer->getGenresByArtistId($artist->getId(), $userId))
571
				];
572
			}, $artists)
573
		]);
574
	}
575
576
	private function renderAlbums($albums, $auth) {
577
		$userId = $this->ampacheUser->getUserId();
578
579
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
580
581
		return $this->ampacheResponse([
582
			'album' => \array_map(function($album) use ($userId, $auth, $genreMap) {
583
				$artist = $this->artistBusinessLayer->find($album->getAlbumArtistId(), $userId);
584
				return [
585
					'id' => (string)$album->getId(),
586
					'name' => $album->getNameString($this->l10n),
587
					'artist' => [
588
						'id' => (string)$artist->getId(),
589
						'value' => $artist->getNameString($this->l10n)
590
					],
591
					'tracks' => $this->trackBusinessLayer->countByAlbum($album->getId()),
592
					'rating' => 0,
593
					'year' => $album->yearToAPI(),
594
					'art' => $this->createCoverUrl($album, $auth),
595
					'preciserating' => 0,
596
					'tag' => \array_map(function($genreId) use ($genreMap) {
597
						return [
598
							'id' => (string)$genreId,
599
							'value' => $genreMap[$genreId]->getNameString($this->l10n),
600
							'count' => 1
601
						];
602
					}, $album->getGenres())
603
				];
604
			}, $albums)
605
		]);
606
	}
607
608
	private function injectArtistAndAlbum(&$tracks, $commonArtist=null, $commonAlbum=null) {
609
		$userId = $this->ampacheUser->getUserId();
610
611
		foreach ($tracks as &$track) {
612
			$artist = $commonArtist ?: $this->artistBusinessLayer->find($track->getArtistId(), $userId);
613
			$track->setArtist($artist);
614
615
			if (!empty($commonAlbum)) {
616
				$track->setAlbum($commonAlbum);
617
			} else {
618
				$album = $this->albumBusinessLayer->find($track->getAlbumId(), $userId);
619
				$album->setAlbumArtist($this->artistBusinessLayer->find($album->getAlbumArtistId(), $userId));
620
				$track->setAlbum($album);
621
			}
622
		}
623
	}
624
625
	private function renderSongs($tracks, $auth) {
626
		$userId = $this->ampacheUser->getUserId();
627
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
628
629
		return $this->ampacheResponse([
630
			'song' => \array_map(function($track) use ($auth, $genreMap) {
631
				$artist = $track->getArtist();
632
				$album = $track->getAlbum();
633
				$albumArtist = $album->getAlbumArtist();
634
635
				$result = [
636
					'id' => (string)$track->getId(),
637
					'title' => $track->getTitle(),
638
					'artist' => [
639
						'id' => (string)$artist->getId(),
640
						'value' => $artist->getNameString($this->l10n)
641
					],
642
					'albumartist' => [
643
						'id' => (string)$albumArtist->getId(),
644
						'value' => $albumArtist->getNameString($this->l10n)
645
					],
646
					'album' => [
647
						'id' => (string)$album->getId(),
648
						'value' => $album->getNameString($this->l10n)
649
					],
650
					'url' => $this->createAmpacheActionUrl('_play', $track->getId(), $auth),
651
					'time' => $track->getLength(),
652
					'year' => $track->getYear(),
653
					'track' => $track->getAdjustedTrackNumber(),
654
					'bitrate' => $track->getBitrate(),
655
					'mime' => $track->getMimetype(),
656
					'size' => $track->getSize(),
657
					'art' => $this->createCoverUrl($album, $auth),
658
					'rating' => 0,
659
					'preciserating' => 0,
660
				];
661
662
				$genreId = $track->getGenreId();
663
				if ($genreId !== null) {
664
					$result['tag'] = [[
665
						'id' => (string)$genreId,
666
						'value' => $genreMap[$genreId]->getNameString($this->l10n),
667
						'count' => 1
668
					]];
669
				}
670
				return $result;
671
			}, $tracks)
672
		]);
673
	}
674
675
	private function renderPlaylists($playlists) {
676
		return $this->ampacheResponse([
677
			'playlist' => \array_map(function($playlist) {
678
				return [
679
					'id' => (string)$playlist->getId(),
680
					'name' => $playlist->getName(),
681
					'owner' => $this->ampacheUser->getUserId(),
682
					'items' => $playlist->getTrackCount(),
683
					'type' => 'Private'
684
				];
685
			}, $playlists)
686
		]);
687
	}
688
689
	private function renderTags($genres) {
690
		return $this->ampacheResponse([
691
			'tag' => \array_map(function($genre) {
692
				return [
693
					'id' => (string)$genre->getId(),
694
					'name' => $genre->getNameString($this->l10n),
695
					'albums' => $genre->getAlbumCount(),
696
					'artists' => $genre->getArtistCount(),
697
					'songs' => $genre->getTrackCount(),
698
					'videos' => 0,
699
					'playlists' => 0,
700
					'stream' => 0
701
				];
702
			}, $genres)
703
		]);
704
	}
705
706
	/**
707
	 * The JSON API has some asymmetries with the JSON API. This function makes the needed
708
	 * translations for the result content before it is converted into JSON. 
709
	 * @param array $content
710
	 * @return array
711
	 */
712
	private static function prepareResultForJsonApi($content) {
713
		if (\array_key_exists('error', $content)) {
714
			$content = Util::convertArrayKeys($content, ['value' => 'message']);
715
		}
716
		else {
717
			// in all responses except the error response, the root node is anonymous
718
			$content = \array_pop($content);
719
			$content = Util::convertArrayKeys($content, ['value' => 'name']);
720
		}
721
		return $content;
722
	}
723
}
724
725
/**
726
 * Adapter class which acts like the Playlist class for the purpose of 
727
 * AmpacheController::renderPlaylists but contains all the track of the user. 
728
 */
729
class AmpacheController_AllTracksPlaylist {
730
731
	private $user;
732
	private $trackBusinessLayer;
733
	private $l10n;
734
735
	public function __construct($user, $trackBusinessLayer, $l10n) {
736
		$this->user = $user;
737
		$this->trackBusinessLayer = $trackBusinessLayer;
738
		$this->l10n = $l10n;
739
	}
740
741
	public function getId() {
742
		return AmpacheController::ALL_TRACKS_PLAYLIST_ID;
743
	}
744
745
	public function getName() {
746
		return $this->l10n->t('All tracks');
747
	}
748
749
	public function getTrackCount() {
750
		return $this->trackBusinessLayer->count($this->user);
751
	}
752
}
753