Passed
Push — master ( 28aace...5efe1b )
by Pauli
04:20
created

AmpacheController::xmlApi()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 9
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2017 - 2020
13
 */
14
15
namespace OCA\Music\Controller;
16
17
use \OCP\AppFramework\Controller;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Controller was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use \OCP\AppFramework\Http\JSONResponse;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\JSONResponse was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use \OCP\IRequest;
0 ignored issues
show
Bug introduced by
The type OCP\IRequest was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use \OCP\IURLGenerator;
0 ignored issues
show
Bug introduced by
The type OCP\IURLGenerator was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
22
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
23
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
24
use \OCA\Music\AppFramework\Core\Logger;
25
use \OCA\Music\Middleware\AmpacheException;
26
27
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
28
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
29
use \OCA\Music\BusinessLayer\GenreBusinessLayer;
30
use \OCA\Music\BusinessLayer\Library;
31
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
32
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
33
34
use \OCA\Music\Db\Album;
35
use \OCA\Music\Db\AmpacheUserMapper;
36
use \OCA\Music\Db\AmpacheSession;
37
use \OCA\Music\Db\AmpacheSessionMapper;
38
use \OCA\Music\Db\Artist;
39
use \OCA\Music\Db\SortBy;
40
41
use \OCA\Music\Http\ErrorResponse;
42
use \OCA\Music\Http\FileResponse;
43
use \OCA\Music\Http\XMLResponse;
44
45
use \OCA\Music\Utility\AmpacheUser;
46
use \OCA\Music\Utility\CoverHelper;
47
use \OCA\Music\Utility\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', '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 'ping':
156
				return $this->ping($auth);
157
			case 'get_indexes':
158
				return $this->get_indexes($filter, $limit, $offset);
159
			case 'artists':
160
				return $this->artists($filter, $exact, $limit, $offset, $auth);
161
			case 'artist':
162
				return $this->artist($filter, $auth);
163
			case 'artist_albums':
164
				return $this->artist_albums($filter, $auth);
165
			case 'album_songs':
166
				return $this->album_songs($filter, $auth);
167
			case 'albums':
168
				return $this->albums($filter, $exact, $limit, $offset, $auth);
169
			case 'album':
170
				return $this->album($filter, $auth);
171
			case 'artist_songs':
172
				return $this->artist_songs($filter, $auth);
173
			case 'songs':
174
				return $this->songs($filter, $exact, $limit, $offset, $auth);
175
			case 'song':
176
				return $this->song($filter, $auth);
177
			case 'search_songs':
178
				return $this->search_songs($filter, $auth);
179
			case 'playlists':
180
				return $this->playlists($filter, $exact, $limit, $offset);
181
			case 'playlist':
182
				return $this->playlist($filter);
183
			case 'playlist_songs':
184
				return $this->playlist_songs($filter, $limit, $offset, $auth);
185
			case 'playlist_generate':
186
				return $this->playlist_generate($filter, $limit, $offset, $auth);
187
			case 'tags':
188
				return $this->tags($filter, $exact, $limit, $offset);
189
			case 'tag':
190
				return $this->tag($filter);
191
			case 'tag_artists':
192
				return $this->tag_artists($filter, $limit, $offset, $auth);
193
			case 'tag_albums':
194
				return $this->tag_albums($filter, $limit, $offset, $auth);
195
			case 'tag_songs':
196
				return $this->tag_songs($filter, $limit, $offset, $auth);
197
			case 'download':
198
				return $this->download($id); // args 'type' and 'format' not supported
199
			case 'stream':
200
				return $this->stream($id, $offset); // args 'type', 'bitrate', 'format', and 'length' not supported
201
202
			# non Ampache API actions
203
			case '_get_album_cover':
204
				return $this->_get_album_cover($id);
205
			case '_get_artist_cover':
206
				return $this->_get_artist_cover($id);
207
		}
208
209
		$this->logger->log("Unsupported Ampache action '$action' requested", 'warn');
210
		throw new AmpacheException('Action not supported', 405);
211
	}
