Passed
Push — master ( 89d351...c23d0c )
by Pauli
01:55
created

AmpacheController::dispatch()   D

Complexity

Conditions 34
Paths 34

Size

Total Lines 77
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 1190

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 34
eloc 72
c 4
b 0
f 0
nc 34
nop 9
dl 0
loc 77
ccs 0
cts 73
cp 0
crap 1190
rs 4.1666

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Random;
48
use \OCA\Music\Utility\Util;
49
50
class AmpacheController extends Controller {
51
	private $ampacheUserMapper;
52
	private $ampacheSessionMapper;
53
	private $albumBusinessLayer;
54
	private $artistBusinessLayer;
55
	private $genreBusinessLayer;
56
	private $playlistBusinessLayer;
57
	private $trackBusinessLayer;
58
	private $library;
59
	private $ampacheUser;
60
	private $urlGenerator;
61
	private $rootFolder;
62
	private $l10n;
63
	private $coverHelper;
64
	private $random;
65
	private $logger;
66
	private $jsonMode;
67
68
	const SESSION_EXPIRY_TIME = 6000;
69
	const ALL_TRACKS_PLAYLIST_ID = 10000000;
70
	const API_VERSION = 400001;
71
	const API_MIN_COMPATIBLE_VERSION = 350001;
72
73
	public function __construct($appname,
74
								IRequest $request,
75
								$l10n,
76
								IURLGenerator $urlGenerator,
77
								AmpacheUserMapper $ampacheUserMapper,
78
								AmpacheSessionMapper $ampacheSessionMapper,
79
								AlbumBusinessLayer $albumBusinessLayer,
80
								ArtistBusinessLayer $artistBusinessLayer,
81
								GenreBusinessLayer $genreBusinessLayer,
82
								PlaylistBusinessLayer $playlistBusinessLayer,
83
								TrackBusinessLayer $trackBusinessLayer,
84
								Library $library,
85
								AmpacheUser $ampacheUser,
86
								$rootFolder,
87
								CoverHelper $coverHelper,
88
								Random $random,
89
								Logger $logger) {
90
		parent::__construct($appname, $request);
91
92
		$this->ampacheUserMapper = $ampacheUserMapper;
93
		$this->ampacheSessionMapper = $ampacheSessionMapper;
94
		$this->albumBusinessLayer = $albumBusinessLayer;
95
		$this->artistBusinessLayer = $artistBusinessLayer;
96
		$this->genreBusinessLayer = $genreBusinessLayer;
97
		$this->playlistBusinessLayer = $playlistBusinessLayer;
98
		$this->trackBusinessLayer = $trackBusinessLayer;
99
		$this->library = $library;
100
		$this->urlGenerator = $urlGenerator;
101
		$this->l10n = $l10n;
102
103
		// used to share user info with middleware
104
		$this->ampacheUser = $ampacheUser;
105
106
		// used to deliver actual media file
107
		$this->rootFolder = $rootFolder;
108
109
		$this->coverHelper = $coverHelper;
110
		$this->random = $random;
111
		$this->logger = $logger;
112
	}
113
114
	public function setJsonMode($useJsonMode) {
115
		$this->jsonMode = $useJsonMode;
116
	}
117
118
	public function ampacheResponse($content) {
119
		if ($this->jsonMode) {
120
			return new JSONResponse(self::prepareResultForJsonApi($content));
121
		} else {
122
			return new XMLResponse(self::prepareResultForXmlApi($content), ['id', 'index', 'count', 'code']);
123
		}
124
	}
125
126
	/**
127
	 * @NoAdminRequired
128
	 * @PublicPage
129
	 * @NoCSRFRequired
130
	 */
131
	public function xmlApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id) {
132
		// differentation between xmlApi and jsonApi is made already by the middleware
133
		return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id);
134
	}
135
136
	/**
137
	 * @NoAdminRequired
138
	 * @PublicPage
139
	 * @NoCSRFRequired
140
	 */
141
	public function jsonApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id) {
142
		// differentation between xmlApi and jsonApi is made already by the middleware
143
		return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id);
144
	}
145
146
	protected function dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id) {
147
		$this->logger->log("Ampache action '$action' requested", 'debug');
148
149
		$limit = self::validateLimitOrOffset($limit);
150
		$offset = self::validateLimitOrOffset($offset);
151
152
		switch ($action) {
153
			case 'handshake':
154
				return $this->handshake($user, $timestamp, $auth);
155
			case 'goodbye':
156
				return $this->goodbye($auth);
157
			case 'ping':
158
				return $this->ping($auth);
159
			case 'get_indexes':
160
				return $this->get_indexes($filter, $limit, $offset);
161
			case 'stats':
162
				return $this->stats($limit, $offset, $auth);
163
			case 'artists':
164
				return $this->artists($filter, $exact, $limit, $offset, $auth);
165
			case 'artist':
166
				return $this->artist($filter, $auth);
167
			case 'artist_albums':
168
				return $this->artist_albums($filter, $auth);
169
			case 'album_songs':
170
				return $this->album_songs($filter, $auth);
171
			case 'albums':
172
				return $this->albums($filter, $exact, $limit, $offset, $auth);
173
			case 'album':
174
				return $this->album($filter, $auth);
175
			case 'artist_songs':
176
				return $this->artist_songs($filter, $auth);
177
			case 'songs':
178
				return $this->songs($filter, $exact, $limit, $offset, $auth);
179
			case 'song':
180
				return $this->song($filter, $auth);
181
			case 'search_songs':
182
				return $this->search_songs($filter, $auth);
183
			case 'playlists':
184
				return $this->playlists($filter, $exact, $limit, $offset);
185
			case 'playlist':
186
				return $this->playlist($filter);
187
			case 'playlist_songs':
188
				return $this->playlist_songs($filter, $limit, $offset, $auth);
189
			case 'playlist_create':
190
				return $this->playlist_create();
191
			case 'playlist_edit':
192
				return $this->playlist_edit($filter);
193
			case 'playlist_delete':
194
				return $this->playlist_delete($filter);
195
			case 'playlist_add_song':
196
				return $this->playlist_add_song($filter);
197
			case 'playlist_remove_song':
198
				return $this->playlist_remove_song($filter);
199
			case 'playlist_generate':
200
				return $this->playlist_generate($filter, $limit, $offset, $auth);
201
			case 'tags':
202
				return $this->tags($filter, $exact, $limit, $offset);
203
			case 'tag':
204
				return $this->tag($filter);
205
			case 'tag_artists':
206
				return $this->tag_artists($filter, $limit, $offset, $auth);
207
			case 'tag_albums':
208
				return $this->tag_albums($filter, $limit, $offset, $auth);
209
			case 'tag_songs':
210
				return $this->tag_songs($filter, $limit, $offset, $auth);
211
			case 'flag':
212
				return $this->flag();
213
			case 'download':
214
				return $this->download($id); // args 'type' and 'format' not supported
215
			case 'stream':
216
				return $this->stream($id, $offset); // args 'type', 'bitrate', 'format', and 'length' not supported
217
			case 'get_art':
218
				return $this->get_art($id);
219
		}
220
221
		$this->logger->log("Unsupported Ampache action '$action' requested", 'warn');
222
		throw new AmpacheException('Action not supported', 405);
223
	}
