Passed
Push — master ( df34b5...42f34d )
by Pauli
01:50
created

AmpacheController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 37
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 15
nc 1
nop 16
dl 0
loc 37
ccs 0
cts 16
cp 0
crap 2
rs 9.7666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author 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, $id) {
127
		// differentation between xmlApi and jsonApi is made already by the middleware
128
		return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id);
129
	}
130
131
	/**
132
	 * @NoAdminRequired
133
	 * @PublicPage
134
	 * @NoCSRFRequired
135
	 */
136
	public function jsonApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id) {
137
		// differentation between xmlApi and jsonApi is made already by the middleware
138
		return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id);
139
	}
140
141
	protected function dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id) {
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
			case 'download':
189
				return $this->download($id); // args 'type' and 'format' not supported
190
			case 'stream':
191
				return $this->stream($id, $offset); // args 'type', 'bitrate', 'format', and 'length' not supported
192
193
			# non Ampache API actions
194
			case '_get_album_cover':
195
				return $this->_get_album_cover($id);
196
			case '_get_artist_cover':
197
				return $this->_get_artist_cover($id);
198
		}
199
200
		$this->logger->log("Unsupported Ampache action '$action' requested", 'warn');
201
		throw new AmpacheException('Action not supported', 405);
202
	}
203
204
	/***********************
205
	 * Ampahce API methods *
206
	 ***********************/
207
208
	protected function handshake($user, $timestamp, $auth) {
209
		$currentTime = \time();
210
		$expiryDate = $currentTime + self::SESSION_EXPIRY_TIME;
211
212
		$this->checkHandshakeTimestamp($timestamp, $currentTime);
213
		$this->checkHandshakeAuthentication($user, $timestamp, $auth);
214
		$token = $this->startNewSession($user, $expiryDate);
215
216
		$currentTimeFormated = \date('c', $currentTime);
217
		$expiryDateFormated = \date('c', $expiryDate);
218
219
		return $this->ampacheResponse([
220
			'auth' => $token,
221
			'version' => self::API_VERSION,
222
			'update' => $currentTimeFormated,
223
			'add' => $currentTimeFormated,
224
			'clean' => $currentTimeFormated,
225
			'songs' => $this->trackBusinessLayer->count($user),
226
			'artists' => $this->artistBusinessLayer->count($user),
227
			'albums' => $this->albumBusinessLayer->count($user),
228
			'playlists' => $this->playlistBusinessLayer->count($user) + 1, // +1 for "All tracks"
229
			'session_expire' => $expiryDateFormated,
230
			'tags' => $this->genreBusinessLayer->count($user),
231
			'videos' => 0
232
		]);
233
	}
234
235
	protected function ping($auth) {
236
		if ($auth !== null && $auth !== '') {
237
			$this->ampacheSessionMapper->extend($auth, \time() + self::SESSION_EXPIRY_TIME);
238
		}
239
240
		return $this->ampacheResponse([
241
			'version' => self::API_VERSION
242
		]);
243
	}
244
245
	protected function artists($filter, $exact, $limit, $offset, $auth) {
246
		$artists = $this->findEntities($this->artistBusinessLayer, $filter, $exact, $limit, $offset);
247
		return $this->renderArtists($artists, $auth);
248
	}
249
250
	protected function artist($artistId, $auth) {
251
		$userId = $this->ampacheUser->getUserId();
252
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
253
		return $this->renderArtists([$artist], $auth);
254
	}
255
256
	protected function artist_albums($artistId, $auth) {
257
		$userId = $this->ampacheUser->getUserId();
258
		$albums = $this->albumBusinessLayer->findAllByArtist($artistId, $userId);
259
		return $this->renderAlbums($albums, $auth);
260
	}
261
262
	protected function artist_songs($artistId, $auth) {
263
		$userId = $this->ampacheUser->getUserId();
264
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
265
		$tracks = $this->trackBusinessLayer->findAllByArtist($artistId, $userId);
266
		$this->injectArtistAndAlbum($tracks, $artist);
267
		return $this->renderSongs($tracks, $auth);
268
	}
