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