Total Complexity | 57 |
Total Lines | 397 |
Duplicated Lines | 0 % |
Changes | 3 | ||
Bugs | 0 | Features | 1 |
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); |
||
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 { |
||
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 { |
||
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 { |
||
439 | } |
||
440 | } |
||
441 | } |
||
442 |