224
225
	/***********************
226
	 * Ampahce API methods *
227
	 ***********************/
228
229
	protected function handshake($user, $timestamp, $auth) {
230
		$currentTime = \time();
231
		$expiryDate = $currentTime + self::SESSION_EXPIRY_TIME;
232
233
		$this->checkHandshakeTimestamp($timestamp, $currentTime);
234
		$this->checkHandshakeAuthentication($user, $timestamp, $auth);
235
		$token = $this->startNewSession($user, $expiryDate);
236
237
		$currentTimeFormated = \date('c', $currentTime);
238
		$expiryDateFormated = \date('c', $expiryDate);
239
240
		return $this->ampacheResponse([
241
			'auth' => $token,
242
			'api' => self::API_VERSION,
243
			'update' => $currentTimeFormated,
244
			'add' => $currentTimeFormated,
245
			'clean' => $currentTimeFormated,
246
			'songs' => $this->trackBusinessLayer->count($user),
247
			'artists' => $this->artistBusinessLayer->count($user),
248
			'albums' => $this->albumBusinessLayer->count($user),
249
			'playlists' => $this->playlistBusinessLayer->count($user) + 1, // +1 for "All tracks"
250
			'session_expire' => $expiryDateFormated,
251
			'tags' => $this->genreBusinessLayer->count($user),
252
			'videos' => 0,
253
			'catalogs' => 0
254
		]);
255
	}
256
257
	protected function goodbye($auth) {
258
		// getting the session should not throw as the middleware has already checked that the token is valid
259
		$session = $this->ampacheSessionMapper->findByToken($auth);
260
		$this->ampacheSessionMapper->delete($session);
261
262
		return $this->ampacheResponse(['success' => "goodbye: $auth"]);
263
	}
264
265
	protected function ping($auth) {
266
		$response = [
267
			'server' => $this->getAppNameAndVersion(),
268
			'version' => self::API_VERSION,
269
			'compatible' => self::API_MIN_COMPATIBLE_VERSION
270
		];
271
272
		if (!empty($auth)) {
273
			// getting the session should not throw as the middleware has already checked that the token is valid
274
			$session = $this->ampacheSessionMapper->findByToken($auth);
275
			$response['session_expire'] = \date('c', $session->getExpiry());
276
		}
277
278
		return $this->ampacheResponse($response);
279
	}
280
281
	protected function get_indexes($filter, $limit, $offset) {
282
		// TODO: args $add, $update
283
		$type = $this->getRequiredParam('type');
284
285
		$businessLayer = $this->getBusinessLayer($type);
286
		$entities = $this->findEntities($businessLayer, $filter, false, $limit, $offset);
287
		return $this->renderEntitiesIndex($entities, $type);
288
	}
289
290
	protected function stats($limit, $offset, $auth) {
291
		$type = $this->getRequiredParam('type');
292
		$filter = $this->getRequiredParam('filter');
293
		$userId = $this->ampacheUser->getUserId();
294
295
		if (!\in_array($type, ['song', 'album', 'artist'])) {
296
			throw new AmpacheException("Unsupported type $type", 400);
297
		}
298
		$businessLayer = $this->getBusinessLayer($type);
299
300
		switch ($filter) {
301
			case 'newest':
302
				$entities = $businessLayer->findAll($userId, SortBy::Newest, $limit, $offset);
303
				break;
304
			case 'flagged':
305
				$entities = $businessLayer->findAllStarred($userId, $limit, $offset);
306
				break;
307
			case 'random':
308
				$entities = $businessLayer->findAll($userId, SortBy::None);
309
				$indices = $this->random->getIndices(\count($entities), $offset, $limit, $userId, 'ampache_stats_'.$type);
310
				$entities = Util::arrayMultiGet($entities, $indices);
311
				break;
312
			case 'highest':		//TODO
313
			case 'frequent':	//TODO
314
			case 'recent':		//TODO
315
			case 'forgotten':	//TODO
316
			default:
317
				throw new AmpacheException("Unsupported filter $filter", 400);
318
		}
319
320
		return $this->renderEntities($entities, $type, $auth);
321
	}
322
323
	protected function artists($filter, $exact, $limit, $offset, $auth) {
324
		$artists = $this->findEntities($this->artistBusinessLayer, $filter, $exact, $limit, $offset);
325
		return $this->renderArtists($artists, $auth);
326
	}
