| Total Complexity | 57 |
| Total Lines | 399 |
| Duplicated Lines | 0 % |
| Changes | 2 | ||
| 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( |
||
| 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 { |
||
| 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 { |
||
| 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 { |
||
| 441 | } |
||
| 442 | } |
||
| 443 | } |
||
| 444 |