212
213
	/***********************
214
	 * Ampahce API methods *
215
	 ***********************/
216
217
	protected function handshake($user, $timestamp, $auth) {
218
		$currentTime = \time();
219
		$expiryDate = $currentTime + self::SESSION_EXPIRY_TIME;
220
221
		$this->checkHandshakeTimestamp($timestamp, $currentTime);
222
		$this->checkHandshakeAuthentication($user, $timestamp, $auth);
223
		$token = $this->startNewSession($user, $expiryDate);
224
225
		$currentTimeFormated = \date('c', $currentTime);
226
		$expiryDateFormated = \date('c', $expiryDate);
227
228
		return $this->ampacheResponse([
229
			'auth' => $token,
230
			'api' => self::API_VERSION,
231
			'update' => $currentTimeFormated,
232
			'add' => $currentTimeFormated,
233
			'clean' => $currentTimeFormated,
234
			'songs' => $this->trackBusinessLayer->count($user),
235
			'artists' => $this->artistBusinessLayer->count($user),
236
			'albums' => $this->albumBusinessLayer->count($user),
237
			'playlists' => $this->playlistBusinessLayer->count($user) + 1, // +1 for "All tracks"
238
			'session_expire' => $expiryDateFormated,
239
			'tags' => $this->genreBusinessLayer->count($user),
240
			'videos' => 0,
241
			'catalogs' => 0
242
		]);
243
	}
244
245
	protected function ping($auth) {
246
		$response = [
247
			// TODO: 'server' => Music app version,
248
			'version' => self::API_VERSION,
249
			'compatible' => self::API_MIN_COMPATIBLE_VERSION
250
		];
251
252
		if (!empty($auth)) {
253
			$response['session_expire'] = \date('c', $this->ampacheSessionMapper->getExpiryTime($auth));
0 ignored issues
show
Bug introduced by
It seems like $this->ampacheSessionMapper->getExpiryTime($auth) can also be of type false; however, parameter $timestamp of date() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

253
			$response['session_expire'] = \date('c', /** @scrutinizer ignore-type */ $this->ampacheSessionMapper->getExpiryTime($auth));
Loading history...
254
		}
255
256
		return $this->ampacheResponse($response);
257
	}
258
259
	protected function get_indexes($filter, $limit, $offset) {
260
		// TODO: args $add, $update
261
		$type = $this->getRequiredParam('type');
262
		$userId = $this->ampacheUser->getUserId();
0 ignored issues
show
Unused Code introduced by
The assignment to $userId is dead and can be removed.
Loading history...
263
264
		switch ($type) {
265
			case 'playlist':
266
				$playlists = $this->findEntities($this->playlistBusinessLayer, $filter, false, $limit, $offset);
267
				return $this->renderPlaylistsIndex($playlists);
268
			case 'song':
269
				$tracks = $this->findEntities($this->trackBusinessLayer, $filter, false, $limit, $offset);
270
				return $this->renderSongsIndex($tracks);
271
			case 'album': // TODO
272
			case 'artist': // TODO
273
			default:
274
				throw new AmpacheException("Unsupported type $type", 400);
275
		}
276
	}
277
278
	protected function artists($filter, $exact, $limit, $offset, $auth) {
279
		$artists = $this->findEntities($this->artistBusinessLayer, $filter, $exact, $limit, $offset);
280
		return $this->renderArtists($artists, $auth);
281
	}
282
283
	protected function artist($artistId, $auth) {
284
		$userId = $this->ampacheUser->getUserId();
285
		$artist = $this->artistBusinessLayer->find($artistId, $userId);
286
		return $this->renderArtists([$artist], $auth);
287
	}
288
289
	protected function artist_albums($artistId, $auth) {
290
		$userId = $this->ampacheUser->getUserId();
291
		$albums = $this->albumBusinessLayer->findAllByArtist($artistId, $userId);
292
		return $this->renderAlbums($albums, $auth);
293
	}
294
295
	protected function artist_songs($artistId, $auth) {
296
		$userId = $this->ampacheUser->getUserId();
297
		$tracks = $this->trackBusinessLayer->findAllByArtist($artistId, $userId);
298
		$this->injectAlbum($tracks);
299
		return $this->renderSongs($tracks, $auth);
300
	}
