Passed
Push — master ( 64b798...0837cd )
by Pauli
01:55
created

AmpacheController::dispatch()   D

Complexity

Conditions 29
Paths 29

Size

Total Lines 69
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 870

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 29
eloc 62
nc 29
nop 9
dl 0
loc 69
ccs 0
cts 63
cp 0
crap 870
rs 4.1666
c 3
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
21
22
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
23
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
24
use \OCA\Music\AppFramework\Core\Logger;
25
use \OCA\Music\Middleware\AmpacheException;
26
27
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
28
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
29
use \OCA\Music\BusinessLayer\GenreBusinessLayer;
30
use \OCA\Music\BusinessLayer\Library;
31
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
32
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
33
34
use \OCA\Music\Db\Album;
35
use \OCA\Music\Db\AmpacheUserMapper;
36
use \OCA\Music\Db\AmpacheSession;
37
use \OCA\Music\Db\AmpacheSessionMapper;
38
use \OCA\Music\Db\Artist;
39
use \OCA\Music\Db\SortBy;
40
41
use \OCA\Music\Http\ErrorResponse;
42
use \OCA\Music\Http\FileResponse;
43
use \OCA\Music\Http\XMLResponse;
44
45
use \OCA\Music\Utility\AmpacheUser;
46
use \OCA\Music\Utility\CoverHelper;
47
use \OCA\Music\Utility\Random;
48
use \OCA\Music\Utility\Util;
49
50
class AmpacheController extends Controller {
51
	private $ampacheUserMapper;
52
	private $ampacheSessionMapper;
53
	private $albumBusinessLayer;
54
	private $artistBusinessLayer;
55
	private $genreBusinessLayer;
56
	private $playlistBusinessLayer;
57
	private $trackBusinessLayer;
58
	private $library;
59
	private $ampacheUser;
60
	private $urlGenerator;
61
	private $rootFolder;
62
	private $l10n;
63
	private $coverHelper;
64
	private $random;
65
	private $logger;
66
	private $jsonMode;
67
68
	const SESSION_EXPIRY_TIME = 6000;
69
	const ALL_TRACKS_PLAYLIST_ID = 10000000;
70
	const API_VERSION = 400001;
71
	const API_MIN_COMPATIBLE_VERSION = 350001;
72
73
	public function __construct($appname,
74
								IRequest $request,
75
								$l10n,
76
								IURLGenerator $urlGenerator,
77
								AmpacheUserMapper $ampacheUserMapper,
78
								AmpacheSessionMapper $ampacheSessionMapper,
79
								AlbumBusinessLayer $albumBusinessLayer,
80
								ArtistBusinessLayer $artistBusinessLayer,
81
								GenreBusinessLayer $genreBusinessLayer,
82
								PlaylistBusinessLayer $playlistBusinessLayer,
83
								TrackBusinessLayer $trackBusinessLayer,
84
								Library $library,
85
								AmpacheUser $ampacheUser,
86
								$rootFolder,
87
								CoverHelper $coverHelper,
88
								Random $random,
89
								Logger $logger) {
90
		parent::__construct($appname, $request);
91
92
		$this->ampacheUserMapper = $ampacheUserMapper;
93
		$this->ampacheSessionMapper = $ampacheSessionMapper;
94
		$this->albumBusinessLayer = $albumBusinessLayer;
95
		$this->artistBusinessLayer = $artistBusinessLayer;
96
		$this->genreBusinessLayer = $genreBusinessLayer;
97
		$this->playlistBusinessLayer = $playlistBusinessLayer;
98
		$this->trackBusinessLayer = $trackBusinessLayer;
99
		$this->library = $library;
100
		$this->urlGenerator = $urlGenerator;
101
		$this->l10n = $l10n;
102
103
		// used to share user info with middleware
104
		$this->ampacheUser = $ampacheUser;
105
106
		// used to deliver actual media file
107
		$this->rootFolder = $rootFolder;
108
109
		$this->coverHelper = $coverHelper;
110
		$this->random = $random;
111
		$this->logger = $logger;
112
	}
113
114
	public function setJsonMode($useJsonMode) {
115
		$this->jsonMode = $useJsonMode;
116
	}
117
118
	public function ampacheResponse($content) {
119
		if ($this->jsonMode) {
120
			return new JSONResponse(self::prepareResultForJsonApi($content));
121
		} else {
122
			return new XMLResponse(self::prepareResultForXmlApi($content), ['id', 'index', 'count', 'code']);
123
		}
124
	}
125
126
	/**
127
	 * @NoAdminRequired
128
	 * @PublicPage
129
	 * @NoCSRFRequired
130
	 */
131
	public function xmlApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id) {
132
		// differentation between xmlApi and jsonApi is made already by the middleware
133
		return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id);
