Passed
Push — feature/909_Ampache_API_improv... ( a4bfe6...150565 )
by Pauli
03:48
created

PlaylistApiController::generate()   C

Complexity

Conditions 12
Paths 2

Size

Total Lines 35
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 27
nc 2
nop 7
dl 0
loc 35
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

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:

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