301
302
	protected function album_songs($albumId, $auth) {
303
		$userId = $this->ampacheUser->getUserId();
304
305
		$album = $this->albumBusinessLayer->find($albumId, $userId);
306
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $userId);
307
		$this->injectAlbum($tracks, $album);
308
309
		return $this->renderSongs($tracks, $auth);
310
	}
311
312
	protected function song($trackId, $auth) {
313
		$userId = $this->ampacheUser->getUserId();
314
		$track = $this->trackBusinessLayer->find($trackId, $userId);
315
		$trackInArray = [$track];
316
		$this->injectAlbum($trackInArray);
317
		return $this->renderSongs($trackInArray, $auth);
318
	}
319
320
	protected function songs($filter, $exact, $limit, $offset, $auth) {
321
322
		// optimized handling for fetching the whole library
323
		// note: the ordering of the songs differs between these two cases
324
		if (empty($filter) && !$limit && !$offset) {
325
			$tracks = $this->getAllTracks();
326
		}
327
		// general case
328
		else {
329
			$tracks = $this->findEntities($this->trackBusinessLayer, $filter, $exact, $limit, $offset);
330
			$this->injectAlbum($tracks);
331
		}
332
333
		return $this->renderSongs($tracks, $auth);
334
	}
335
336
	protected function search_songs($filter, $auth) {
337
		$userId = $this->ampacheUser->getUserId();
338
		$tracks = $this->trackBusinessLayer->findAllByNameRecursive($filter, $userId);
339
		$this->injectAlbum($tracks);
340
		return $this->renderSongs($tracks, $auth);
341
	}
342
343
	protected function albums($filter, $exact, $limit, $offset, $auth) {
344
		$albums = $this->findEntities($this->albumBusinessLayer, $filter, $exact, $limit, $offset);
345
		return $this->renderAlbums($albums, $auth);
346
	}
347
348
	protected function album($albumId, $auth) {
349
		$userId = $this->ampacheUser->getUserId();
350
		$album = $this->albumBusinessLayer->find($albumId, $userId);
351
		return $this->renderAlbums([$album], $auth);
352
	}
353
354
	protected function playlists($filter, $exact, $limit, $offset) {
355
		$userId = $this->ampacheUser->getUserId();
356
		$playlists = $this->findEntities($this->playlistBusinessLayer, $filter, $exact, $limit, $offset);
357
358
		// append "All tracks" if not searching by name, and it is not off-limit
359
		$allTracksIndex = $this->playlistBusinessLayer->count($userId);
360
		if (empty($filter) && self::indexIsWithinOffsetAndLimit($allTracksIndex, $offset, $limit)) {
361
			$playlists[] = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n);
362
		}
363
364
		return $this->renderPlaylists($playlists);
365
	}
366
367
	protected function playlist($listId) {
368
		$userId = $this->ampacheUser->getUserId();
369
		if ($listId == self::ALL_TRACKS_PLAYLIST_ID) {
370
			$playlist = new AmpacheController_AllTracksPlaylist($userId, $this->trackBusinessLayer, $this->l10n);
371
		} else {
372
			$playlist = $this->playlistBusinessLayer->find($listId, $userId);
373
		}
374
		return $this->renderPlaylists([$playlist]);
375
	}
376
377
	protected function playlist_songs($listId, $limit, $offset, $auth) {
378
		if ($listId == self::ALL_TRACKS_PLAYLIST_ID) {
379
			$playlistTracks = $this->getAllTracks();
380
			$playlistTracks = \array_slice($playlistTracks, $offset, $limit);
381
		}
382
		else {
383
			$userId = $this->ampacheUser->getUserId();
384
			$playlistTracks = $this->playlistBusinessLayer->getPlaylistTracks($listId, $userId, $limit, $offset);
385
			$this->injectAlbum($playlistTracks);
386
		}
387
		return $this->renderSongs($playlistTracks, $auth);
388
	}