269
270
	protected function album_songs($albumId, $auth) {
271
		$userId = $this->ampacheUser->getUserId();
272
273
		$album = $this->albumBusinessLayer->find($albumId, $userId);
274
		$album->setAlbumArtist($this->artistBusinessLayer->find($album->getAlbumArtistId(), $userId));
275
276
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $userId);
277
		$this->injectArtistAndAlbum($tracks, null, $album);
278
279
		return $this->renderSongs($tracks, $auth);
280
	}
281
282
	protected function song($trackId, $auth) {
283
		$userId = $this->ampacheUser->getUserId();
284
		$track = $this->trackBusinessLayer->find($trackId, $userId);
285
		$trackInArray = [$track];
286
		$this->injectArtistAndAlbum($trackInArray);
287
		return $this->renderSongs($trackInArray, $auth);
288
	}
289
290
	protected function songs($filter, $exact, $limit, $offset, $auth) {
291
292
		// optimized handling for fetching the whole library
293
		// note: the ordering of the songs differs between these two cases
294
		if (empty($filter) && !$limit && !$offset) {
295
			$tracks = $this->getAllTracks();
296
		}
297
		// general case
298
		else {
299
			$tracks = $this->findEntities($this->trackBusinessLayer, $filter, $exact, $limit, $offset);
300
			$this->injectArtistAndAlbum($tracks);
301
		}
302
303
		return $this->renderSongs($tracks, $auth);
304
	}
305
306
	protected function search_songs($filter, $auth) {
307
		$userId = $this->ampacheUser->getUserId();
308
		$tracks = $this->trackBusinessLayer->findAllByNameRecursive($filter, $userId);
309
		$this->injectArtistAndAlbum($tracks);
310
		return $this->renderSongs($tracks, $auth);
311
	}
312
313
	protected function albums($filter, $exact, $limit, $offset, $auth) {
314
		$albums = $this->findEntities($this->albumBusinessLayer, $filter, $exact, $limit, $offset);
315
		return $this->renderAlbums($albums, $auth);
316
	}
317
318
	protected function album($albumId, $auth) {
319
		$userId = $this->ampacheUser->getUserId();
320
		$album = $this->albumBusinessLayer->find($albumId, $userId);
321
		return $this->renderAlbums([$album], $auth);
322
	}
323
324
	protected function playlists($filter, $exact, $limit, $offset) {
325
		$userId = $this->ampacheUser->getUserId();
326
		$playlists = $this->findEntities($this->playlistBusinessLayer, $filter, $exact, $limit, $offset);
327
328
		// append "All tracks" if not searching by name, and it is not off-limit
329
		$allTracksIndex = $this->playlistBusinessLayer->count($userId);
330
		if (empty($filter) && self::indexIsWithinOffsetAndLimit($allTracksIndex, $offset, $limit)) {
331
			$playlists[] = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n);
332
		}
333
334
		return $this->renderPlaylists($playlists);
335
	}
336
337
	protected function playlist($listId) {
338
		$userId = $this->ampacheUser->getUserId();
339
		if ($listId == self::ALL_TRACKS_PLAYLIST_ID) {
340
			$playlist = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n);
341
		} else {
342
			$playlist = $this->playlistBusinessLayer->find($listId, $userId);
343
		}
344
		return $this->renderPlaylists([$playlist]);
345
	}
346
347
	protected function playlist_songs($listId, $limit, $offset, $auth) {
348
		if ($listId == self::ALL_TRACKS_PLAYLIST_ID) {
349
			$playlistTracks = $this->getAllTracks();
350
			$playlistTracks = \array_slice($playlistTracks, $offset, $limit);
351
		}
352
		else {
353
			$userId = $this->ampacheUser->getUserId();
354
			$playlistTracks = $this->playlistBusinessLayer->getPlaylistTracks($listId, $userId, $limit, $offset);
355
			$this->injectArtistAndAlbum($playlistTracks);
356
		}
357
		return $this->renderSongs($playlistTracks, $auth);
358
	}
359
360
	protected function tags($filter, $exact, $limit, $offset) {
361
		$genres = $this->findEntities($this->genreBusinessLayer, $filter, $exact, $limit, $offset);
362
		return $this->renderTags($genres);
363
	}
