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

AmpacheController::renderArtistsIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 13
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 17
ccs 0
cts 10
cp 0
crap 2
rs 9.8333
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