389
390
	protected function playlist_generate($filter, $limit, $offset, $auth) {
391
		$mode = $this->request->getParam('mode', 'random');
392
		$album = $this->request->getParam('album');
393
		$artist = $this->request->getParam('artist');
394
		$flag = $this->request->getParam('flag');
395
		$format = $this->request->getParam('format', 'song');
396
397
		$tracks = $this->findEntities($this->trackBusinessLayer, $filter, false); // $limit and $offset are applied later
398
399
		// filter the found tracks according to the additional requirements
400
		if ($album !== null) {
401
			$tracks = \array_filter($tracks, function($track) use ($album) {
402
				return ($track->getAlbumId() == $album);
403
			});
404
		}
405
		if ($artist !== null) {
406
			$tracks = \array_filter($tracks, function($track) use ($artist) {
407
				return ($track->getArtistId() == $artist);
408
			});
409
		}
410
		if ($flag == 1) {
411
			$tracks = \array_filter($tracks, function($track) {
412
				return ($track->getStarred() !== null);
413
			});
414
		}
415
		// After filtering, there may be "holes" between the array indices. Reindex the array.
416
		$tracks = \array_values($tracks);
417
418
		if ($mode == 'random') {
419
			$userId = $this->ampacheUser->getUserId();
420
			$indices = $this->random->getIndices(\count($tracks), $offset, $limit, $userId, 'ampache_tracks');
421
			$tracks = Util::arrayMultiGet($tracks, $indices);
422
		} else { // 'recent', 'forgotten', 'unplayed'
423
			throw new AmpacheException("Mode '$mode' is not supported", 400);
424
		}
425
426
		if ($format == 'song') {
427
			$this->injectAlbum($tracks);
428
			return $this->renderSongs($tracks, $auth);
429
		} elseif ($format == 'index') {
430
			return $this->renderSongsIndex($tracks);
431
		} else {
432
			throw new AmpacheException("Format '$format' is not supported", 400);
433
		}
434
	}
435
436
	protected function tags($filter, $exact, $limit, $offset) {
437
		$genres = $this->findEntities($this->genreBusinessLayer, $filter, $exact, $limit, $offset);
438
		return $this->renderTags($genres);
439
	}
440
441
	protected function tag($tagId) {
442
		$userId = $this->ampacheUser->getUserId();
443
		$genre = $this->genreBusinessLayer->find($tagId, $userId);
444
		return $this->renderTags([$genre]);
445
	}
446
447
	protected function tag_artists($genreId, $limit, $offset, $auth) {
448
		$userId = $this->ampacheUser->getUserId();
449
		$artists = $this->artistBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
450
		return $this->renderArtists($artists, $auth);
451
	}
452
453
	protected function tag_albums($genreId, $limit, $offset, $auth) {
454
		$userId = $this->ampacheUser->getUserId();
455
		$albums = $this->albumBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
456
		return $this->renderAlbums($albums, $auth);
457
	}
458
459
	protected function tag_songs($genreId, $limit, $offset, $auth) {
460
		$userId = $this->ampacheUser->getUserId();
461
		$tracks = $this->trackBusinessLayer->findAllByGenre($genreId, $userId, $limit, $offset);
462
		$this->injectAlbum($tracks);
463
		return $this->renderSongs($tracks, $auth);
464
	}
465
466
	protected function download($trackId) {
467
		$userId = $this->ampacheUser->getUserId();
468
469
		try {
470
			$track = $this->trackBusinessLayer->find($trackId, $userId);
471
		} catch (BusinessLayerException $e) {
472
			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...
473
		}
474
475
		$files = $this->rootFolder->getUserFolder($userId)->getById($track->getFileId());
476
477
		if (\count($files) === 1) {
478
			return new FileResponse($files[0]);
479
		} else {
480
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
481
		}
482
	}
