| 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 |