134
	}
135
136
	/**
137
	 * @NoAdminRequired
138
	 * @PublicPage
139
	 * @NoCSRFRequired
140
	 */
141
	public function jsonApi($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id) {
142
		// differentation between xmlApi and jsonApi is made already by the middleware
143
		return $this->dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id);
144
	}
145
146
	protected function dispatch($action, $user, $timestamp, $auth, $filter, $exact, $limit, $offset, $id) {
147
		$this->logger->log("Ampache action '$action' requested", 'debug');
148
149
		$limit = self::validateLimitOrOffset($limit);
150
		$offset = self::validateLimitOrOffset($offset);
151
152
		switch ($action) {
153
			case 'handshake':
154
				return $this->handshake($user, $timestamp, $auth);
155
			case '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 'flag':
200
				return $this->flag();
201
			case 'download':
202
				return $this->download($id); // args 'type' and 'format' not supported
203
			case 'stream':
204
				return $this->stream($id, $offset); // args 'type', 'bitrate', 'format', and 'length' not supported
205
206
			# non Ampache API actions
207
			case '_get_album_cover':
208
				return $this->_get_album_cover($id);
209
			case '_get_artist_cover':
210
				return $this->_get_artist_cover($id);
211
		}
212
213
		$this->logger->log("Unsupported Ampache action '$action' requested", 'warn');
214
		throw new AmpacheException('Action not supported', 405);
215
	}
216
217
	/***********************
218
	 * Ampahce API methods *
219
	 ***********************/
220
221
	protected function handshake($user, $timestamp, $auth) {
222
		$currentTime = \time();
223
		$expiryDate = $currentTime + self::SESSION_EXPIRY_TIME;
224
225
		$this->checkHandshakeTimestamp($timestamp, $currentTime);
226
		$this->checkHandshakeAuthentication($user, $timestamp, $auth);
227
		$token = $this->startNewSession($user, $expiryDate);
228
229
		$currentTimeFormated = \date('c', $currentTime);
230
		$expiryDateFormated = \date('c', $expiryDate);
231
232
		return $this->ampacheResponse([
233
			'auth' => $token,
234
			'api' => self::API_VERSION,
235
			'update' => $currentTimeFormated,
236
			'add' => $currentTimeFormated,
237
			'clean' => $currentTimeFormated,
238
			'songs' => $this->trackBusinessLayer->count($user),
239
			'artists' => $this->artistBusinessLayer->count($user),
240
			'albums' => $this->albumBusinessLayer->count($user),
241
			'playlists' => $this->playlistBusinessLayer->count($user) + 1, // +1 for "All tracks"
242
			'session_expire' => $expiryDateFormated,
243
			'tags' => $this->genreBusinessLayer->count($user),
244
			'videos' => 0,
245
			'catalogs' => 0
246
		]);
247
	}
248
249
	protected function ping($auth) {
250
		$response = [
251
			// TODO: 'server' => Music app version,
252
			'version' => self::API_VERSION,
253
			'compatible' => self::API_MIN_COMPATIBLE_VERSION
254
		];
255
256
		if (!empty($auth)) {
257
			$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

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