483
484
	protected function stream($trackId, $offset) {
485
		// This is just a dummy implementation. We don't support transcoding or streaming
486
		// from a time offset.
487
		// All the other unsupported arguments are just ignored, but a request with an offset
488
		// is responded with an error. This is becuase the client would probably work in an
489
		// unexpected way if it thinks it's streaming from offset but actually it is streaming
490
		// from the beginning of the file. Returning an error gives the client a chance to fallback
491
		// to other methods of seeking.
492
		if ($offset !== null) {
493
			throw new AmpacheException('Streaming with time offset is not supported', 400);
494
		}
495
496
		return $this->download($trackId);
497
	}
498
499
	/***************************************************************
500
	 * API methods which are not part of the Ampache specification *
501
	 ***************************************************************/
502
	protected function _get_album_cover($albumId) {
503
		return $this->getCover($albumId, $this->albumBusinessLayer);
504
	}
505
506
	protected function _get_artist_cover($artistId) {
507
		return $this->getCover($artistId, $this->artistBusinessLayer);
508
	}
509
510
511
	/********************
512
	 * Helper functions *
513
	 ********************/
514
515
	private function getCover($entityId, BusinessLayer $businessLayer) {
516
		$userId = $this->ampacheUser->getUserId();
517
		$userFolder = $this->rootFolder->getUserFolder($userId);
518
		$entity = $businessLayer->find($entityId, $userId);
519
520
		try {
521
			$coverData = $this->coverHelper->getCover($entity, $userId, $userFolder);
522
			if ($coverData !== null) {
523
				return new FileResponse($coverData);
524
			}
525
		} catch (BusinessLayerException $e) {
526
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity not found');
527
		}
528
529
		return new ErrorResponse(Http::STATUS_NOT_FOUND, 'entity has no cover');
530
	}
531
532
	private function checkHandshakeTimestamp($timestamp, $currentTime) {
533
		$providedTime = \intval($timestamp);
534
535
		if ($providedTime === 0) {
536
			throw new AmpacheException('Invalid Login - cannot parse time', 401);
537
		}
538
		if ($providedTime < ($currentTime - self::SESSION_EXPIRY_TIME)) {
539
			throw new AmpacheException('Invalid Login - session is outdated', 401);
540
		}
541
		// Allow the timestamp to be at maximum 10 minutes in the future. The client may use its
542
		// own system clock to generate the timestamp and that may differ from the server's time.
543
		if ($providedTime > $currentTime + 600) {
544
			throw new AmpacheException('Invalid Login - timestamp is in future', 401);
545
		}
546
	}
547
548
	private function checkHandshakeAuthentication($user, $timestamp, $auth) {
549
		$hashes = $this->ampacheUserMapper->getPasswordHashes($user);
550
551
		foreach ($hashes as $hash) {
552
			$expectedHash = \hash('sha256', $timestamp . $hash);
553
554
			if ($expectedHash === $auth) {
555
				return;
556
			}
557
		}
558
559
		throw new AmpacheException('Invalid Login - passphrase does not match', 401);
560
	}
561
562
	private function startNewSession($user, $expiryDate) {
563
		// this can cause collision, but it's just a temporary token
564
		$token = \md5(\uniqid(\rand(), true));
565
566
		// create new session
567
		$session = new AmpacheSession();
568
		$session->setUserId($user);
569
		$session->setToken($token);
570
		$session->setExpiry($expiryDate);
571
572
		// save session
573
		$this->ampacheSessionMapper->insert($session);
574
575
		return $token;
576
	}
577
578
	private function findEntities(BusinessLayer $businessLayer, $filter, $exact, $limit=null, $offset=null) {
579
		$userId = $this->ampacheUser->getUserId();
580
581
		if ($filter) {
582
			$fuzzy = !((boolean) $exact);
583
			return $businessLayer->findAllByName($filter, $userId, $fuzzy, $limit, $offset);
584
		} else {
585
			return $businessLayer->findAll($userId, SortBy::Name, $limit, $offset);
586
		}
587
	}
588
589
	/**
590
	 * Getting all tracks with this helper is more efficient than with `findEntities`
591
	 * followed by `injectAlbum`. This is because, under the hood, the albums
592
	 * are fetched with a single DB query instead of fetching each separately.
593
	 * 
594
	 * The result set is ordered first by artist and then by song title.
595
	 */