364
365
	protected function tag($tagId) {
366
		$userId = $this->ampacheUser->getUserId();
367
		$genre = $this->genreBusinessLayer->find($tagId, $userId);
368
		return $this->renderTags([$genre]);
369
	}
370
371
	protected function tag_artists($genreId, $limit, $offset, $auth) {
372
		$userId = $this->ampacheUser->getUserId();
373
		$artists = $this->artistBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
374
		return $this->renderArtists($artists, $auth);
375
	}
376
377
	protected function tag_albums($genreId, $limit, $offset, $auth) {
378
		$userId = $this->ampacheUser->getUserId();
379
		$albums = $this->albumBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
380
		return $this->renderAlbums($albums, $auth);
381
	}
382
383
	protected function tag_songs($genreId, $limit, $offset, $auth) {
384
		$userId = $this->ampacheUser->getUserId();
385
		$tracks = $this->trackBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
386
		$this->injectArtistAndAlbum($tracks, $artist);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $artist seems to be never defined.
Loading history...
387
		return $this->renderSongs($tracks, $auth);
388
	}
389
390
	protected function download($trackId) {
391
		$userId = $this->ampacheUser->getUserId();
392
393
		try {
394
			$track = $this->trackBusinessLayer->find($trackId, $userId);
395
		} catch (BusinessLayerException $e) {
396
			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...
397
		}
398
399
		$files = $this->rootFolder->getUserFolder($userId)->getById($track->getFileId());
400
401
		if (\count($files) === 1) {
402
			return new FileResponse($files[0]);
403
		} else {
404
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
405
		}
406
	}
407
408
	protected function stream($trackId, $offset) {
409
		// This is just a dummy implementation. We don't support transcoding or streaming
410
		// from a time offset.
411
		// All the other unsupported arguments are just ignored, but a request with an offset
412
		// is responded with an error. This is becuase the client would probably work in an
413
		// unexpected way if it thinks it's streaming from offset but actually it is streaming
414
		// from the beginning of the file. Returning an error gives the clien a chance to fallback
415
		// to other methods of seeking.
416
		if ($offset !== null) {
417
			throw new AmpacheException('Streaming with time offset is not supported', 405);
418
		}
419
420
		return $this->download($trackId);
421
	}
422
423
	/***************************************************************
424
	 * API methods which are not part of the Ampache specification *
425
	 ***************************************************************/
426
	protected function _get_album_cover($albumId) {
427
		return $this->getCover($albumId, $this->albumBusinessLayer);
428
	}
429
430
	protected function _get_artist_cover($artistId) {
431
		return $this->getCover($artistId, $this->artistBusinessLayer);
432
	}
433
434
435
	/********************
436
	 * Helper functions *
437
	 ********************/
438
439
	private function getCover($entityId, BusinessLayer $businessLayer) {
440
		$userId = $this->ampacheUser->getUserId();
441
		$userFolder = $this->rootFolder->getUserFolder($userId);
442
		$entity = $businessLayer->find($entityId, $userId);
443
444
		try {
445
			$coverData = $this->coverHelper->getCover($entity, $userId, $userFolder);
446
			if ($coverData !== null) {
447
				return new FileResponse($coverData);
448
			}
449
		} catch (BusinessLayerException $e) {
450
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity not found');
451
		}
452
453
		return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity has no cover');
454
	}
455
456
	private function checkHandshakeTimestamp($timestamp, $currentTime) {
457
		$providedTime = \intval($timestamp);
458
459
		if ($providedTime === 0) {
460
			throw new AmpacheException('Invalid Login - cannot parse time', 401);
461
		}
462
		if ($providedTime < ($currentTime - self::SESSION_EXPIRY_TIME)) {
463
			throw new AmpacheException('Invalid Login - session is outdated', 401);
464
		}
465
		// Allow the timestamp to be at maximum 10 minutes in the future. The client may use its
466
		// own system clock to generate the timestamp and that may differ from the server's time.
467
		if ($providedTime > $currentTime + 600) {
468
			throw new AmpacheException('Invalid Login - timestamp is in future', 401);
469
		}
470
	}
