Passed
Push — master ( 859657...64b798 )
by Pauli
01:53
created

AmpacheController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 39
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 16
nc 1
nop 17
dl 0
loc 39
ccs 0
cts 17
cp 0
crap 2
rs 9.7333
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
883
				$userId = $this->ampacheUser->getUserId();
884
				$albums = $this->albumBusinessLayer->findAllByArtist($artist->getId(), $userId);
885
886
				return [
887
					'id' => (string)$artist->getId(),
888
					'name' => $artist->getNameString($this->l10n),
889
					'album' => \array_map(function($album) {
890
						return [
891
							'id' => (string)$album->getId(),
892
							'value' => $album->getNameString($this->l10n)
893
						];
894
					}, $albums)
895
				];
896
			}, $artists)
897
		]);
898
	}
899
900
	private function renderPlaylistsIndex($playlists) {
901
		return $this->ampacheResponse([
902
			'playlist' => \array_map(function($playlist) {
903
				return [
904
					'id' => (string)$playlist->getId(),
905
					'name' => $playlist->getName(),
906
					'playlisttrack' => $playlist->getTrackIdsAsArray()
907
				];
908
			}, $playlists)
909
		]);
910
	}
911
912
	private function renderEntityIds($entities) {
913
		return $this->ampacheResponse(['id' => Util::extractIds($entities)]);
914
	}
915
916
	/**
917
	 * Array is considered to be "indexed" if its first element has numerical key.
918
	 * Empty array is considered to be "indexed".
919
	 * @param array $array
920
	 */
921
	private static function arrayIsIndexed(array $array) {
922
		reset($array);
923
		return empty($array) || \is_int(key($array));
924
	}
925
926
	/**
927
	 * The JSON API has some asymmetries with the XML API. This function makes the needed
928
	 * translations for the result content before it is converted into JSON. 
929
	 * @param array $content
930
	 * @return array
931
	 */
932
	private static function prepareResultForJsonApi($content) {
933
		// In all responses returning an array of library entities, the root node is anonymous.
934
		// Unwrap the outermost array if it is an associative array with a single array-type value.
935
		if (\count($content) === 1 && !self::arrayIsIndexed($content)
936
				&& \is_array(\current($content)) && self::arrayIsIndexed(\current($content))) {
937
			$content = \array_pop($content);
938
		}
939
940
		// The key 'value' has a special meaning on XML responses, as it makes the corresponding value
941
		// to be treated as text content of the parent element. In the JSON API, these are mostly
942
		// substituted with property 'name', but error responses use the property 'message', instead.
943
		if (\array_key_exists('error', $content)) {
944
			$content = Util::convertArrayKeys($content, ['value' => 'message']);
945
		} else {
946
			$content = Util::convertArrayKeys($content, ['value' => 'name']);
947
		}
948
		return $content;
949
	}
950
951
	/**
952
	 * The XML API has some asymmetries with the JSON API. This function makes the needed
953
	 * translations for the result content before it is converted into XML. 
954
	 * @param array $content
955
	 * @return array
956
	 */
957
	private static function prepareResultForXmlApi($content) {
958
		\reset($content);
959
		$firstKey = \key($content);
960
961
		// all 'entity list' kind of responses shall have the (deprecated) total_count element
962
		if ($firstKey == 'song' || $firstKey == 'album' || $firstKey == 'artist'
963
				|| $firstKey == 'playlist' || $firstKey == 'tag') {
964
			$content = ['total_count' => \count($content[$firstKey])] + $content;
965
		}
966
967
		// for some bizarre reason, the 'id' arrays have 'index' attributes in the XML format
968
		if ($firstKey == 'id') {
969
			$content['id'] = \array_map(function($id, $index) {
970
				return ['index' => $index, 'value' => $id];
971
			}, $content['id'], \array_keys($content['id']));
972
		}
973
974
		return ['root' => $content];
975
	}
976
977
	private function getRequiredParam($paramName) {
978
		$param = $this->request->getParam($paramName);
979
980
		if ($param === null) {
981
			throw new AmpacheException("Required parameter '$paramName' missing", 400);
982
		}
983
984
		return $param;
985
	}
986
}
987
988
/**
989
 * Adapter class which acts like the Playlist class for the purpose of 
990
 * AmpacheController::renderPlaylists but contains all the track of the user. 
991
 */
992
class AmpacheController_AllTracksPlaylist {
993
994
	private $user;
995
	private $trackBusinessLayer;
996
	private $l10n;
997
998
	public function __construct($user, $trackBusinessLayer, $l10n) {
999
		$this->user = $user;
1000
		$this->trackBusinessLayer = $trackBusinessLayer;
1001
		$this->l10n = $l10n;
1002
	}
1003
1004
	public function getId() {
1005
		return AmpacheController::ALL_TRACKS_PLAYLIST_ID;
1006
	}
1007
1008
	public function getName() {
1009
		return $this->l10n->t('All tracks');
1010
	}
1011
1012
	public function getTrackCount() {
1013
		return $this->trackBusinessLayer->count($this->user);
1014
	}
1015
}
1016