Passed
Push — master ( f0b4ab...0b391b )
by Pauli
04:06
created

AmpacheController::tag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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