Passed
Push — master ( a4260e...ab8f86 )
by Pauli
13:01
created

PlaylistApiController::generate()   C

Complexity

Conditions 14
Paths 2

Size

Total Lines 45
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 34
c 0
b 0
f 0
nc 2
nop 9
dl 0
loc 45
rs 6.2666

How to fix   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 declare(strict_types=1);
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 - 2024
13
 */
14
15
namespace OCA\Music\Controller;
16
17
use OCP\AppFramework\Controller;
18
use OCP\AppFramework\Http;
19
use OCP\AppFramework\Http\JSONResponse;
20
21
use OCP\Files\Folder;
22
use OCP\IConfig;
23
use OCP\IRequest;
24
use OCP\IURLGenerator;
25
26
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
27
use OCA\Music\AppFramework\Core\Logger;
28
use OCA\Music\BusinessLayer\AlbumBusinessLayer;
29
use OCA\Music\BusinessLayer\ArtistBusinessLayer;
30
use OCA\Music\BusinessLayer\GenreBusinessLayer;
31
use OCA\Music\BusinessLayer\PlaylistBusinessLayer;
32
use OCA\Music\BusinessLayer\TrackBusinessLayer;
33
use OCA\Music\Http\ErrorResponse;
34
use OCA\Music\Http\FileResponse;
35
use OCA\Music\Utility\CoverHelper;
36
use OCA\Music\Utility\PlaylistFileService;
37
use OCA\Music\Utility\Util;
38
39
class PlaylistApiController extends Controller {
40
	private $urlGenerator;
41
	private $playlistBusinessLayer;
42
	private $artistBusinessLayer;
43
	private $albumBusinessLayer;
44
	private $trackBusinessLayer;
45
	private $genreBusinessLayer;
46
	private $coverHelper;
47
	private $playlistFileService;
48
	private $userId;
49
	private $userFolder;
50
	private $configManager;
51
	private $logger;
52
53
	public function __construct(string $appname,
54
								IRequest $request,
55
								IURLGenerator $urlGenerator,
56
								PlaylistBusinessLayer $playlistBusinessLayer,
57
								ArtistBusinessLayer $artistBusinessLayer,
58
								AlbumBusinessLayer $albumBusinessLayer,
59
								TrackBusinessLayer $trackBusinessLayer,
60
								GenreBusinessLayer $genreBusinessLayer,
61
								CoverHelper $coverHelper,
62
								PlaylistFileService $playlistFileService,
63
								string $userId,
64
								Folder $userFolder,
65
								IConfig $configManager,
66
								Logger $logger) {
67
		parent::__construct($appname, $request);
68
		$this->urlGenerator = $urlGenerator;
69
		$this->playlistBusinessLayer = $playlistBusinessLayer;
70
		$this->artistBusinessLayer = $artistBusinessLayer;
71
		$this->albumBusinessLayer = $albumBusinessLayer;
72
		$this->trackBusinessLayer = $trackBusinessLayer;
73
		$this->genreBusinessLayer = $genreBusinessLayer;
74
		$this->coverHelper = $coverHelper;
75
		$this->playlistFileService = $playlistFileService;
76
		$this->userId = $userId;
77
		$this->userFolder = $userFolder;
78
		$this->configManager = $configManager;
79
		$this->logger = $logger;
80
	}
81
82
	/**
83
	 * lists all playlists
84
	 *
85
	 * @NoAdminRequired
86
	 * @NoCSRFRequired
87
	 */
88
	public function getAll() {
89
		$playlists = $this->playlistBusinessLayer->findAll($this->userId);
90
		return Util::arrayMapMethod($playlists, 'toAPI', [$this->urlGenerator]);
91
	}
92
93
	/**
94
	 * creates a playlist
95
	 *
96
	 * @NoAdminRequired
97
	 * @NoCSRFRequired
98
	 */
99
	public function create(string $name, /*mixed*/ $trackIds, ?string $comment=null) {
100
		$playlist = $this->playlistBusinessLayer->create($name, $this->userId);
101
102
		// add trackIds and comment to the newly created playlist if provided
103
		if (!empty($trackIds)) {
104
			$playlist = $this->playlistBusinessLayer->addTracks(
105
					self::toIntArray($trackIds), $playlist->getId(), $this->userId);
106
		}
107
		if ($comment !== null) {
108
			$playlist = $this->playlistBusinessLayer->setComment($comment, $playlist->getId(), $this->userId);
109
		}
110
111
		return $playlist->toAPI($this->urlGenerator);
112
	}
113
114
	/**
115
	 * deletes a playlist
116
	 * @param  int $id playlist ID
117
	 *
118
	 * @NoAdminRequired
119
	 * @NoCSRFRequired
120
	 */
121
	public function delete(int $id) {
122
		$this->playlistBusinessLayer->delete($id, $this->userId);
123
		return [];
124
	}
125
126
	/**
127
	 * lists a single playlist
128
	 * @param  int $id playlist ID
129
	 *
130
	 * @NoAdminRequired
131
	 * @NoCSRFRequired
132
	 */
133
	public function get(int $id, $fulltree) {
134
		try {
135
			$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
136
137
			$fulltree = \filter_var($fulltree, FILTER_VALIDATE_BOOLEAN);
138
			if ($fulltree) {
139
				return $this->toFullTree($playlist);
140
			} else {
141
				return $playlist->toAPI($this->urlGenerator);
142
			}
143
		} catch (BusinessLayerException $ex) {
144
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
145
		}
146
	}
147
148
	private function toFullTree($playlist) {
149
		$trackIds = $playlist->getTrackIdsAsArray();
150
		$tracks = $this->trackBusinessLayer->findById($trackIds, $this->userId);
151
		$this->albumBusinessLayer->injectAlbumsToTracks($tracks, $this->userId);
152
153
		$result = $playlist->toAPI($this->urlGenerator);
154
		unset($result['trackIds']);
155
		$result['tracks'] = Util::arrayMapMethod($tracks, 'toAPI', [$this->urlGenerator]);
156
157
		return $result;
158
	}
159
160
	/**
161
	 * generate a smart playlist according to the given rules
162
	 *
163
	 * @NoAdminRequired
164
	 * @NoCSRFRequired
165
	 */
166
	public function generate(
167
			?bool $useLatestParams, ?string $history, ?string $genres, ?string $artists,
168
			?int $fromYear, ?int $toYear, ?string $favorite=null, int $size=100, string $historyStrict='false') {
169
170
		if ($useLatestParams) {
171
			$history = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_history') ?: null;
172
			$genres = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_genres') ?: null;
173
			$artists = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_artists') ?: null;
174
			$fromYear = (int)$this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_from_year') ?: null;
175
			$toYear = (int)$this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_to_year') ?: null;
176
			$favorite = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_favorite') ?: null;
177
			$size = (int)$this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_size', 100);
178
			$historyStrict = $this->configManager->getUserValue($this->userId, $this->appName, 'smartlist_history_strict', 'false');
179
		} else {
180
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_history', $history ?? '');
181
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_genres', $genres ?? '');
182
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_artists', $artists ?? '');
183
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_from_year', (string)$fromYear);
184
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_to_year', (string)$toYear);
185
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_favorite', $favorite ?? '');
186
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_size', (string)$size);
187
			$this->configManager->setUserValue($this->userId, $this->appName, 'smartlist_history_strict', $historyStrict);
188
		}
189
		$historyStrict = \filter_var($historyStrict, FILTER_VALIDATE_BOOLEAN);
190
191
		// ensure the artists and genres contain only valid IDs
192
		$genres = $this->genreBusinessLayer->findAllIds($this->userId, self::toIntArray($genres));
193
		$artists = $this->artistBusinessLayer->findAllIds($this->userId, self::toIntArray($artists));
194
195
		$playlist = $this->playlistBusinessLayer->generate(
196
				$history, $historyStrict, $genres, $artists, $fromYear, $toYear, $favorite, $size, $this->userId);
197
		$result = $playlist->toAPI($this->urlGenerator);
198
199
		$result['params'] = [
200
			'history' => $history ?: null,
201
			'historyStrict' => $historyStrict,
202
			'genres' => \implode(',', $genres) ?: null,
203
			'artists' => \implode(',', $artists) ?: null,
204
			'fromYear' => $fromYear ?: null,
205
			'toYear' => $toYear ?: null,
206
			'favorite' => $favorite ?: null,
207
			'size' => $size
208
		];
209
210
		return $result;
211
	}
212
213
	/**
214
	 * get cover image for a playlist
215
	 * @param int $id playlist ID
216
	 *
217
	 * @NoAdminRequired
218
	 * @NoCSRFRequired
219
	 */
220
	public function getCover(int $id) {
221
		try {
222
			$playlist = $this->playlistBusinessLayer->find($id, $this->userId);
223
			$cover = $this->coverHelper->getCover($playlist, $this->userId, $this->userFolder);
224
225
			if ($cover !== null) {
226
				return new FileResponse($cover);
227
			} else {
228
				return new ErrorResponse(Http::STATUS_NOT_FOUND, 'The playlist has no cover art');
229
			}
230
		} catch (BusinessLayerException $ex) {
231
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
232
		}
233
	}
234
235
	/**
236
	 * update a playlist
237
	 * @param int $id playlist ID
238
	 *
239
	 * @NoAdminRequired
240
	 * @NoCSRFRequired
241
	 */
242
	public function update(int $id, string $name = null, string $comment = null, string $trackIds = null) {
243
		$result = null;
244
		if ($name !== null) {
245
			$result = $this->modifyPlaylist('rename', [$name, $id, $this->userId]);
246
		}
247
		if ($comment !== null) {
248
			$result = $this->modifyPlaylist('setComment', [$comment, $id, $this->userId]);
249
		}
250
		if ($trackIds!== null) {
251
			$result = $this->modifyPlaylist('setTracks', [self::toIntArray($trackIds), $id, $this->userId]);
252
		}
253
		if ($result === null) {
254
			$result = new ErrorResponse(Http::STATUS_BAD_REQUEST, "at least one of the args ['name', 'comment', 'trackIds'] must be given");
255
		}
256
		return $result;
257
	}
258
259
	/**
260
	 * add tracks to a playlist
261
	 * @param  int $id playlist ID
262
	 *
263
	 * @NoAdminRequired
264
	 * @NoCSRFRequired
265
	 */
266
	public function addTracks(int $id, $trackIds) {
267
		return $this->modifyPlaylist('addTracks', [self::toIntArray($trackIds), $id, $this->userId]);
268
	}
269
270
	/**
271
	 * removes tracks from a playlist
272
	 * @param  int $id playlist ID
273
	 *
274
	 * @NoAdminRequired
275
	 * @NoCSRFRequired
276
	 */
277
	public function removeTracks(int $id, $indices) {
278
		return $this->modifyPlaylist('removeTracks', [self::toIntArray($indices), $id, $this->userId]);
279
	}
280
281
	/**
282
	 * moves single track on playlist to a new position
283
	 * @param  int $id playlist ID
284
	 *
285
	 * @NoAdminRequired
286
	 * @NoCSRFRequired
287
	 */
288
	public function reorder(int $id, $fromIndex, $toIndex) {
289
		return $this->modifyPlaylist('moveTrack',
290
				[$fromIndex, $toIndex, $id, $this->userId]);
291
	}
292
293
	/**
294
	 * export the playlist to a file
295
	 * @param int $id playlist ID
296
	 * @param string $path parent folder path
297
	 * @param string $oncollision action to take on file name collision,
298
	 *								supported values:
299
	 *								- 'overwrite' The existing file will be overwritten
300
	 *								- 'keepboth' The new file is named with a suffix to make it unique
301
	 *								- 'abort' (default) The operation will fail
302
	 *
303
	 * @NoAdminRequired
304
	 * @NoCSRFRequired
305
	 */
306
	public function exportToFile(int $id, string $path, string $oncollision) {
307
		try {
308
			$exportedFilePath = $this->playlistFileService->exportToFile(
309
					$id, $this->userId, $this->userFolder, $path, $oncollision);
310
			return new JSONResponse(['wrote_to_file' => $exportedFilePath]);
311
		} catch (BusinessLayerException $ex) {
312
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist not found');
313
		} catch (\OCP\Files\NotFoundException $ex) {
314
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'folder not found');
315
		} catch (\RuntimeException $ex) {
316
			return new ErrorResponse(Http::STATUS_CONFLICT, $ex->getMessage());
317
		} catch (\OCP\Files\NotPermittedException $ex) {
318
			return new ErrorResponse(Http::STATUS_FORBIDDEN, 'user is not allowed to write to the target file');
319
		}
320
	}
