Total Complexity | 50 |
Total Lines | 364 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like PlaylistApiController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use PlaylistApiController, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
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() { |
||
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 { |
||
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 { |
||
404 | } |
||
405 | } |
||
406 | } |
||
407 |