596
	private function getAllTracks() {
597
		$userId = $this->ampacheUser->getUserId();
598
		$tracks = $this->library->getTracksAlbumsAndArtists($userId)['tracks'];
599
		\usort($tracks, ['\OCA\Music\Db\Track', 'compareArtistAndTitle']);
600
		foreach ($tracks as $index => &$track) {
601
			$track->setNumberOnPlaylist($index + 1);
602
		}
603
		return $tracks;
604
	}
605
606
	private function createAmpacheActionUrl($action, $id, $auth) {
607
		$api = $this->jsonMode ? 'music.ampache.jsonApi' : 'music.ampache.xmlApi';
608
		return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute($api))
609
				. "?action=$action&id=$id&auth=$auth";
610
	}
611
612
	private function createCoverUrl($entity, $auth) {
613
		if ($entity instanceof Album) {
614
			$type = 'album';
615
		} elseif ($entity instanceof Artist) {
616
			$type = 'artist';
617
		} else {
618
			throw new AmpacheException('unexpeted entity type for cover image', 500);
619
		}
620
621
		if ($entity->getCoverFileId()) {
622
			return $this->createAmpacheActionUrl("_get_{$type}_cover", $entity->getId(), $auth);
623
		} else {
624
			return '';
625
		}
626
	}
627
628
	/**
629
	 * Any non-integer values and integer value 0 are converted to null to
630
	 * indicate "no limit" or "no offset".
631
	 * @param string $value
632
	 * @return integer|null
633
	 */
634
	private static function validateLimitOrOffset($value) {
635
		if (\ctype_digit(\strval($value)) && $value !== 0) {
636
			return \intval($value);
637
		} else {
638
			return null;
639
		}
640
	}
641
642
	/**
643
	 * @param int $index
644
	 * @param int|null $offset
645
	 * @param int|null $limit
646
	 * @return boolean
647
	 */
648
	private static function indexIsWithinOffsetAndLimit($index, $offset, $limit) {
649
		$offset = \intval($offset); // missing offset is interpreted as 0-offset
650
		return ($limit === null) || ($index >= $offset && $index < $offset + $limit);
651
	}
652
653
	private function renderArtists($artists, $auth) {
654
		$userId = $this->ampacheUser->getUserId();
655
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
656
657
		return $this->ampacheResponse([
658
			'artist' => \array_map(function($artist) use ($userId, $genreMap, $auth) {
659
				return [
660
					'id' => (string)$artist->getId(),
661
					'name' => $artist->getNameString($this->l10n),
662
					'albums' => $this->albumBusinessLayer->countByArtist($artist->getId()),
663
					'songs' => $this->trackBusinessLayer->countByArtist($artist->getId()),
664
					'art' => $this->createCoverUrl($artist, $auth),
665
					'rating' => 0,
666
					'preciserating' => 0,
667
					'tag' => \array_map(function($genreId) use ($genreMap) {
668
						return [
669
							'id' => (string)$genreId,
670
							'value' => $genreMap[$genreId]->getNameString($this->l10n),
671
							'count' => 1
672
						];
673
					}, $this->trackBusinessLayer->getGenresByArtistId($artist->getId(), $userId))
674
				];
675
			}, $artists)
676
		]);
677
	}
678
679
	private function renderAlbums($albums, $auth) {
680
		$userId = $this->ampacheUser->getUserId();
681
682
		$genreMap = Util::createIdLookupTable($this->genreBusinessLayer->findAll($userId));
683
684
		return $this->ampacheResponse([
685
			'album' => \array_map(function($album) use ($auth, $genreMap) {
686
				return [
687
					'id' => (string)$album->getId(),
688
					'name' => $album->getNameString($this->l10n),
689
					'artist' => [
690
						'id' => (string)$album->getAlbumArtistId(),
691
						'value' => $album->getAlbumArtistNameString($this->l10n)
692
					],
693
					'tracks' => $this->trackBusinessLayer->countByAlbum($album->getId()),
694
					'rating' => 0,
695
					'year' => $album->yearToAPI(),
696
					'art' => $this->createCoverUrl($album, $auth),
697
					'preciserating' => 0,
698
					'tag' => \array_map(function($genreId) use ($genreMap) {
699
						return [
700
							'id' => (string)$genreId,
701
							'value' => $genreMap[$genreId]->getNameString($this->l10n),
702
							'count' => 1
703
						];
704
					}, $album->getGenres())
705
				];
706
			}, $albums)
707
		]);
708
	}
