Passed
Push — master ( 124be7...f358a5 )
by Pauli
03:35 queued 16s
created

PodcastApiController::parseListFile()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 3
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2021 - 2025
11
 */
12
13
namespace OCA\Music\Controller;
14
15
use OCP\AppFramework\Controller;
16
use OCP\AppFramework\Http;
17
use OCP\AppFramework\Http\JSONResponse;
18
use OCP\AppFramework\Http\RedirectResponse;
19
20
use OCP\Files\IRootFolder;
21
22
use OCP\IConfig;
23
use OCP\IRequest;
24
use OCP\IURLGenerator;
25
26
use OCA\Music\AppFramework\Core\Logger;
27
use OCA\Music\AppFramework\Utility\FileExistsException;
28
use OCA\Music\Http\ErrorResponse;
29
use OCA\Music\Http\RelayStreamResponse;
30
use OCA\Music\Utility\PodcastService;
31
32
class PodcastApiController extends Controller {
33
	private IConfig $config;
34
	private IURLGenerator $urlGenerator;
35
	private IRootFolder $rootFolder;
36
	private PodcastService $podcastService;
37
	private string $userId;
38
	private Logger $logger;
39
40
	public function __construct(string $appname,
41
								IRequest $request,
42
								IConfig $config,
43
								IURLGenerator $urlGenerator,
44
								IRootFolder $rootFolder,
45
								PodcastService $podcastService,
46
								?string $userId,
47
								Logger $logger) {
48
		parent::__construct($appname, $request);
49
		$this->config = $config;
50
		$this->urlGenerator = $urlGenerator;
51
		$this->rootFolder = $rootFolder;
52
		$this->podcastService = $podcastService;
53
		$this->userId = $userId ?? ''; // ensure non-null to satisfy Scrutinizer; the null case should happen only when the user has already logged out
54
		$this->logger = $logger;
55
	}
56
57
	/**
58
	 * lists all podcasts
59
	 *
60
	 * @NoAdminRequired
61
	 * @NoCSRFRequired
62
	 */
63
	public function getAll() {
64
		$channels = $this->podcastService->getAllChannels($this->userId, /*$includeEpisodes=*/ true);
65
		return \array_map(fn($c) => $c->toApi($this->urlGenerator), $channels);
66
	}
67
68
	/**
69
	 * add a followed podcast
70
	 *
71
	 * @NoAdminRequired
72
	 * @NoCSRFRequired
73
	 */
74
	public function subscribe(?string $url) {
75
		if ($url === null) {
76
			return new ErrorResponse(Http::STATUS_BAD_REQUEST, "Mandatory argument 'url' not given");
77
		}
78
79
		$result = $this->podcastService->subscribe($url, $this->userId);
80
81
		switch ($result['status']) {
82
			case PodcastService::STATUS_OK:
83
				return $result['channel']->toApi($this->urlGenerator);
84
			case PodcastService::STATUS_INVALID_URL:
85
				return new ErrorResponse(Http::STATUS_BAD_REQUEST, "Invalid URL $url");
86
			case PodcastService::STATUS_INVALID_RSS:
87
				return new ErrorResponse(Http::STATUS_BAD_REQUEST, "The document at URL $url is not a valid podcast RSS feed");
88
			case PodcastService::STATUS_ALREADY_EXISTS:
89
				return new ErrorResponse(Http::STATUS_CONFLICT, 'User already has this podcast channel subscribed');
90
			default:
91
				return new ErrorResponse(Http::STATUS_INTERNAL_SERVER_ERROR, "Unexpected status code {$result['status']}");
92
		}
93
	}
94
95
	/**
96
	 * deletes a station
97
	 *
98
	 * @NoAdminRequired
99
	 * @NoCSRFRequired
100
	 */
101
	public function unsubscribe(int $id) {
102
		$status = $this->podcastService->unsubscribe($id, $this->userId);
103
104
		switch ($status) {
105
			case PodcastService::STATUS_OK:
106
				return new JSONResponse(['success' => true]);
107
			case PodcastService::STATUS_NOT_FOUND:
108
				return new ErrorResponse(Http::STATUS_NOT_FOUND);
109
			default:
110
				return new ErrorResponse(Http::STATUS_INTERNAL_SERVER_ERROR, "Unexpected status code $status");
111
		}
112
	}
113
114
	/**
115
	 * get a single podcast channel
116
	 *
117
	 * @NoAdminRequired
118
	 * @NoCSRFRequired
119
	 */
120
	public function get(int $id) {
121
		$channel = $this->podcastService->getChannel($id, $this->userId, /*includeEpisodes=*/ true);
122
123
		if ($channel !== null) {
124
			return $channel->toApi($this->urlGenerator);
125
		} else {
126
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
127
		}
128
	}
129
130
	/**
131
	 * get details for a podcast channel
132
	 *
133
	 * @NoAdminRequired
134
	 * @NoCSRFRequired
135
	 */
136
	public function channelDetails(int $id) {
137
		$channel = $this->podcastService->getChannel($id, $this->userId, /*includeEpisodes=*/ false);
138
139
		if ($channel !== null) {
140
			return $channel->detailsToApi($this->urlGenerator);
141
		} else {
142
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
143
		}
144
	}
145
146
	/**
147
	 * get details for a podcast episode
148
	 *
149
	 * @NoAdminRequired
150
	 * @NoCSRFRequired
151
	 */
152
	public function episodeDetails(int $id) {
153
		$episode = $this->podcastService->getEpisode($id, $this->userId);
154
155
		if ($episode !== null) {
156
			return $episode->detailsToApi();
157
		} else {
158
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
159
		}
160
	}
161
162
	/**
163
	 * get audio stream for a podcast episode
164
	 *
165
	 * @NoAdminRequired
166
	 * @NoCSRFRequired
167
	 */
168
	public function episodeStream(int $id) {
169
		$episode = $this->podcastService->getEpisode($id, $this->userId);
170
171
		if ($episode !== null) {
172
			$streamUrl = $episode->getStreamUrl();
173
			if ($streamUrl === null) {
174
				return new ErrorResponse(Http::STATUS_NOT_FOUND, "The podcast episode $id has no stream URL");
175
			} elseif ($this->config->getSystemValue('music.relay_podcast_stream', true)) {
176
				return new RelayStreamResponse($streamUrl);
177
			} else {
178
				return new RedirectResponse($streamUrl);
179
			}
180
		} else {
181
			return new ErrorResponse(Http::STATUS_NOT_FOUND);
182
		}
183
	}
184
185
	/**
186
	 * check a single channel for updates
187
	 * @param int $id Channel ID
188
	 * @param string|null $prevHash Previous content hash known by the client. If given, the result will tell
189
	 *								if the channel content has updated from this state. If omitted, the result
190
	 *								will tell if the channel changed from its previous server-known state.
191
	 *
192
	 * @NoAdminRequired
193
	 * @NoCSRFRequired
194
	 */
195
	public function updateChannel(int $id, ?string $prevHash) {
196
		$updateResult = $this->podcastService->updateChannel($id, $this->userId, $prevHash);
197
198
		$response = [
199
			'success' => ($updateResult['status'] === PodcastService::STATUS_OK),
200
			'updated' => $updateResult['updated']
201
		];
202
		if ($updateResult['updated']) {
203
			$response['channel'] = $updateResult['channel']->toApi($this->urlGenerator);
204
		}
205
206
		return new JSONResponse($response);
207
	}
208
209
	/**
210
	 * reset all the subscribed podcasts of the user
211
	 *
212
	 * @NoAdminRequired
213
	 * @NoCSRFRequired
214
	 */
215
	public function resetAll() {
216
		$this->podcastService->resetAll($this->userId);
217
		return new JSONResponse(['success' => true]);
218
	}
219
220
	/**
221
	 * export all podcast channels to an OPML file
222
	 *
223
	 * @param string $name target file name
224
	 * @param string $path parent folder path
225
	 * @param string $oncollision action to take on file name collision,
226
	 *								supported values:
227
	 *								- 'overwrite' The existing file will be overwritten
228
	 *								- 'keepboth' The new file is named with a suffix to make it unique
229
	 *								- 'abort' (default) The operation will fail
230
	 *
231
	 * @NoAdminRequired
232
	 * @NoCSRFRequired
233
	 */
234
	public function exportAllToFile(string $name, string $path, string $oncollision) {
235
		try {
236
			$userFolder = $this->rootFolder->getUserFolder($this->userId);
237
			$exportedFilePath = $this->podcastService->exportToFile(
238
					$this->userId, $userFolder, $path, $name, $oncollision);
239
			return new JSONResponse(['wrote_to_file' => $exportedFilePath]);
240
		} catch (\OCP\Files\NotFoundException $ex) {
241
			return new ErrorResponse(Http::STATUS_NOT_FOUND, 'folder not found');
242
		} catch (FileExistsException $ex) {
243
			return new ErrorResponse(Http::STATUS_CONFLICT, 'file already exists', ['path' => $ex->getPath(), 'suggested_name' => $ex->getAltName()]);
244
		} catch (\OCP\Files\NotPermittedException $ex) {
245
			return new ErrorResponse(Http::STATUS_FORBIDDEN, 'user is not allowed to write to the target file');
246
		}
247
	}
248
249
	/**
250
	 * parse an OPML file and return list of contained channels
251
	 *
252
	 * @param string $filePath path of the file to parse
253
	 *
254
	 * @NoAdminRequired
255
	 * @NoCSRFRequired
256
	 */
257
	public function parseListFile(string $filePath) {
258
		try {
259
			$userFolder = $this->rootFolder->getUserFolder($this->userId);
260
			$list = $this->podcastService->parseOpml($userFolder, $filePath);
261
			return $list;
262
		} catch (\UnexpectedValueException $ex) {
263
			return new ErrorResponse(Http::STATUS_UNSUPPORTED_MEDIA_TYPE, $ex->getMessage());
264
		}
265
	}
266
}
267