Total Complexity | 57 |
Total Lines | 375 |
Duplicated Lines | 0 % |
Changes | 7 | ||
Bugs | 0 | Features | 0 |
Complex classes like RadioApiController 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 RadioApiController, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
39 | class RadioApiController extends Controller { |
||
40 | private IConfig $config; |
||
41 | private IURLGenerator $urlGenerator; |
||
42 | private RadioStationBusinessLayer $businessLayer; |
||
43 | private RadioService $service; |
||
44 | private StreamTokenService $tokenService; |
||
45 | private PlaylistFileService $playlistFileService; |
||
46 | private string $userId; |
||
47 | private IRootFolder $rootFolder; |
||
48 | private Logger $logger; |
||
49 | |||
50 | public function __construct(string $appName, |
||
51 | IRequest $request, |
||
52 | IConfig $config, |
||
53 | IURLGenerator $urlGenerator, |
||
54 | RadioStationBusinessLayer $businessLayer, |
||
55 | RadioService $service, |
||
56 | StreamTokenService $tokenService, |
||
57 | PlaylistFileService $playlistFileService, |
||
58 | ?string $userId, |
||
59 | IRootFolder $rootFolder, |
||
60 | Logger $logger) { |
||
61 | parent::__construct($appName, $request); |
||
62 | $this->config = $config; |
||
63 | $this->urlGenerator = $urlGenerator; |
||
64 | $this->businessLayer = $businessLayer; |
||
65 | $this->service = $service; |
||
66 | $this->tokenService = $tokenService; |
||
67 | $this->playlistFileService = $playlistFileService; |
||
68 | $this->userId = $userId ?? ''; // ensure non-null to satisfy Scrutinizer; may be null when resolveStreamUrl used on public share |
||
69 | $this->rootFolder = $rootFolder; |
||
70 | $this->logger = $logger; |
||
71 | } |
||
72 | |||
73 | /** |
||
74 | * lists all radio stations |
||
75 | * |
||
76 | * @NoAdminRequired |
||
77 | * @NoCSRFRequired |
||
78 | */ |
||
79 | public function getAll() : JSONResponse { |
||
80 | $stations = $this->businessLayer->findAll($this->userId); |
||
81 | return new JSONResponse( |
||
82 | \array_map(fn($s) => $s->toApi(), $stations) |
||
83 | ); |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * creates a station |
||
88 | * |
||
89 | * @NoAdminRequired |
||
90 | * @NoCSRFRequired |
||
91 | */ |
||
92 | public function create(?string $name, ?string $streamUrl, ?string $homeUrl) : JSONResponse { |
||
93 | if ($streamUrl === null) { |
||
94 | return new ErrorResponse(Http::STATUS_BAD_REQUEST, "Mandatory argument 'streamUrl' not given"); |
||
95 | } |
||
96 | |||
97 | try { |
||
98 | $station = $this->businessLayer->create($this->userId, $name, $streamUrl, $homeUrl); |
||
99 | return new JSONResponse($station->toApi()); |
||
100 | } catch (\DomainException $ex) { |
||
101 | return new ErrorResponse(Http::STATUS_BAD_REQUEST, $ex->getMessage()); |
||
102 | } |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * deletes a station |
||
107 | * |
||
108 | * @NoAdminRequired |
||
109 | * @NoCSRFRequired |
||
110 | */ |
||
111 | public function delete(int $id) : JSONResponse { |
||
112 | try { |
||
113 | $this->businessLayer->delete($id, $this->userId); |
||
114 | return new JSONResponse([]); |
||
115 | } catch (BusinessLayerException $ex) { |
||
116 | return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage()); |
||
117 | } |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * get a single radio station |
||
122 | * |
||
123 | * @NoAdminRequired |
||
124 | * @NoCSRFRequired |
||
125 | */ |
||
126 | public function get(int $id) : JSONResponse { |
||
127 | try { |
||
128 | $station = $this->businessLayer->find($id, $this->userId); |
||
129 | return new JSONResponse($station->toApi()); |
||
130 | } catch (BusinessLayerException $ex) { |
||
131 | return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage()); |
||
132 | } |
||
133 | } |
||
134 | |||
135 | /** |
||
136 | * update a station |
||
137 | * |
||
138 | * @NoAdminRequired |
||
139 | * @NoCSRFRequired |
||
140 | */ |
||
141 | public function update(int $id, ?string $name = null, ?string $streamUrl = null, ?string $homeUrl = null) : JSONResponse { |
||
142 | if ($name === null && $streamUrl === null && $homeUrl === null) { |
||
143 | return new ErrorResponse(Http::STATUS_BAD_REQUEST, "at least one of the args ['name', 'streamUrl', 'homeUrl'] must be given"); |
||
144 | } |
||
145 | |||
146 | try { |
||
147 | $station = $this->businessLayer->updateStation($id, $this->userId, $name, $streamUrl, $homeUrl); |
||
148 | return new JSONResponse($station->toApi()); |
||
149 | } catch (BusinessLayerException $ex) { |
||
150 | return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage()); |
||
151 | } catch (\DomainException $ex) { |
||
152 | return new ErrorResponse(Http::STATUS_BAD_REQUEST, $ex->getMessage()); |
||
153 | } |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * export all radio stations to a file |
||
158 | * |
||
159 | * @param string $name target file name |
||
160 | * @param string $path parent folder path |
||
161 | * @param string $oncollision action to take on file name collision, |
||
162 | * supported values: |
||
163 | * - 'overwrite' The existing file will be overwritten |
||
164 | * - 'keepboth' The new file is named with a suffix to make it unique |
||
165 | * - 'abort' (default) The operation will fail |
||
166 | * |
||
167 | * @NoAdminRequired |
||
168 | * @NoCSRFRequired |
||
169 | */ |
||
170 | public function exportAllToFile(string $name, string $path, string $oncollision='abort') : JSONResponse { |
||
171 | try { |
||
172 | $userFolder = $this->rootFolder->getUserFolder($this->userId); |
||
173 | $exportedFilePath = $this->playlistFileService->exportRadioStationsToFile( |
||
174 | $this->userId, $userFolder, $path, $name, $oncollision); |
||
175 | return new JSONResponse(['wrote_to_file' => $exportedFilePath]); |
||
176 | } catch (\OCP\Files\NotFoundException $ex) { |
||
177 | return new ErrorResponse(Http::STATUS_NOT_FOUND, 'folder not found'); |
||
178 | } catch (FileExistsException $ex) { |
||
179 | return new ErrorResponse(Http::STATUS_CONFLICT, 'file already exists', ['path' => $ex->getPath(), 'suggested_name' => $ex->getAltName()]); |
||
180 | } catch (\OCP\Files\NotPermittedException $ex) { |
||
181 | return new ErrorResponse(Http::STATUS_FORBIDDEN, 'user is not allowed to write to the target file'); |
||
182 | } |
||
183 | } |
||
184 | |||
185 | /** |
||
186 | * import radio stations from a file |
||
187 | * @param string $filePath path of the file to import |
||
188 | * |
||
189 | * @NoAdminRequired |
||
190 | * @NoCSRFRequired |
||
191 | */ |
||
192 | public function importFromFile(string $filePath) : JSONResponse { |
||
193 | try { |
||
194 | $userFolder = $this->rootFolder->getUserFolder($this->userId); |
||
195 | $result = $this->playlistFileService->importRadioStationsFromFile($this->userId, $userFolder, $filePath); |
||
196 | $result['stations'] = \array_map(fn($s) => $s->toApi(), $result['stations']); |
||
197 | return new JSONResponse($result); |
||
198 | } catch (\OCP\Files\NotFoundException $ex) { |
||
199 | return new ErrorResponse(Http::STATUS_NOT_FOUND, 'playlist file not found'); |
||
200 | } catch (\UnexpectedValueException $ex) { |
||
201 | return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage()); |
||
202 | } |
||
203 | } |
||
204 | |||
205 | /** |
||
206 | * reset all the radio stations of the user |
||
207 | * |
||
208 | * @NoAdminRequired |
||
209 | * @NoCSRFRequired |
||
210 | */ |
||
211 | public function resetAll() : JSONResponse { |
||
212 | $this->businessLayer->deleteAll($this->userId); |
||
213 | return new JSONResponse(['success' => true]); |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * get metadata for a channel |
||
218 | * |
||
219 | * @NoAdminRequired |
||
220 | * @NoCSRFRequired |
||
221 | */ |
||
222 | public function getChannelInfo(int $id, ?string $type=null) : JSONResponse { |
||
223 | try { |
||
224 | $station = $this->businessLayer->find($id, $this->userId); |
||
225 | $streamUrl = $station->getStreamUrl(); |
||
226 | |||
227 | switch ($type) { |
||
228 | case 'icy': |
||
229 | $metadata = $this->service->readIcyMetadata($streamUrl, 3, 5); |
||
230 | break; |
||
231 | case 'shoutcast-v1': |
||
232 | $metadata = $this->service->readShoutcastV1Metadata($streamUrl); |
||
233 | break; |
||
234 | case 'shoutcast-v2': |
||
235 | $metadata = $this->service->readShoutcastV2Metadata($streamUrl); |
||
236 | break; |
||
237 | case 'icecast': |
||
238 | $metadata = $this->service->readIcecastMetadata($streamUrl); |
||
239 | break; |
||
240 | default: |
||
241 | $metadata = $this->service->readIcyMetadata($streamUrl, 3, 5) |
||
242 | ?? $this->service->readShoutcastV2Metadata($streamUrl) |
||
243 | ?? $this->service->readIcecastMetadata($streamUrl) |
||
244 | ?? $this->service->readShoutcastV1Metadata($streamUrl); |
||
245 | break; |
||
246 | } |
||
247 | |||
248 | return new JSONResponse($metadata); |
||
249 | } catch (BusinessLayerException $ex) { |
||
250 | return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage()); |
||
251 | } |
||
252 | } |
||
253 | |||
254 | /** |
||
255 | * get stream URL for a radio station |
||
256 | * |
||
257 | * @NoAdminRequired |
||
258 | * @NoCSRFRequired |
||
259 | */ |
||
260 | public function stationStreamUrl(int $id) : JSONResponse { |
||
261 | try { |
||
262 | $station = $this->businessLayer->find($id, $this->userId); |
||
263 | $streamUrl = $station->getStreamUrl(); |
||
264 | $resolved = $this->service->resolveStreamUrl($streamUrl); |
||
265 | $relayEnabled = $this->streamRelayEnabled(); |
||
266 | if ($relayEnabled && !$resolved['hls']) { |
||
267 | $resolved['url'] = $this->urlGenerator->linkToRoute('music.radioApi.stationStream', ['id' => $id]); |
||
268 | } |
||
269 | return new JSONResponse($resolved); |
||
270 | } catch (BusinessLayerException $ex) { |
||
271 | return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage()); |
||
272 | } |
||
273 | } |
||
274 | |||
275 | /** |
||
276 | * get audio stream for a radio station |
||
277 | * |
||
278 | * @NoAdminRequired |
||
279 | * @NoCSRFRequired |
||
280 | */ |
||
281 | public function stationStream(int $id) : Response { |
||
282 | try { |
||
283 | $station = $this->businessLayer->find($id, $this->userId); |
||
284 | $streamUrl = $station->getStreamUrl(); |
||
285 | $resolved = $this->service->resolveStreamUrl($streamUrl); |
||
286 | if ($this->streamRelayEnabled()) { |
||
287 | return new RelayStreamResponse($resolved['url']); |
||
288 | } else { |
||
289 | return new RedirectResponse($resolved['url']); |
||
290 | } |
||
291 | } catch (BusinessLayerException $ex) { |
||
292 | return new ErrorResponse(Http::STATUS_NOT_FOUND, $ex->getMessage()); |
||
293 | } |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * get the actual stream URL from the given public URL |
||
298 | * |
||
299 | * Available without login since no user data is handled and this may be used on link-shared folder. |
||
300 | * |
||
301 | * @PublicPage |
||
302 | * @NoCSRFRequired |
||
303 | */ |
||
304 | public function resolveStreamUrl(string $url, ?string $token) : JSONResponse { |
||
305 | $url = \rawurldecode($url); |
||
306 | |||
307 | if ($token === null) { |
||
308 | return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'a security token must be passed'); |
||
309 | } elseif (!$this->tokenService->urlTokenIsValid($url, \rawurldecode($token))) { |
||
310 | return new ErrorResponse(Http::STATUS_UNAUTHORIZED, 'the security token is invalid'); |
||
311 | } else { |
||
312 | $resolved = $this->service->resolveStreamUrl($url); |
||
313 | $relayEnabled = $this->streamRelayEnabled(); |
||
314 | if ($relayEnabled && !$resolved['hls']) { |
||
315 | $token = $this->tokenService->tokenForUrl($resolved['url']); |
||
316 | $resolved['url'] = $this->urlGenerator->linkToRoute('music.radioApi.streamFromUrl', |
||
317 | ['url' => \rawurlencode($resolved['url']), 'token' => \rawurlencode($token)]); |
||
318 | } |
||
319 | return new JSONResponse($resolved); |
||
320 | } |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * create a relayed stream for the given URL if relaying enabled; otherwise just redirect to the URL |
||
325 | * |
||
326 | * @PublicPage |
||
327 | * @NoCSRFRequired |
||
328 | */ |
||
329 | public function streamFromUrl(string $url, ?string $token) : Response { |
||
340 | } |
||
341 | } |
||
342 | |||
343 | /** |
||
344 | * get manifest of a HLS stream |
||
345 | * |
||
346 | * This fetches the manifest file from the given URL and returns a modified version of it. |
||
347 | * The front-end can't easily stream directly from the original source because of the Content-Security-Policy. |
||
348 | * |
||
349 | * @PublicPage |
||
350 | * @NoCSRFRequired |
||
351 | */ |
||
352 | public function hlsManifest(string $url, ?string $token) : Response { |
||
369 | } |
||
370 | } |
||
371 | |||
372 | /** |
||
373 | * get one segment of a HLS stream |
||
374 | * |
||
375 | * The segment is fetched from the given URL and relayed as such to the client. |
||
376 | * |
||
377 | * @PublicPage |
||
378 | * @NoCSRFRequired |
||
379 | */ |
||
380 | public function hlsSegment(string $url, ?string $token) : Response { |
||
397 | } |
||
398 | } |
||
399 | |||
400 | private function hlsEnabled() : bool { |
||
401 | $enabled = (bool)$this->config->getSystemValue('music.enable_radio_hls', true); |
||
402 | if ($this->userId === '') { |
||
403 | $enabled = (bool)$this->config->getSystemValue('music.enable_radio_hls_on_share', $enabled); |
||
404 | } |
||
405 | return $enabled; |
||
406 | } |
||
407 | |||
408 | private function streamRelayEnabled() : bool { |
||
414 | } |
||
415 | } |
||
416 |