327
328
	protected function artist($artistId, $auth) {
329
		$userId = $this->ampacheUser->getUserId();
330
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
331
		return $this->renderArtists([$artist], $auth);
332
	}
333
334
	protected function artist_albums($artistId, $auth) {
335
		$userId = $this->ampacheUser->getUserId();
336
		$albums = $this->albumBusinessLayer->findAllByArtist($artistId, $userId);
337
		return $this->renderAlbums($albums, $auth);
338
	}
339
340
	protected function artist_songs($artistId, $auth) {
341
		$userId = $this->ampacheUser->getUserId();
342
		$tracks = $this->trackBusinessLayer->findAllByArtist($artistId, $userId);
343
		return $this->renderSongs($tracks, $auth);
344
	}
345
346
	protected function album_songs($albumId, $auth) {
347
		$userId = $this->ampacheUser->getUserId();
348
349
		$album = $this->albumBusinessLayer->find($albumId, $userId);
350
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $userId);
351
352
		foreach ($tracks as &$track) {
353
			$track->setAlbum($album);
354
		}
355
356
		return $this->renderSongs($tracks, $auth);
357
	}
358
359
	protected function song($trackId, $auth) {
360
		$userId = $this->ampacheUser->getUserId();
361
		$track = $this->trackBusinessLayer->find($trackId, $userId);
362
		$trackInArray = [$track];
363
		return $this->renderSongs($trackInArray, $auth);
364
	}
365
366
	protected function songs($filter, $exact, $limit, $offset, $auth) {
367
368
		// optimized handling for fetching the whole library
369
		// note: the ordering of the songs differs between these two cases
370
		if (empty($filter) && !$limit && !$offset) {
371
			$tracks = $this->getAllTracks();
372
		}
373
		// general case
374
		else {
375
			$tracks = $this->findEntities($this->trackBusinessLayer, $filter, $exact, $limit, $offset);
376
		}
377
378
		return $this->renderSongs($tracks, $auth);
379
	}
380
381
	protected function search_songs($filter, $auth) {
382
		$userId = $this->ampacheUser->getUserId();
383
		$tracks = $this->trackBusinessLayer->findAllByNameRecursive($filter, $userId);
384
		return $this->renderSongs($tracks, $auth);
385
	}
386
387
	protected function albums($filter, $exact, $limit, $offset, $auth) {
388
		$albums = $this->findEntities($this->albumBusinessLayer, $filter, $exact, $limit, $offset);
389
		return $this->renderAlbums($albums, $auth);
390
	}
391
392
	protected function album($albumId, $auth) {
393
		$userId = $this->ampacheUser->getUserId();
394
		$album = $this->albumBusinessLayer->find($albumId, $userId);
395
		return $this->renderAlbums([$album], $auth);
396
	}
397
398
	protected function playlists($filter, $exact, $limit, $offset) {
399
		$userId = $this->ampacheUser->getUserId();
400
		$playlists = $this->findEntities($this->playlistBusinessLayer, $filter, $exact, $limit, $offset);
401
402
		// append "All tracks" if not searching by name, and it is not off-limit
403
		$allTracksIndex = $this->playlistBusinessLayer->count($userId);
404
		if (empty($filter) && self::indexIsWithinOffsetAndLimit($allTracksIndex, $offset, $limit)) {
405
			$playlists[] = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n);
406
		}
407
408
		return $this->renderPlaylists($playlists);
409
	}
410
411
	protected function playlist($listId) {
412
		$userId = $this->ampacheUser->getUserId();
413
		if ($listId == self::ALL_TRACKS_PLAYLIST_ID) {
414
			$playlist = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n);
415
		} else {
416
			$playlist = $this->playlistBusinessLayer->find($listId, $userId);
417
		}
418
		return $this->renderPlaylists([$playlist]);
419
	}
420
421
	protected function playlist_songs($listId, $limit, $offset, $auth) {
422
		if ($listId == self::ALL_TRACKS_PLAYLIST_ID) {
423
			$playlistTracks = $this->getAllTracks();
424
			$playlistTracks = \array_slice($playlistTracks, $offset, $limit);
425
		}
426
		else {
427
			$userId = $this->ampacheUser->getUserId();
428
			$playlistTracks = $this->playlistBusinessLayer->getPlaylistTracks($listId, $userId, $limit, $offset);
429
		}
430
		return $this->renderSongs($playlistTracks, $auth);
431
	}
432
433
	protected function playlist_create() {
434
		$name = $this->getRequiredParam('name');
435
		$playlist = $this->playlistBusinessLayer->create($name, $this->ampacheUser->getUserId());
436
		return $this->renderPlaylists([$playlist]);
437
	}
438
439
	protected function playlist_edit($listId) {
440
		$name = $this->request->getParam('name');
441
		$items = $this->request->getParam('items'); // track IDs
442
		$tracks = $this->request->getParam('tracks'); // 1-based indices of the tracks
443
444
		$edited = false;
445
		$userId = $this->ampacheUser->getUserId();
446
		$playlist = $this->playlistBusinessLayer->find($listId, $userId);
447
448
		if (!empty($name)) {
449
			$playlist->setName($name);
450
			$edited = true;
451
		}
452
453
		$newTrackIds = Util::explode(',', $items);
454
		$newTrackOrdinals = Util::explode(',', $tracks);
455
456
		if (\count($newTrackIds) != \count($newTrackOrdinals)) {
457
			throw new AmpacheException("Arguments 'items' and 'tracks' must contain equal amount of elements", 400);
458
		}
459
		else if (\count($newTrackIds) > 0) {
460
			$trackIds = $playlist->getTrackIdsAsArray();
461
462
			for ($i = 0, $count = \count($newTrackIds); $i < $count; ++$i) {
463
				if (!$this->trackBusinessLayer->exists($newTrackIds[$i], $userId)) {
464
					throw new AmpacheException("Invalid song ID $song", 400);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $song seems to be never defined.
Loading history...
465
				}
466
				$trackIds[$newTrackOrdinals[$i]-1] = $newTrackIds[$i];
467
			}
468
469
			$playlist->setTrackIdsFromArray($trackIds);
470
			$edited = true;
471
		}
472
473
		if ($edited) {
474
			$this->playlistBusinessLayer->update($playlist);
475
			return $this->ampacheResponse(['success' => 'playlist changes saved']);
476
		} else {
477
			throw new AmpacheException('Nothing was changed', 400);
478
		}
479
	}
