Passed
Push — master ( b03450...fdfc67 )
by Pauli
01:52
created

AmpacheController::getAllTracks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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