471
472
	private function checkHandshakeAuthentication($user, $timestamp, $auth) {
473
		$hashes = $this->ampacheUserMapper->getPasswordHashes($user);
474
475
		foreach ($hashes as $hash) {
476
			$expectedHash = \hash('sha256', $timestamp . $hash);
477
478
			if ($expectedHash === $auth) {
479
				return;
480
			}
481
		}
482
483
		throw new AmpacheException('Invalid Login - passphrase does not match', 401);
484
	}
485
486
	private function startNewSession($user, $expiryDate) {
487
		// this can cause collision, but it's just a temporary token
488
		$token = \md5(\uniqid(\rand(), true));
489
490
		// create new session
491
		$session = new AmpacheSession();
492
		$session->setUserId($user);
493
		$session->setToken($token);
494
		$session->setExpiry($expiryDate);
495
496
		// save session
497
		$this->ampacheSessionMapper->insert($session);
498
499
		return $token;
500
	}
501
502
	private function findEntities(BusinessLayer $businessLayer, $filter, $exact, $limit=null, $offset=null) {
503
		$userId = $this->ampacheUser->getUserId();
504
505
		if ($filter) {
506
			$fuzzy = !((boolean) $exact);
507
			return $businessLayer->findAllByName($filter, $userId, $fuzzy, $limit, $offset);
508
		} else {
509
			return $businessLayer->findAll($userId, SortBy::Name, $limit, $offset);
510
		}
511
	}
512
513
	/**
514
	 * Getting all tracks with this helper is more efficient than with `findEntities`
515
	 * followed by `injectArtistAndAlbum`. This is because, under the hood, the albums
516
	 * and artists are fetched with a single DB query instead of fetching each separately.
517
	 * 
518
	 * The result set is ordered first by artist and then by song title.
519
	 */
520
	private function getAllTracks() {
521
		$userId = $this->ampacheUser->getUserId();
522
		$tracks = $this->library->getTracksAlbumsAndArtists($userId)['tracks'];
523
		\usort($tracks, ['\OCA\Music\Db\Track', 'compareArtistAndTitle']);
524
		foreach ($tracks as $index => &$track) {
525
			$track->setNumberOnPlaylist($index + 1);
526
		}
527
		return $tracks;
528
	}
529
530
	private function createAmpacheActionUrl($action, $id, $auth) {
531
		$api = $this->jsonMode ? 'music.ampache.jsonApi' : 'music.ampache.xmlApi';
532
		return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute($api))
533
				. "?action=$action&id=$id&auth=$auth";
534
	}
535
536
	private function createCoverUrl($entity, $auth) {
537
		if ($entity instanceof Album) {
538
			$type = 'album';
539
		} elseif ($entity instanceof Artist) {
540
			$type = 'artist';
541
		} else {
542
			throw new AmpacheException('unexpeted entity type for cover image', 500);
543
		}
544
545
		if ($entity->getCoverFileId()) {
546
			return $this->createAmpacheActionUrl("_get_{$type}_cover", $entity->getId(), $auth);
547
		} else {
548
			return '';
549
		}
550
	}
551
552
	/**
553
	 * Any non-integer values and integer value 0 are converted to null to
554
	 * indicate "no limit" or "no offset".
555
	 * @param string $value
556
	 * @return integer|null
557
	 */
558
	private static function validateLimitOrOffset($value) {
559
		if (\ctype_digit(\strval($value)) && $value !== 0) {
560
			return \intval($value);
561
		} else {
562
			return null;
563
		}
564
	}
565
566
	/**
567
	 * @param int $index
568
	 * @param int|null $offset
569
	 * @param int|null $limit
570
	 * @return boolean
571
	 */
572
	private static function indexIsWithinOffsetAndLimit($index, $offset, $limit) {
573
		$offset = \intval($offset); // missing offset is interpreted as 0-offset
574
		return ($limit === null) || ($index >= $offset && $index < $offset + $limit);
575
	}