480
481
	protected function playlist_delete($listId) {
482
		$this->playlistBusinessLayer->delete($listId, $this->ampacheUser->getUserId());
483
		return $this->ampacheResponse(['success' => 'playlist deleted']);
484
	}
485
486
	protected function playlist_add_song($listId) {
487
		$song = $this->getRequiredParam('song'); // track ID
488
		$check = $this->request->getParam('check', false);
489
490
		$userId = $this->ampacheUser->getUserId();
491
		if (!$this->trackBusinessLayer->exists($song, $userId)) {
492
			throw new AmpacheException("Invalid song ID $song", 400);
493
		}
494
495
		$playlist = $this->playlistBusinessLayer->find($listId, $userId);
496
		$trackIds = $playlist->getTrackIdsAsArray();
497
498
		if ($check && \in_array($song, $trackIds)) {
499
			throw new AmpacheException("Can't add a duplicate item when check is enabled", 400);
500
		}
501
502
		$trackIds[] = $song;
503
		$playlist->setTrackIdsFromArray($trackIds);
504
		$this->playlistBusinessLayer->update($playlist);
505
		return $this->ampacheResponse(['success' => 'song added to playlist']);
506
	}
507
508
	protected function playlist_remove_song($listId) {
509
		$song = $this->request->getParam('song'); // track ID
510
		$track = $this->request->getParam('track'); // 1-based index of the track
511
		$clear = $this->request->getParam('clear'); // added in API v420000 but we support this already now
512
513
		$playlist = $this->playlistBusinessLayer->find($listId, $this->ampacheUser->getUserId());
514
515
		if ((int)$clear === 1) {
516
			$trackIds = [];
517
			$message = 'all songs removed from playlist';
518
		}
519
		elseif ($song !== null) {
520
			$trackIds = $playlist->getTrackIdsAsArray();
521
			if (!\in_array($song, $trackIds)) {
522
				throw new AmpacheException("Song $song not found in playlist", 400);
523
			}
524
			$trackIds = Util::arrayDiff($trackIds, [$song]);
525
			$message = 'song removed from playlist';
526
		}
527
		elseif ($track !== null) {
528
			$trackIds = $playlist->getTrackIdsAsArray();
529
			if ($track < 1 || $track > \count($trackIds)) {
530
				throw new AmpacheException("Track ordinal $track is out of bounds", 400);
531
			}
532
			unset($trackIds[$track-1]);
533
			$message = 'song removed from playlist';
534
		}
535
		else {
536
			throw new AmpacheException("One of the arguments 'clear', 'song', 'track' is required", 400);
537
		}
538
539
		$playlist->setTrackIdsFromArray($trackIds);
540
		$this->playlistBusinessLayer->update($playlist);
541
		return $this->ampacheResponse(['success' => $message]);
542
	}
543
544
	protected function playlist_generate($filter, $limit, $offset, $auth) {
545
		$mode = $this->request->getParam('mode', 'random');
546
		$album = $this->request->getParam('album');
547
		$artist = $this->request->getParam('artist');
548
		$flag = $this->request->getParam('flag');
549
		$format = $this->request->getParam('format', 'song');
550
551
		$tracks = $this->findEntities($this->trackBusinessLayer, $filter, false); // $limit and $offset are applied later
552
553
		// filter the found tracks according to the additional requirements
554
		if ($album !== null) {
555
			$tracks = \array_filter($tracks, function($track) use ($album) {
556
				return ($track->getAlbumId() == $album);
557
			});
558
		}
559
		if ($artist !== null) {
560
			$tracks = \array_filter($tracks, function($track) use ($artist) {
561
				return ($track->getArtistId() == $artist);
562
			});
563
		}
564
		if ($flag == 1) {
565
			$tracks = \array_filter($tracks, function($track) {
566
				return ($track->getStarred() !== null);
567
			});
568
		}
569
		// After filtering, there may be "holes" between the array indices. Reindex the array.
570
		$tracks = \array_values($tracks);
571
572
		if ($mode == 'random') {
573
			$userId = $this->ampacheUser->getUserId();
574
			$indices = $this->random->getIndices(\count($tracks), $offset, $limit, $userId, 'ampache_playlist_generate');
575
			$tracks = Util::arrayMultiGet($tracks, $indices);
576
		} else { // 'recent', 'forgotten', 'unplayed'
577
			throw new AmpacheException("Mode '$mode' is not supported", 400);
578
		}
579
580
		switch ($format) {
581
			case 'song':
582
				return $this->renderSongs($tracks, $auth);
583
			case 'index':
584
				return $this->renderSongsIndex($tracks);
585
			case 'id':
586
				return $this->renderEntityIds($tracks);
587
			default:
588
				throw new AmpacheException("Format '$format' is not supported", 400);
589
		}
590
	}
591
592
	protected function tags($filter, $exact, $limit, $offset) {
593
		$genres = $this->findEntities($this->genreBusinessLayer, $filter, $exact, $limit, $offset);
594
		return $this->renderTags($genres);
595
	}
