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