576
577
	private function renderArtists($artists, $auth) {
578
		$userId = $this->ampacheUser->getUserId();
579
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
580
581
		return $this->ampacheResponse([
582
			'artist' => \array_map(function($artist) use ($userId, $genreMap, $auth) {
583
				return [
584
					'id' => (string)$artist->getId(),
585
					'name' => $artist->getNameString($this->l10n),
586
					'albums' => $this->albumBusinessLayer->countByArtist($artist->getId()),
587
					'songs' => $this->trackBusinessLayer->countByArtist($artist->getId()),
588
					'art' => $this->createCoverUrl($artist, $auth),
589
					'rating' => 0,
590
					'preciserating' => 0,
591
					'tag' => \array_map(function($genreId) use ($genreMap) {
592
						return [
593
							'id' => (string)$genreId,
594
							'value' => $genreMap[$genreId]->getNameString($this->l10n),
595
							'count' => 1
596
						];
597
					}, $this->trackBusinessLayer->getGenresByArtistId($artist->getId(), $userId))
598
				];
599
			}, $artists)
600
		]);
601
	}
602
603
	private function renderAlbums($albums, $auth) {
604
		$userId = $this->ampacheUser->getUserId();
605
606
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
607
608
		return $this->ampacheResponse([
609
			'album' => \array_map(function($album) use ($userId, $auth, $genreMap) {
610
				$artist = $this->artistBusinessLayer->find($album->getAlbumArtistId(), $userId);
611
				return [
612
					'id' => (string)$album->getId(),
613
					'name' => $album->getNameString($this->l10n),
614
					'artist' => [
615
						'id' => (string)$artist->getId(),
616
						'value' => $artist->getNameString($this->l10n)
617
					],
618
					'tracks' => $this->trackBusinessLayer->countByAlbum($album->getId()),
619
					'rating' => 0,
620
					'year' => $album->yearToAPI(),
621
					'art' => $this->createCoverUrl($album, $auth),
622
					'preciserating' => 0,
623
					'tag' => \array_map(function($genreId) use ($genreMap) {
624
						return [
625
							'id' => (string)$genreId,
626
							'value' => $genreMap[$genreId]->getNameString($this->l10n),
627
							'count' => 1
628
						];
629
					}, $album->getGenres())
630
				];
631
			}, $albums)
632
		]);
633
	}
634
635
	private function injectArtistAndAlbum(&$tracks, $commonArtist=null, $commonAlbum=null) {
636
		$userId = $this->ampacheUser->getUserId();
637
638
		foreach ($tracks as &$track) {
639
			$artist = $commonArtist ?: $this->artistBusinessLayer->find($track->getArtistId(), $userId);
640
			$track->setArtist($artist);
641
642
			if (!empty($commonAlbum)) {
643
				$track->setAlbum($commonAlbum);
644
			} else {
645
				$album = $this->albumBusinessLayer->find($track->getAlbumId(), $userId);
646
				$album->setAlbumArtist($this->artistBusinessLayer->find($album->getAlbumArtistId(), $userId));
647
				$track->setAlbum($album);
648
			}
649
		}
650
	}
651
652
	private function renderSongs($tracks, $auth) {
653
		$userId = $this->ampacheUser->getUserId();
654
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
655
656
		return $this->ampacheResponse([
657
			'song' => \array_map(function($track) use ($auth, $genreMap) {
658
				$artist = $track->getArtist();
659
				$album = $track->getAlbum();
660
				$albumArtist = $album->getAlbumArtist();
661
662
				$result = [
663
					'id' => (string)$track->getId(),
664
					'title' => $track->getTitle(),
665
					'artist' => [
666
						'id' => (string)$artist->getId(),
667
						'value' => $artist->getNameString($this->l10n)
668
					],
669
					'albumartist' => [
670
						'id' => (string)$albumArtist->getId(),
671
						'value' => $albumArtist->getNameString($this->l10n)
672
					],
673
					'album' => [
674
						'id' => (string)$album->getId(),
675
						'value' => $album->getNameString($this->l10n)
676
					],
677
					'url' => $this->createAmpacheActionUrl('download', $track->getId(), $auth),
678
					'time' => $track->getLength(),
679
					'year' => $track->getYear(),
680
					'track' => $track->getAdjustedTrackNumber(),
681
					'bitrate' => $track->getBitrate(),
682
					'mime' => $track->getMimetype(),
683
					'size' => $track->getSize(),
684
					'art' => $this->createCoverUrl($album, $auth),
685
					'rating' => 0,
686
					'preciserating' => 0,
687
				];
688
689
				$genreId = $track->getGenreId();
690
				if ($genreId !== null) {
691
					$result['tag'] = [[
692
						'id' => (string)$genreId,
693
						'value' => $genreMap[$genreId]->getNameString($this->l10n),
694
						'count' => 1
695
					]];
696
				}
697
				return $result;
698
			}, $tracks)
699
		]);
700
	}