596
597
	protected function tag($tagId) {
598
		$userId = $this->ampacheUser->getUserId();
599
		$genre = $this->genreBusinessLayer->find($tagId, $userId);
600
		return $this->renderTags([$genre]);
601
	}
602
603
	protected function tag_artists($genreId, $limit, $offset, $auth) {
604
		$userId = $this->ampacheUser->getUserId();
605
		$artists = $this->artistBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
606
		return $this->renderArtists($artists, $auth);
607
	}
608
609
	protected function tag_albums($genreId, $limit, $offset, $auth) {
610
		$userId = $this->ampacheUser->getUserId();
611
		$albums = $this->albumBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
612
		return $this->renderAlbums($albums, $auth);
613
	}
614
615
	protected function tag_songs($genreId, $limit, $offset, $auth) {
616
		$userId = $this->ampacheUser->getUserId();
617
		$tracks = $this->trackBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
618
		return $this->renderSongs($tracks, $auth);
619
	}
620
621
	protected function flag() {
622
		$type = $this->getRequiredParam('type');
623
		$id = $this->getRequiredParam('id');
624
		$flag = $this->getRequiredParam('flag');
625
		$flag = \filter_var($flag, FILTER_VALIDATE_BOOLEAN);
626
627
		if (!\in_array($type, ['song', 'album', 'artist'])) {
628
			throw new AmpacheException("Unsupported type $type", 400);
629
		}
630
631
		$userId = $this->ampacheUser->getUserId();
632
		$businessLayer = $this->getBusinessLayer($type);
633
		if ($flag) {
634
			$modifiedCount = $businessLayer->setStarred([$id], $userId);
635
			$message = "flag ADDED to $id";
636
		} else {
637
			$modifiedCount = $businessLayer->unsetStarred([$id], $userId);
638
			$message = "flag REMOVED from $id";
639
		}
640
641
		if ($modifiedCount > 0) {
642
			return $this->ampacheResponse(['success' => $message]);
643
		} else {
644
			throw new AmpacheException("The $type $id was not found", 400);
645
		}
646
	}
647
648
	protected function download($trackId) {
649
		$userId = $this->ampacheUser->getUserId();
650
651
		try {
652
			$track = $this->trackBusinessLayer->find($trackId, $userId);
653
		} catch (BusinessLayerException $e) {
654
			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...
655
		}
656
657
		$files = $this->rootFolder->getUserFolder($userId)->getById($track->getFileId());
658
659
		if (\count($files) === 1) {
660
			return new FileResponse($files[0]);
661
		} else {
662
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
663
		}
664
	}
665
666
	protected function stream($trackId, $offset) {
667
		// This is just a dummy implementation. We don't support transcoding or streaming
668
		// from a time offset.
669
		// All the other unsupported arguments are just ignored, but a request with an offset
670
		// is responded with an error. This is becuase the client would probably work in an
671
		// unexpected way if it thinks it's streaming from offset but actually it is streaming
672
		// from the beginning of the file. Returning an error gives the client a chance to fallback
673
		// to other methods of seeking.
674
		if ($offset !== null) {
675
			throw new AmpacheException('Streaming with time offset is not supported', 400);
676
		}
677
678
		return $this->download($trackId);
679
	}
680
681
	protected function get_art($id) {
682
		$type = $this->getRequiredParam('type');
683
684
		if (!\in_array($type, ['song', 'album', 'artist'])) {
685
			throw new AmpacheException("Unsupported type $type", 400);
686
		}
687
688
		if ($type === 'song') {
689
			// map song to its parent album
690
			$id = $this->trackBusinessLayer->find($id, $this->ampacheUser->getUserId())->getAlbumId();
691
			$type = 'album';
692
		}
693
694
		return $this->getCover($id, $this->getBusinessLayer($type));
695
	}
696
697
698
	/********************
699
	 * Helper functions *
700
	 ********************/
701
702
	private function getBusinessLayer($type) {
703
		switch ($type) {
704
			case 'song':		return $this->trackBusinessLayer;
705
			case 'album':		return $this->albumBusinessLayer;
706
			case 'artist':		return $this->artistBusinessLayer;
707
			case 'playlist':	return $this->playlistBusinessLayer;
708
			case 'tag':			return $this->genreBusinessLayer;
709
			default:			throw new AmpacheException("Unsupported type $type", 400);
710
		}
711
	}
712
713
	private function renderEntities($entities, $type, $auth) {
714
		switch ($type) {
715
			case 'song':		return $this->renderSongs($entities, $auth);
716
			case 'album':		return $this->renderAlbums($entities, $auth);
717
			case 'artist':		return $this->renderArtists($entities, $auth);
718
			case 'playlist':	return $this->renderPlaylists($entities);
719
			case 'tag':			return $this->renderTags($entities);
720
			default:			throw new AmpacheException("Unsupported type $type", 400);
721
		}
722
	}
723
724
	private function renderEntitiesIndex($entities, $type) {
725
		switch ($type) {
726
			case 'song':		return $this->renderSongsIndex($entities);
727
			case 'album':		return $this->renderAlbumsIndex($entities);
728
			case 'artist':		return $this->renderArtistsIndex($entities);
729
			case 'playlist':	return $this->renderPlaylistsIndex($entities);
730
			default:			throw new AmpacheException("Unsupported type $type", 400);
731
		}
732
	}
733
734
	private function getAppNameAndVersion() {
735
		require \OC::$SERVERROOT . '/version.php'; // for vendor name ownlcoud/nextcloud
736
737
		// Note: the following is deprecated since NC14 but the replacement
738
		// \OCP\App\IAppManager::getAppVersion is not available before NC14.
739
		$appVersion = \OCP\App::getAppVersion($this->appName);
0 ignored issues
show
Bug introduced by
The type OCP\App 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...
740
741
		return "$vendor {$this->appName} $appVersion";
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $vendor seems to be never defined.
Loading history...
742
	}
