Passed
Push — master ( 1bf36a...3f521f )
by Pauli
02:26
created

AmpacheController::playlist_songs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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