701
702
	private function renderPlaylists($playlists) {
703
		return $this->ampacheResponse([
704
			'playlist' => \array_map(function($playlist) {
705
				return [
706
					'id' => (string)$playlist->getId(),
707
					'name' => $playlist->getName(),
708
					'owner' => $this->ampacheUser->getUserId(),
709
					'items' => $playlist->getTrackCount(),
710
					'type' => 'Private'
711
				];
712
			}, $playlists)
713
		]);
714
	}
715
716
	private function renderTags($genres) {
717
		return $this->ampacheResponse([
718
			'tag' => \array_map(function($genre) {
719
				return [
720
					'id' => (string)$genre->getId(),
721
					'name' => $genre->getNameString($this->l10n),
722
					'albums' => $genre->getAlbumCount(),
723
					'artists' => $genre->getArtistCount(),
724
					'songs' => $genre->getTrackCount(),
725
					'videos' => 0,
726
					'playlists' => 0,
727
					'stream' => 0
728
				];
729
			}, $genres)
730
		]);
731
	}
732
733
	/**
734
	 * Array is considered to be "indexed" if its first element has numerical key.
735
	 * Empty array is considered to be "indexed".
736
	 * @param array $array
737
	 */
738
	private static function arrayIsIndexed(array $array) {
739
		reset($array);
740
		return empty($array) || \is_int(key($array));
741
	}
742
743
	/**
744
	 * The JSON API has some asymmetries with the JSON API. This function makes the needed
745
	 * translations for the result content before it is converted into JSON. 
746
	 * @param array $content
747
	 * @return array
748
	 */
749
	private static function prepareResultForJsonApi($content) {
750
		// In all responses returning an array of library entities, the root node is anonymous.
751
		// Unwrap the outermost array if it is an associative array with a single array-type value.
752
		if (\count($content) === 1 && !self::arrayIsIndexed($content)
753
				&& \is_array(\current($content)) && self::arrayIsIndexed(\current($content))) {
754
			$content = \array_pop($content);
755
		}
756
757
		// The key 'value' has a special meaning on XML responses, as it makes the corresponding value
758
		// to be treated as text content of the parent element. In the JSON API, these are mostly
759
		// substituted with property 'name', but error responses use the property 'message', instead.
760
		if (\array_key_exists('error', $content)) {
761
			$content = Util::convertArrayKeys($content, ['value' => 'message']);
762
		} else {
763
			$content = Util::convertArrayKeys($content, ['value' => 'name']);
764
		}
765
		return $content;
766
	}
767
}
768
769
/**
770
 * Adapter class which acts like the Playlist class for the purpose of 
771
 * AmpacheController::renderPlaylists but contains all the track of the user. 
772
 */
773
class AmpacheController_AllTracksPlaylist {
774
775
	private $user;
776
	private $trackBusinessLayer;
777
	private $l10n;
778
779
	public function __construct($user, $trackBusinessLayer, $l10n) {
780
		$this->user = $user;
781
		$this->trackBusinessLayer = $trackBusinessLayer;
782
		$this->l10n = $l10n;
783
	}
784
785
	public function getId() {
786
		return AmpacheController::ALL_TRACKS_PLAYLIST_ID;
787
	}
788
789
	public function getName() {
790
		return $this->l10n->t('All tracks');
791
	}
792
793
	public function getTrackCount() {
794
		return $this->trackBusinessLayer->count($this->user);
795
	}
796
}
797