743
744
	private function getCover($entityId, BusinessLayer $businessLayer) {
745
		$userId = $this->ampacheUser->getUserId();
746
		$userFolder = $this->rootFolder->getUserFolder($userId);
747
		$entity = $businessLayer->find($entityId, $userId);
748
749
		try {
750
			$coverData = $this->coverHelper->getCover($entity, $userId, $userFolder);
751
			if ($coverData !== null) {
752
				return new FileResponse($coverData);
753
			}
754
		} catch (BusinessLayerException $e) {
755
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity not found');
756
		}
757
758
		return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity has no cover');
759
	}
760
761
	private function checkHandshakeTimestamp($timestamp, $currentTime) {
762
		$providedTime = \intval($timestamp);
763
764
		if ($providedTime === 0) {
765
			throw new AmpacheException('Invalid Login - cannot parse time', 401);
766
		}
767
		if ($providedTime < ($currentTime - self::SESSION_EXPIRY_TIME)) {
768
			throw new AmpacheException('Invalid Login - session is outdated', 401);
769
		}
770
		// Allow the timestamp to be at maximum 10 minutes in the future. The client may use its
771
		// own system clock to generate the timestamp and that may differ from the server's time.
772
		if ($providedTime > $currentTime + 600) {
773
			throw new AmpacheException('Invalid Login - timestamp is in future', 401);
774
		}
775
	}
776
777
	private function checkHandshakeAuthentication($user, $timestamp, $auth) {
778
		$hashes = $this->ampacheUserMapper->getPasswordHashes($user);
779
780
		foreach ($hashes as $hash) {
781
			$expectedHash = \hash('sha256', $timestamp . $hash);
782
783
			if ($expectedHash === $auth) {
784
				return;
785
			}
786
		}
787
788
		throw new AmpacheException('Invalid Login - passphrase does not match', 401);
789
	}
790
791
	private function startNewSession($user, $expiryDate) {
792
		// this can cause collision, but it's just a temporary token
793
		$token = \md5(\uniqid(\rand(), true));
794
795
		// create new session
796
		$session = new AmpacheSession();
797
		$session->setUserId($user);
798
		$session->setToken($token);
799
		$session->setExpiry($expiryDate);
800
801
		// save session
802
		$this->ampacheSessionMapper->insert($session);
803
804
		return $token;
805
	}
806
807
	private function findEntities(BusinessLayer $businessLayer, $filter, $exact, $limit=null, $offset=null) {
808
		$userId = $this->ampacheUser->getUserId();
809
810
		if ($filter) {
811
			$fuzzy = !((boolean) $exact);
812
			return $businessLayer->findAllByName($filter, $userId, $fuzzy, $limit, $offset);
813
		} else {
814
			return $businessLayer->findAll($userId, SortBy::Name, $limit, $offset);
815
		}
816
	}
817
818
	/**
819
	 * Getting all tracks with this helper is more efficient than with `findEntities`
820
	 * followed by a call to `albumBusinessLayer->find(...)` on each track.
821
	 * This is because, under the hood, the albums are fetched with a single DB query
822
	 * instead of fetching each separately.
823
	 *
824
	 * The result set is ordered first by artist and then by song title.
825
	 */
826
	private function getAllTracks() {
827
		$userId = $this->ampacheUser->getUserId();
828
		$tracks = $this->library->getTracksAlbumsAndArtists($userId)['tracks'];
829
		\usort($tracks, ['\OCA\Music\Db\Track', 'compareArtistAndTitle']);
830
		foreach ($tracks as $index => &$track) {
831
			$track->setNumberOnPlaylist($index + 1);
832
		}
833
		return $tracks;
834
	}
835
836
	private function createAmpacheActionUrl($action, $id, $auth, $type=null) {
837
		$api = $this->jsonMode ? 'music.ampache.jsonApi' : 'music.ampache.xmlApi';
838
		return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute($api))
839
				. "?action=$action&id=$id&auth=$auth"
840
				. (!empty($type) ? "&type=$type" : '');
841
	}
842
843
	private function createCoverUrl($entity, $auth) {
844
		if ($entity instanceof Album) {
845
			$type = 'album';
846
		} elseif ($entity instanceof Artist) {
847
			$type = 'artist';
848
		} else {
849
			throw new AmpacheException('unexpeted entity type for cover image', 500);
850
		}
851
852
		if ($entity->getCoverFileId()) {
853
			return $this->createAmpacheActionUrl("get_art", $entity->getId(), $auth, $type);
854
		} else {
855
			return '';
856
		}
857
	}
858
859
	/**
860
	 * Any non-integer values and integer value 0 are converted to null to
861
	 * indicate "no limit" or "no offset".
862
	 * @param string $value
863
	 * @return integer|null
864
	 */
865
	private static function validateLimitOrOffset($value) {
866
		if (\ctype_digit(\strval($value)) && $value !== 0) {
867
			return \intval($value);
868
		} else {
869
			return null;
870
		}
871
	}
872
873
	/**
874
	 * @param int $index
875
	 * @param int|null $offset
876
	 * @param int|null $limit
877
	 * @return boolean
878
	 */
879
	private static function indexIsWithinOffsetAndLimit($index, $offset, $limit) {
880
		$offset = \intval($offset); // missing offset is interpreted as 0-offset
881
		return ($limit === null) || ($index >= $offset && $index < $offset + $limit);
882
	}