709
710
	private function injectAlbum(&$tracks, $commonAlbum=null) {
711
		$userId = $this->ampacheUser->getUserId();
712
713
		foreach ($tracks as &$track) {
714
			if (!empty($commonAlbum)) {
715
				$track->setAlbum($commonAlbum);
716
			} else {
717
				$track->setAlbum($this->albumBusinessLayer->find($track->getAlbumId(), $userId));
718
			}
719
		}
720
	}
721
722
	private function renderSongs($tracks, $auth) {
723
		return $this->ampacheResponse([
724
			'song' => \array_map(function($track) use ($auth) {
725
				$album = $track->getAlbum();
726
727
				$result = [
728
					'id' => (string)$track->getId(),
729
					'title' => $track->getTitle(),
730
					'name' => $track->getTitle(),
731
					'artist' => [
732
						'id' => (string)$track->getArtistId(),
733
						'value' => $track->getArtistNameString($this->l10n)
734
					],
735
					'albumartist' => [
736
						'id' => (string)$album->getAlbumArtistId(),
737
						'value' => $album->getAlbumArtistNameString($this->l10n)
738
					],
739
					'album' => [
740
						'id' => (string)$album->getId(),
741
						'value' => $album->getNameString($this->l10n)
742
					],
743
					'url' => $this->createAmpacheActionUrl('download', $track->getId(), $auth),
744
					'time' => $track->getLength(),
745
					'year' => $track->getYear(),
746
					'track' => $track->getAdjustedTrackNumber(),
747
					'bitrate' => $track->getBitrate(),
748
					'mime' => $track->getMimetype(),
749
					'size' => $track->getSize(),
750
					'art' => $this->createCoverUrl($album, $auth),
751
					'rating' => 0,
752
					'preciserating' => 0,
753
				];
754
755
				$genreId = $track->getGenreId();
756
				if ($genreId !== null) {
757
					$result['tag'] = [[
758
						'id' => (string)$genreId,
759
						'value' => $track->getGenreNameString($this->l10n),
760
						'count' => 1
761
					]];
762
				}
763
				return $result;
764
			}, $tracks)
765
		]);
766
	}
767
768
	private function renderPlaylists($playlists) {
769
		return $this->ampacheResponse([
770
			'playlist' => \array_map(function($playlist) {
771
				return [
772
					'id' => (string)$playlist->getId(),
773
					'name' => $playlist->getName(),
774
					'owner' => $this->ampacheUser->getUserId(),
775
					'items' => $playlist->getTrackCount(),
776
					'type' => 'Private'
777
				];
778
			}, $playlists)
779
		]);
780
	}
781
782
	private function renderTags($genres) {
783
		return $this->ampacheResponse([
784
			'tag' => \array_map(function($genre) {
785
				return [
786
					'id' => (string)$genre->getId(),
787
					'name' => $genre->getNameString($this->l10n),
788
					'albums' => $genre->getAlbumCount(),
789
					'artists' => $genre->getArtistCount(),
790
					'songs' => $genre->getTrackCount(),
791
					'videos' => 0,
792
					'playlists' => 0,
793
					'stream' => 0
794
				];
795
			}, $genres)
796
		]);
797
	}
798
799
	private function renderSongsIndex($tracks) {
800
		return $this->ampacheResponse([
801
			'song' => \array_map(function($track) {
802
				return [
803
					'id' => (string)$track->getId(),
804
					'title' => $track->getTitle(),
805
					'name' => $track->getTitle(),
806
					'artist' => [
807
						'id' => (string)$track->getArtistId(),
808
						'value' => $track->getArtistNameString($this->l10n)
809
					],
810
					'album' => [
811
						'id' => (string)$track->getAlbumId(),
812
						'value' => $track->getAlbumNameString($this->l10n)
813
					]
814
				];
815
			}, $tracks)
816
		]);
817
	}
