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