883
884
	private function renderArtists($artists, $auth) {
885
		$userId = $this->ampacheUser->getUserId();
886
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
887
888
		return $this->ampacheResponse([
889
			'artist' => \array_map(function($artist) use ($userId, $genreMap, $auth) {
890
				return [
891
					'id' => (string)$artist->getId(),
892
					'name' => $artist->getNameString($this->l10n),
893
					'albums' => $this->albumBusinessLayer->countByArtist($artist->getId()),
894
					'songs' => $this->trackBusinessLayer->countByArtist($artist->getId()),
895
					'art' => $this->createCoverUrl($artist, $auth),
896
					'rating' => 0,
897
					'preciserating' => 0,
898
					'tag' => \array_map(function($genreId) use ($genreMap) {
899
						return [
900
							'id' => (string)$genreId,
901
							'value' => $genreMap[$genreId]->getNameString($this->l10n),
902
							'count' => 1
903
						];
904
					}, $this->trackBusinessLayer->getGenresByArtistId($artist->getId(), $userId))
905
				];
906
			}, $artists)
907
		]);
908
	}
909
910
	private function renderAlbums($albums, $auth) {
911
		$userId = $this->ampacheUser->getUserId();
912
913
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
914
915
		return $this->ampacheResponse([
916
			'album' => \array_map(function($album) use ($auth, $genreMap) {
917
				return [
918
					'id' => (string)$album->getId(),
919
					'name' => $album->getNameString($this->l10n),
920
					'artist' => [
921
						'id' => (string)$album->getAlbumArtistId(),
922
						'value' => $album->getAlbumArtistNameString($this->l10n)
923
					],
924
					'tracks' => $this->trackBusinessLayer->countByAlbum($album->getId()),
925
					'rating' => 0,
926
					'year' => $album->yearToAPI(),
927
					'art' => $this->createCoverUrl($album, $auth),
928
					'preciserating' => 0,
929
					'tag' => \array_map(function($genreId) use ($genreMap) {
930
						return [
931
							'id' => (string)$genreId,
932
							'value' => $genreMap[$genreId]->getNameString($this->l10n),
933
							'count' => 1
934
						];
935
					}, $album->getGenres())
936
				];
937
			}, $albums)
938
		]);
939
	}
940
941
	private function renderSongs($tracks, $auth) {
942
		return $this->ampacheResponse([
943
			'song' => \array_map(function($track) use ($auth) {
944
				$userId = $this->ampacheUser->getUserId();
945
				$album = $track->getAlbum()
946
						?: $this->albumBusinessLayer->findOrDefault($track->getAlbumId(), $userId);
947
948
				$result = [
949
					'id' => (string)$track->getId(),
950
					'title' => $track->getTitle() ?: '',
951
					'name' => $track->getTitle() ?: '',
952
					'artist' => [
953
						'id' => (string)$track->getArtistId() ?: '0',
954
						'value' => $track->getArtistNameString($this->l10n)
955
					],
956
					'albumartist' => [
957
						'id' => (string)$album->getAlbumArtistId() ?: '0',
958
						'value' => $album->getAlbumArtistNameString($this->l10n)
959
					],
960
					'album' => [
961
						'id' => (string)$album->getId() ?: '0',
962
						'value' => $album->getNameString($this->l10n)
963
					],
964
					'url' => $this->createAmpacheActionUrl('download', $track->getId(), $auth),
965
					'time' => $track->getLength(),
966
					'year' => $track->getYear(),
967
					'track' => $track->getAdjustedTrackNumber(),
968
					'bitrate' => $track->getBitrate(),
969
					'mime' => $track->getMimetype(),
970
					'size' => $track->getSize(),
971
					'art' => $this->createCoverUrl($album, $auth),
972
					'rating' => 0,
973
					'preciserating' => 0,
974
				];
975
976
				$genreId = $track->getGenreId();
977
				if ($genreId !== null) {
978
					$result['tag'] = [[
979
						'id' => (string)$genreId,
980
						'value' => $track->getGenreNameString($this->l10n),
981
						'count' => 1
982
					]];
983
				}
984
				return $result;
985
			}, $tracks)
986
		]);
987
	}
988
989
	private function renderPlaylists($playlists) {
990
		return $this->ampacheResponse([
991
			'playlist' => \array_map(function($playlist) {
992
				return [
993
					'id' => (string)$playlist->getId(),
994
					'name' => $playlist->getName(),
995
					'owner' => $this->ampacheUser->getUserId(),
996
					'items' => $playlist->getTrackCount(),
997
					'type' => 'Private'
998
				];
999
			}, $playlists)
1000
		]);
1001
	}
1002
1003
	private function renderTags($genres) {
1004
		return $this->ampacheResponse([
1005
			'tag' => \array_map(function($genre) {
1006
				return [
1007
					'id' => (string)$genre->getId(),
1008
					'name' => $genre->getNameString($this->l10n),
1009
					'albums' => $genre->getAlbumCount(),
1010
					'artists' => $genre->getArtistCount(),
1011
					'songs' => $genre->getTrackCount(),
1012
					'videos' => 0,
1013
					'playlists' => 0,
1014
					'stream' => 0
1015
				];
1016
			}, $genres)
1017
		]);
1018
	}
1019
1020
	private function renderSongsIndex($tracks) {
1021
		return $this->ampacheResponse([
1022
			'song' => \array_map(function($track) {
1023
				return [
1024
					'id' => (string)$track->getId(),
1025
					'title' => $track->getTitle(),
1026
					'name' => $track->getTitle(),
1027
					'artist' => [
1028
						'id' => (string)$track->getArtistId(),
1029
						'value' => $track->getArtistNameString($this->l10n)
1030
					],
1031
					'album' => [
1032
						'id' => (string)$track->getAlbumId(),
1033
						'value' => $track->getAlbumNameString($this->l10n)
1034
					]
1035
				];
1036
			}, $tracks)
1037
		]);
1038
	}