818
819
	private function renderPlaylistsIndex($playlists) {
820
		return $this->ampacheResponse([
821
			'playlist' => \array_map(function($playlist) {
822
				return [
823
					'id' => (string)$playlist->getId(),
824
					'name' => $playlist->getName(),
825
					'playlisttrack' => $playlist->getTrackIdsAsArray()
826
				];
827
			}, $playlists)
828
		]);
829
	}
830
831
	/**
832
	 * Array is considered to be "indexed" if its first element has numerical key.
833
	 * Empty array is considered to be "indexed".
834
	 * @param array $array
835
	 */
836
	private static function arrayIsIndexed(array $array) {
837
		reset($array);
838
		return empty($array) || \is_int(key($array));
839
	}
840
841
	/**
842
	 * The JSON API has some asymmetries with the XML API. This function makes the needed
843
	 * translations for the result content before it is converted into JSON. 
844
	 * @param array $content
845
	 * @return array
846
	 */
847
	private static function prepareResultForJsonApi($content) {
848
		// In all responses returning an array of library entities, the root node is anonymous.
849
		// Unwrap the outermost array if it is an associative array with a single array-type value.
850
		if (\count($content) === 1 && !self::arrayIsIndexed($content)
851
				&& \is_array(\current($content)) && self::arrayIsIndexed(\current($content))) {
852
			$content = \array_pop($content);
853
		}
854
855
		// The key 'value' has a special meaning on XML responses, as it makes the corresponding value
856
		// to be treated as text content of the parent element. In the JSON API, these are mostly
857
		// substituted with property 'name', but error responses use the property 'message', instead.
858
		if (\array_key_exists('error', $content)) {
859
			$content = Util::convertArrayKeys($content, ['value' => 'message']);
860
		} else {
861
			$content = Util::convertArrayKeys($content, ['value' => 'name']);
862
		}
863
		return $content;
864
	}
865
866
	/**
867
	 * The XML API has some asymmetries with the JSON API. This function makes the needed
868
	 * translations for the result content before it is converted into XML. 
869
	 * @param array $content
870
	 * @return array
871
	 */
872
	private static function prepareResultForXmlApi($content) {
873
		\reset($content);
874
		$firstKey = \key($content);
875
876
		// all 'entity list' kind of responses shall have the (deprecated) total_count element
877
		if ($firstKey == 'song' || $firstKey == 'album' || $firstKey == 'artist'
878
				|| $firstKey == 'playlist' || $firstKey == 'tag') {
879
			$content = ['total_count' => \count($content[$firstKey])] + $content;
880
		}
881
		return ['root' => $content];
882
	}
883
884
	private function getRequiredParam($paramName) {
885
		$param = $this->request->getParam($paramName);
886
887
		if ($param === null) {
888
			throw new AmpacheException("Required parameter '$paramName' missing", 400);
889
		}
890
891
		return $param;
892
	}
893
}
894
895
/**
896
 * Adapter class which acts like the Playlist class for the purpose of 
897
 * AmpacheController::renderPlaylists but contains all the track of the user. 
898
 */
899
class AmpacheController_AllTracksPlaylist {
900
901
	private $user;
902
	private $trackBusinessLayer;
903
	private $l10n;
904
905
	public function __construct($user, $trackBusinessLayer, $l10n) {
906
		$this->user = $user;
907
		$this->trackBusinessLayer = $trackBusinessLayer;
908
		$this->l10n = $l10n;
909
	}
910
911
	public function getId() {
912
		return AmpacheController::ALL_TRACKS_PLAYLIST_ID;
913
	}
914
915
	public function getName() {
916
		return $this->l10n->t('All tracks');
917
	}
918
919
	public function getTrackCount() {
920
		return $this->trackBusinessLayer->count($this->user);
921
	}
922
}
923