PlaylistApiController::generate()   C
last analyzed

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