1039
1040
	private function renderAlbumsIndex($albums) {
1041
		return $this->ampacheResponse([
1042
			'album' => \array_map(function($album) {
1043
				return [
1044
					'id' => (string)$album->getId(),
1045
					'name' => $album->getNameString($this->l10n),
1046
					'artist' => [
1047
						'id' => (string)$album->getAlbumArtistId(),
1048
						'value' => $album->getAlbumArtistNameString($this->l10n)
1049
					]
1050
				];
1051
			}, $albums)
1052
		]);
1053
	}
1054
1055
	private function renderArtistsIndex($artists) {
1056
		return $this->ampacheResponse([
1057
			'artist' => \array_map(function($artist) {
1058
				$userId = $this->ampacheUser->getUserId();
1059
				$albums = $this->albumBusinessLayer->findAllByArtist($artist->getId(), $userId);
1060
1061
				return [
1062
					'id' => (string)$artist->getId(),
1063
					'name' => $artist->getNameString($this->l10n),
1064
					'album' => \array_map(function($album) {
1065
						return [
1066
							'id' => (string)$album->getId(),
1067
							'value' => $album->getNameString($this->l10n)
1068
						];
1069
					}, $albums)
1070
				];
1071
			}, $artists)
1072
		]);
1073
	}
1074
1075
	private function renderPlaylistsIndex($playlists) {
1076
		return $this->ampacheResponse([
1077
			'playlist' => \array_map(function($playlist) {
1078
				return [
1079
					'id' => (string)$playlist->getId(),
1080
					'name' => $playlist->getName(),
1081
					'playlisttrack' => $playlist->getTrackIdsAsArray()
1082
				];
1083
			}, $playlists)
1084
		]);
1085
	}
1086
1087
	private function renderEntityIds($entities) {
1088
		return $this->ampacheResponse(['id' => Util::extractIds($entities)]);
1089
	}
1090
1091
	/**
1092
	 * Array is considered to be "indexed" if its first element has numerical key.
1093
	 * Empty array is considered to be "indexed".
1094
	 * @param array $array
1095
	 */
1096
	private static function arrayIsIndexed(array $array) {
1097
		reset($array);
1098
		return empty($array) || \is_int(key($array));
1099
	}
1100
1101
	/**
1102
	 * The JSON API has some asymmetries with the XML API. This function makes the needed
1103
	 * translations for the result content before it is converted into JSON. 
1104
	 * @param array $content
1105
	 * @return array
1106
	 */
1107
	private static function prepareResultForJsonApi($content) {
1108
		// In all responses returning an array of library entities, the root node is anonymous.
1109
		// Unwrap the outermost array if it is an associative array with a single array-type value.
1110
		if (\count($content) === 1 && !self::arrayIsIndexed($content)
1111
				&& \is_array(\current($content)) && self::arrayIsIndexed(\current($content))) {
1112
			$content = \array_pop($content);
1113
		}
1114
1115
		// The key 'value' has a special meaning on XML responses, as it makes the corresponding value
1116
		// to be treated as text content of the parent element. In the JSON API, these are mostly
1117
		// substituted with property 'name', but error responses use the property 'message', instead.
1118
		if (\array_key_exists('error', $content)) {
1119
			$content = Util::convertArrayKeys($content, ['value' => 'message']);
1120
		} else {
1121
			$content = Util::convertArrayKeys($content, ['value' => 'name']);
1122
		}
1123
		return $content;
1124
	}
1125
1126
	/**
1127
	 * The XML API has some asymmetries with the JSON API. This function makes the needed
1128
	 * translations for the result content before it is converted into XML. 
1129
	 * @param array $content
1130
	 * @return array
1131
	 */
1132
	private static function prepareResultForXmlApi($content) {
1133
		\reset($content);
1134
		$firstKey = \key($content);
1135
1136
		// all 'entity list' kind of responses shall have the (deprecated) total_count element
1137
		if ($firstKey == 'song' || $firstKey == 'album' || $firstKey == 'artist'
1138
				|| $firstKey == 'playlist' || $firstKey == 'tag') {
1139
			$content = ['total_count' => \count($content[$firstKey])] + $content;
1140
		}
1141
1142
		// for some bizarre reason, the 'id' arrays have 'index' attributes in the XML format
1143
		if ($firstKey == 'id') {
1144
			$content['id'] = \array_map(function($id, $index) {
1145
				return ['index' => $index, 'value' => $id];
1146
			}, $content['id'], \array_keys($content['id']));
1147
		}
1148
1149
		return ['root' => $content];
1150
	}
1151
1152
	private function getRequiredParam($paramName) {
1153
		$param = $this->request->getParam($paramName);
1154
1155
		if ($param === null) {
1156
			throw new AmpacheException("Required parameter '$paramName' missing", 400);
1157
		}
1158
1159
		return $param;
1160
	}
1161
}
1162
1163
/**
1164
 * Adapter class which acts like the Playlist class for the purpose of 
1165
 * AmpacheController::renderPlaylists but contains all the track of the user. 
1166
 */
1167
class AmpacheController_AllTracksPlaylist {
1168
1169
	private $user;
1170
	private $trackBusinessLayer;
1171
	private $l10n;
1172
1173
	public function __construct($user, $trackBusinessLayer, $l10n) {
1174
		$this->user = $user;
1175
		$this->trackBusinessLayer = $trackBusinessLayer;
1176
		$this->l10n = $l10n;
1177
	}
1178
1179
	public function getId() {
1180
		return AmpacheController::ALL_TRACKS_PLAYLIST_ID;
1181
	}
1182
1183
	public function getName() {
1184
		return $this->l10n->t('All tracks');
1185
	}
1186
1187
	public function getTrackCount() {
1188
		return $this->trackBusinessLayer->count($this->user);
1189
	}
1190
}
1191