321
322
	/**
323
	 * import playlist contents from a file
324
	 * @param int $id playlist ID
325
	 * @param string $filePath path of the file to import
326
	 *
327
	 * @NoAdminRequired
328
	 * @NoCSRFRequired
329
	 */
330
	public function importFromFile(int $id, string $filePath) {
331
		try {
332
			$result = $this->playlistFileService->importFromFile($id, $this->userId, $this->userFolder, $filePath);
333
			$result['playlist'] = $result['playlist']->toAPI($this->urlGenerator);
334
			return $result;
335
		} catch (BusinessLayerException $ex) {
336
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist not found');
337
		} catch (\OCP\Files\NotFoundException $ex) {
338
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist file not found');
339
		} catch (\UnexpectedValueException $ex) {
340
			return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage());
341
		}
342
	}
343
344
	/**
345
	 * read and parse a playlist file
346
	 * @param int $fileId ID of the file to parse
347
	 *
348
	 * @NoAdminRequired
349
	 * @NoCSRFRequired
350
	 */
351
	public function parseFile(int $fileId) {
352
		try {
353
			$result = $this->playlistFileService->parseFile($fileId, $this->userFolder);
354
355
			// Make a lookup table of all the file IDs in the user library to avoid having to run
356
			// a DB query for each track in the playlist to check if it is in the library. This
357
			// could make a difference in case of a huge playlist.
358
			$libFileIds = $this->trackBusinessLayer->findAllFileIds($this->userId);
359
			$libFileIds = \array_flip($libFileIds);
360
361
			$bogusUrlId = -1;
362
363
			// compose the final result
364
			$result['files'] = \array_map(function ($fileInfo) use ($libFileIds, &$bogusUrlId) {
365
				if (isset($fileInfo['url'])) {
366
					$fileInfo['id'] = $bogusUrlId--;
367
					$fileInfo['mimetype'] = null;
368
					$fileInfo['external'] = true;
369
					return $fileInfo;
370
				} else {
371
					$file = $fileInfo['file'];
372
					return [
373
						'id' => $file->getId(),
374
						'name' => $file->getName(),
375
						'path' => $this->userFolder->getRelativePath($file->getParent()->getPath()),
376
						'mimetype' => $file->getMimeType(),
377
						'caption' => $fileInfo['caption'],
378
						'in_library' => isset($libFileIds[$file->getId()]),
379
						'external' => false
380
					];
381
				}
382
			}, $result['files']);
383
			return new JSONResponse($result);
384
		} catch (\OCP\Files\NotFoundException $ex) {
385
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist file not found');
386
		} catch (\UnexpectedValueException $ex) {
387
			return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage());
388
		}
389
	}
390
391
	/**
392
	 * Modify playlist by calling a supplied method from PlaylistBusinessLayer
393
	 * @param string $funcName  Name of a function to call from PlaylistBusinessLayer
394
	 * @param array $funcParams Parameters to pass to the function 'funcName'
395
	 * @return JSONResponse JSON representation of the modified playlist
396
	 */
397
	private function modifyPlaylist(string $funcName, array $funcParams) : JSONResponse {
398
		try {
399
			$playlist = \call_user_func_array([$this->playlistBusinessLayer, $funcName], $funcParams);
400
			return new JSONResponse($playlist->toAPI($this->urlGenerator));
401
		} catch (BusinessLayerException $ex) {
402
			return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage());
403
		}
404
	}
405
406
	/**
407
	 * Get integer array passed as parameter to the Playlist API
408
	 * @param string|int|null $listAsString Comma-separated integer values in string, or a single integer
409
	 * @return int[]
410
	 */
411
	private static function toIntArray(/*mixed*/ $listAsString) : array {
412
		if ($listAsString === null || $listAsString === '') {
413
			return [];
414
		} else {
415
			return \array_map('intval', \explode(',', (string)$listAsString));
416
		}
417
	}
418
}
419