Passed
Push — feature/786_podcasts ( 7b8be7...af6910 )
by Pauli
02:22
created

PodcastService::updateChannel()   B

Complexity

Conditions 11
Paths 18

Size

Total Lines 44
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 31
nc 18
nop 3
dl 0
loc 44
rs 7.3166
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
11
 */
12
13
namespace OCA\Music\Utility;
14
15
use \OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
16
use \OCA\Music\AppFramework\Core\Logger;
17
use \OCA\Music\BusinessLayer\PodcastChannelBusinessLayer;
18
use \OCA\Music\BusinessLayer\PodcastEpisodeBusinessLayer;
19
use \OCA\Music\Db\PodcastChannel;
20
use \OCA\Music\Db\PodcastEpisode;
21
use \OCA\Music\Db\SortBy;
22
23
24
class PodcastService {
25
	private $channelBusinessLayer;
26
	private $episodeBusinessLayer;
27
	private $logger;
28
29
	public const STATUS_OK = 0;
30
	public const STATUS_INVALID_URL = 1;
31
	public const STATUS_INVALID_RSS = 2;
32
	public const STATUS_ALREADY_EXISTS = 3;
33
	public const STATUS_NOT_FOUND = 4;
34
35
	public function __construct(
36
			PodcastChannelBusinessLayer $channelBusinessLayer,
37
			PodcastEpisodeBusinessLayer $episodeBusinessLayer,
38
			Logger $logger) {
39
		$this->channelBusinessLayer = $channelBusinessLayer;
40
		$this->episodeBusinessLayer = $episodeBusinessLayer;
41
		$this->logger = $logger;
42
	}
43
44
	/**
45
	 * Get a specified podcast channel of a user
46
	 */
47
	public function getChannel(int $id, string $userId, bool $includeEpisodes) : ?PodcastChannel {
48
		try {
49
			$channel = $this->channelBusinessLayer->find($id, $userId);
50
			if ($includeEpisodes) {
51
				$channel->setEpisodes($this->episodeBusinessLayer->findAllByChannel($id, $userId));
52
			}
53
			return $channel;
54
		} catch (BusinessLayerException $ex) {
55
			$this->logger->log("Requested channel $id not found: " . $ex->getMessage(), 'warn');
56
			return null;
57
		}
58
	}
59
60
	/**
61
	 * Get all podcast channels of a user
62
	 * @return PodcastChannel[]
63
	 */
64
	public function getAllChannels(string $userId, bool $includeEpisodes) : array {
65
		$channels = $this->channelBusinessLayer->findAll($userId, SortBy::Name);
66
67
		if ($includeEpisodes) {
68
			$this->injectEpisodes($channels, $userId, /*$allChannelsIncluded=*/ true);
69
		}
70
71
		return $channels;
72
	}
73
74
	/**
75
	 * Get a specified podcast episode of a user
76
	 */
77
	public function getEpisode(int $id, string $userId) : ?PodcastEpisode {
78
		try {
79
			return $this->episodeBusinessLayer->find($id, $userId);
80
		} catch (BusinessLayerException $ex) {
81
			$this->logger->log("Requested episode $id not found: " . $ex->getMessage(), 'warn');
82
			return null;
83
		}
84
	}
85
86
	/**
87
	 * Inject episodes to the given podcast channels
88
	 * @param PodcastChannel[] $channels input/output 
89
	 * @param bool $allChannelsIncluded Set this to true if $channels contains all the podcasts of the user.
90
	 *									This helps in optimizing the DB query.
91
	 */
92
	public function injectEpisodes(array &$channels, string $userId, bool $allChannelsIncluded) : void {
93
		if ($allChannelsIncluded || \count($channels) > 999) {
94
			$episodes = $this->episodeBusinessLayer->findAll($userId);
95
		} else {
96
			$episodes = $this->episodeBusinessLayer->findAllByChannel(Util::extractIds($channels), $userId);
97
		}
98
99
		$episodesPerChannel = [];
100
		foreach ($episodes as $episode) {
101
			$episodesPerChannel[$episode->getChannelId()][] = $episode;
102
		}
103
104
		foreach ($channels as &$channel) {
105
			$channel->setEpisodes($episodesPerChannel[$channel->getId()] ?? []);
106
		}
107
	}
108
109
	/**
110
	 * Add a followed podcast for a user from an RSS feed
111
	 * @return array like ['status' => int, 'channel' => ?PodcastChannel]
112
	 */
113
	public function subscribe(string $url, string $userId) : array {
114
		$content = \file_get_contents($url);
115
		if ($content === false) {
116
			return ['status' => self::STATUS_INVALID_URL, 'channel' => null];
117
		}
118
119
		$xmlTree = \simplexml_load_string($content, \SimpleXMLElement::class, LIBXML_NOCDATA);
120
		if ($xmlTree === false || !$xmlTree->channel) {
121
			return ['status' => self::STATUS_INVALID_RSS, 'channel' => null];
122
		}
123
124
		try {
125
			$channel = $this->channelBusinessLayer->create($userId, $url, $content, $xmlTree->channel);
126
		} catch (\OCA\Music\AppFramework\Db\UniqueConstraintViolationException $ex) {
127
			return ['status' => self::STATUS_ALREADY_EXISTS, 'channel' => null];
128
		}
129
130
		$episodes = [];
131
		foreach ($xmlTree->channel->item as $episodeNode) {
132
			if ($episodeNode !== null) {
133
				$episodes[] = $this->episodeBusinessLayer->addOrUpdate($userId, $channel->getId(), $episodeNode);
134
			}
135
		}
136
137
		$channel->setEpisodes($episodes);
138
139
		return ['status' => self::STATUS_OK, 'channel' => $channel];
140
	}
141
142
	/**
143
	 * Deletes a podcast channel from a user
144
	 * @return int status code
145
	 */
146
	public function unsubscribe(int $channelId, string $userId) : int {
147
		try {
148
			$this->channelBusinessLayer->delete($channelId, $userId); // throws if not found
149
			$this->episodeBusinessLayer->deleteByChannel($channelId, $userId); // does not throw
150
			return self::STATUS_OK;
151
		} catch (BusinessLayerException $ex) {
152
			$this->logger->log("Channel $channelId to be unsubscirbed not found: " . $ex->getMessage(), 'warn');
153
			return self::STATUS_NOT_FOUND;
154
		}
155
	}
156
157
	/**
158
	 * Check a single podcast channel for updates
159
	 * @param string|null $prevHash Previous content hash known by the client. If given, the result will tell
160
	 *								if the channel content has updated from this state. If omitted, the result
161
	 *								will tell if the channel changed from its previous server-known state.
162
	 * @return array like ['status' => int, 'updated' => bool, 'channel' => ?PodcastChannel]
163
	 */
164
	public function updateChannel(int $id, string $userId, ?string $prevHash = null) : array {
165
		$updated = false;
166
		$status = self::STATUS_OK;
167
168
		try {
169
			$channel = $this->channelBusinessLayer->find($id, $userId);
170
		} catch (BusinessLayerException $ex) {
171
			$this->logger->log("Channel $id to be updated not found: " . $ex->getMessage(), 'warn');
172
			$status = self::STATUS_NOT_FOUND;
173
		}
174
175
		if ($channel) {
0 ignored issues
show
introduced by
$channel is of type OCA\Music\Db\PodcastChannel, thus it always evaluated to true.
Loading history...
176
			$content = \file_get_contents($channel->getRssUrl());
177
			if ($content === null) {
178
				$status = self::STATUS_INVALID_URL;
179
			} else {
180
				$xmlTree = \simplexml_load_string($content, \SimpleXMLElement::class, LIBXML_NOCDATA);
181
			}
182
183
			if (!$xmlTree || !$xmlTree->channel) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $xmlTree does not seem to be defined for all execution paths leading up to this point.
Loading history...
184
				$this->logger->log("RSS feed for the chanenl {$channel->id} was invalid", 'warn');
185
				$status = self::STATUS_INVALID_RSS;
186
			} else if ($this->channelBusinessLayer->updateChannel($channel, $content, $xmlTree->channel)) {
187
				// channel content has actually changed, update the episodes too
188
				$episodes = [];
189
				foreach ($xmlTree->channel->item as $episodeNode) {
190
					if ($episodeNode !== null) {
191
						$episodes[] = $this->episodeBusinessLayer->addOrUpdate($userId, $id, $episodeNode);
192
					}
193
				}
194
				$channel->setEpisodes($episodes);
195
				$this->episodeBusinessLayer->deleteByChannelExcluding($id, Util::extractIds($episodes), $userId);
196
				$updated = true;
197
			} else if ($prevHash !== null && $prevHash !== $channel->getContentHash()) {
198
				// the channel content is not new for the server but it is still new for the client
199
				$channel->setEpisodes($this->episodeBusinessLayer->findAllByChannel($id, $this->userId));
0 ignored issues
show
Bug Best Practice introduced by
The property userId does not exist on OCA\Music\Utility\PodcastService. Did you maybe forget to declare it?
Loading history...
200
				$updated = true;
201
			}
202
		}
203
204
		return [
205
			'status' => $status,
206
			'updated' => $updated,
207
			'channel' => $channel
208
		];
209
	}
210
211
	/**
212
	 * Check updates for all chanenls of the user, one-by-one
213
	 * @return array like ['changed' => int, 'unchanged' => int, 'failed' => int]
214
	 *			where each int represent number of channels in that category
215
	 */
216
	public function updateAllChannels(string $userId) : array {
217
		$result = ['changed' => 0, 'unchanged' => 0, 'failed' => 0];
218
		$ids = $this->channelBusinessLayer->findAllIds($userId);
219
220
		foreach ($ids as $id) {
221
			$channelResult = $this->updateChannel($id, $userId);
222
			if ($channelResult['updated']) {
223
				$result['changed']++;
224
			} elseif ($channelResult['status'] === self::STATUS_OK) {
225
				$result['unchanged']++;
226
			} else {
227
				$result['failed']++;
228
			}
229
		}
230
231
		return $result;
232
	}
233
234
	/**
235
	 * Reset all the subscribed podcasts of the user
236
	 */
237
	public function resetAll(string $userId) : void {
238
		$this->episodeBusinessLayer->deleteAll($userId);
239
		$this->channelBusinessLayer->deleteAll($userId);
240
	}
241
242
}
243