diff --git a/lib/Controller/PersonalSettingsController.php b/lib/Controller/PersonalSettingsController.php index f5e7690..4d7d417 100644 --- a/lib/Controller/PersonalSettingsController.php +++ b/lib/Controller/PersonalSettingsController.php @@ -3,42 +3,27 @@ declare(strict_types=1); namespace OCA\GPodderSync\Controller; -use DateTime; - -use OCA\GPodderSync\Service\PodcastCacheService; -use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository; -use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeEntity; -use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository; +use OCA\GPodderSync\Core\PodcastData\PodcastMetrics; +use OCA\GPodderSync\Core\PodcastData\PodcastMetricsReader; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; -use Psr\Log\LoggerInterface; - class PersonalSettingsController extends Controller { - private LoggerInterface $logger; private string $userId; - private SubscriptionChangeRepository $subscriptionChangeRepository; - private EpisodeActionRepository $episodeActionRepository; - private PodcastCacheService $podcastCacheService; + private PodcastMetricsReader $metricsReader; public function __construct( string $AppName, IRequest $request, - LoggerInterface $logger, string $UserId, - SubscriptionChangeRepository $subscriptionChangeRepository, - EpisodeActionRepository $episodeActionRepository, - PodcastCacheService $podcastCacheService, + PodcastMetricsReader $metricsReader, ) { parent::__construct($AppName, $request); - $this->logger = $logger; $this->userId = $UserId ?? ''; - $this->subscriptionChangeRepository = $subscriptionChangeRepository; - $this->episodeActionRepository = $episodeActionRepository; - $this->podcastCacheService = $podcastCacheService; + $this->metricsReader = $metricsReader; } /** @@ -49,65 +34,13 @@ class PersonalSettingsController extends Controller { * @return JSONResponse */ public function metrics(): JSONResponse { - $sinceDatetime = (new DateTime)->setTimestamp(0); - $subscriptionChanges = $this->subscriptionChangeRepository->findAllSubscribed($sinceDatetime, $this->userId); - $episodeActions = $this->episodeActionRepository->findAll(0, $this->userId); - - $subStats = array(); - foreach ($episodeActions as $ep) { - $url = $ep->getPodcast(); - $stats = $subStats[$url] ?? self::defaultSubscriptionData(); - $actionCounts = $stats['actionCounts']; - $actionLower = strtolower($ep->getAction()); - if (array_key_exists($actionLower, $actionCounts)) { - $actionCounts[$actionLower]++; - } - $stats['actionCounts'] = $actionCounts; - if ($actionLower == 'play') { - $seconds = $ep->getPosition(); - if ($seconds && $seconds != -1) { - $stats['listenedSeconds'] += $seconds; - } - } - $subStats[$url] = $stats; - } - - $subscriptions = array_map(function (SubscriptionChangeEntity $sub) use ($subStats) { - $url = $sub->getUrl(); - $stats = $subStats[$url] ?? self::defaultSubscriptionData(); - $sub = [ - 'url' => $url ?? '', - 'listenedSeconds' => $stats['listenedSeconds'], - 'actionCounts' => $stats['actionCounts'], - ]; - try { - $podcast = $this->podcastCacheService->getCachedOrFetchPodcastData($url); - $sub['podcast'] = $podcast; - } catch (\Exception $e) { - $sub['podcast'] = null; - $this->logger->error("Failed to get podcast data.", [ - 'exception' => $e, - 'podcastUrl' => $url, - ]); - } - return $sub; - }, $subscriptionChanges); + $metrics = $this->metricsReader->metrics($this->userId); + $metricsArrays = array_map(function (PodcastMetrics $metric) { + return $metric->toArray(); + }, $metrics); return new JSONResponse([ - 'subscriptions' => $subscriptions, + 'subscriptions' => $metricsArrays, ]); } - - private static function defaultSubscriptionData(): array { - return [ - 'listenedSeconds' => 0, - 'actionCounts' => [ - 'download' => 0, - 'delete' => 0, - 'play' => 0, - 'new' => 0, - 'flattr' => 0, - ], - ]; - } } diff --git a/lib/Core/PodcastData/PodcastActionCounts.php b/lib/Core/PodcastData/PodcastActionCounts.php new file mode 100644 index 0000000..0da4e43 --- /dev/null +++ b/lib/Core/PodcastData/PodcastActionCounts.php @@ -0,0 +1,36 @@ +delete++; break; + case 'download': $this->download++; break; + case 'flattr': $this->flattr++; break; + case 'new': $this->new++; break; + case 'play': $this->play++; break; + } + } + + public function toArray(): array { + return + [ + 'delete' => $this->delete, + 'download' => $this->download, + 'flattr' => $this->flattr, + 'new' => $this->new, + 'play' => $this->play, + ]; + } +} diff --git a/lib/Core/PodcastData/PodcastData.php b/lib/Core/PodcastData/PodcastData.php new file mode 100644 index 0000000..c9234c8 --- /dev/null +++ b/lib/Core/PodcastData/PodcastData.php @@ -0,0 +1,122 @@ +title = $title; + $this->author = $author; + $this->link = $link; + $this->description = $description; + $this->image = $image; + $this->fetchedAtUnix = $fetchedAtUnix; + } + + public static function parseXml(string $xmlString): PodcastData { + $xml = new SimpleXMLElement($xmlString); + $channel = $xml->channel; + return new PodcastData( + title: (string)$channel->title, + author: self::getXPathContent($xml, '/rss/channel/itunes:author'), + link: (string)$channel->link, + description: (string)$channel->description, + image: + self::getXPathContent($xml, '/rss/channel/image/url') + ?? self::getXPathAttribute($xml, '/rss/channel/itunes:image/@href'), + fetchedAtUnix: (new DateTime())->getTimestamp(), + ); + } + + private static function getXPathContent(SimpleXMLElement $xml, string $xpath) { + $match = $xml->xpath($xpath); + if ($match) { + return (string)$match[0]; + } + return null; + } + + private static function getXPathAttribute(SimpleXMLElement $xml, string $xpath) { + $match = $xml->xpath($xpath); + if ($match) { + return (string)$match[0][0]; + } + return null; + } + + /** + * @return string + */ + public function getTitle(): string { + return $this->title; + } + + /** + * @return string + */ + public function getAuthor(): string { + return $this->author; + } + + /** + * @return string + */ + public function getLink(): string { + return $this->link; + } + + /** + * @return string + */ + public function getDescription(): string { + return $this->description; + } + + /** + * @return string + */ + public function getImage(): string { + return $this->image; + } + + /** + * @return int + */ + public function getFetchedAtUnix(): int { + return $this->fetchedAtUnix; + } + + public function __toString() : String { + return $this->title; + } + + public function toArray(): array { + return + [ + 'title' => $this->title, + 'author' => $this->author, + 'link' => $this->link, + 'description' => $this->description, + 'image' => $this->image, + 'fetchedAtUnix' => $this->fetchedAtUnix, + ]; + } +} + diff --git a/lib/Core/PodcastData/PodcastDataCache.php b/lib/Core/PodcastData/PodcastDataCache.php new file mode 100644 index 0000000..277b78b --- /dev/null +++ b/lib/Core/PodcastData/PodcastDataCache.php @@ -0,0 +1,66 @@ +isLocalCacheAvailable()) { + $this->cache = $cacheFactory->createLocal('GPodderSync-Podcasts'); + } + $this->httpClient = $httpClientService->newClient(); + } + + public function getCachedOrFetchPodcastData(string $url): PodcastData { + if ($this->cache == null) { + return $this->fetchPodcastData($url); + } + $oldData = $this->tryGetCachedPodcastData($url); + if ($oldData) { + return $oldData; + } + $newData = $this->fetchPodcastData($url); + $this->trySetCachedPodcastData($url, $newData); + return $newData; + } + + public function fetchPodcastData(string $url): PodcastData { + $resp = $this->httpClient->get($url); + $statusCode = $resp->getStatusCode(); + if ($statusCode < 200 || $statusCode >= 300) { + throw new \ErrorException("Podcast RSS URL returned non-2xx status code: $statusCode"); + } + $body = $resp->getBody(); + return PodcastData::parseXml($body); + } + + public function tryGetCachedPodcastData(string $url): ?PodcastData { + $oldData = $this->cache->get($url); + if (!$oldData) { + return null; + } + return new PodcastData( + title: $oldData['title'], + author: $oldData['author'], + link: $oldData['link'], + description: $oldData['description'], + image: $oldData['image'], + fetchedAtUnix: $oldData['fetchedAtUnix'], + ); + } + + public function trySetCachedPodcastData(string $url, PodcastData $data) { + $this->cache->set($url, $data->toArray()); + } +} diff --git a/lib/Core/PodcastData/PodcastMetrics.php b/lib/Core/PodcastData/PodcastMetrics.php new file mode 100644 index 0000000..aa76262 --- /dev/null +++ b/lib/Core/PodcastData/PodcastMetrics.php @@ -0,0 +1,68 @@ +url = $url; + $this->actionCounts = $actionCounts ?? new PodcastActionCounts; + $this->listenedSeconds = $listenedSeconds; + $this->podcastData = $podcastData; + } + + /** + * @return string + */ + public function getUrl(): string { + return $this->url; + } + + /** + * @return PodcastActionCounts + */ + public function getActionCounts(): PodcastActionCounts { + return $this->actionCounts; + } + + /** + * @return int + */ + public function getListenedSeconds(): int { + return $this->listenedSeconds; + } + + /** + * @param int $seconds + */ + public function addListenedSeconds(int $seconds): void { + $this->listenedSeconds += $seconds; + } + + /** + * @return PodcastData|null + */ + public function getPodcastData(): ?PodcastData { + return $this->podcastData; + } + + public function toArray(): array { + return + [ + 'url' => $this->url, + 'listenedSeconds' => $this->listenedSeconds, + 'actionCounts' => $this->actionCounts->toArray(), + 'podcastData' => $this->podcastData->toArray(), + ]; + } +} diff --git a/lib/Core/PodcastData/PodcastMetricsReader.php b/lib/Core/PodcastData/PodcastMetricsReader.php new file mode 100644 index 0000000..a532499 --- /dev/null +++ b/lib/Core/PodcastData/PodcastMetricsReader.php @@ -0,0 +1,92 @@ +logger = $logger; + $this->subscriptionChangeRepository = $subscriptionChangeRepository; + $this->episodeActionRepository = $episodeActionRepository; + $this->cache = $cache; + } + + /** + * @param string $userId + * + * @return PodcastMetrics[] + */ + public function metrics(string $userId): array { + $episodeActions = $this->episodeActionRepository->findAll(0, $userId); + + $metricsPerUrl = array(); + foreach ($episodeActions as $ep) { + $url = $ep->getPodcast(); + /** @var PodcastMetrics */ + $metrics = $metricsPerUrl[$url] ?? $this->createMetricsForUrl($url); + + $actionLower = strtolower($ep->getAction()); + $metrics->getActionCounts()->incrementAction($actionLower); + + if ($actionLower == 'play') { + $seconds = $ep->getPosition(); + if ($seconds && $seconds != -1) { + $metrics->addListenedSeconds($seconds); + } + } + + $metricsPerUrl[$url] = $metrics; + } + + $sinceDatetime = (new DateTime)->setTimestamp(0); + $subscriptionChanges = $this->subscriptionChangeRepository->findAllSubscribed($sinceDatetime, $userId); + /** @var PodcastMetrics[] */ + $subscriptions = array_map(function (SubscriptionChangeEntity $sub) use ($metricsPerUrl) { + $url = $sub->getUrl(); + $metrics = $metricsPerUrl[$url] ?? $this->createMetricsForUrl($url); + return $metrics; + }, $subscriptionChanges); + + return $subscriptions; + } + + private function tryGetParsedPodcastData(string $url): ?PodcastData { + try { + return $this->cache->getCachedOrFetchPodcastData($url); + } catch (\Exception $e) { + $this->logger->error("Failed to get podcast data.", [ + 'exception' => $e, + 'podcastUrl' => $url, + ]); + return null; + } + } + + private function createMetricsForUrl(string $url): PodcastMetrics { + return new PodcastMetrics( + url: $url, + listenedSeconds: 0, + actionCounts: new PodcastActionCounts(), + podcastData: $this->tryGetParsedPodcastData($url), + ); + } +} diff --git a/lib/Service/PodcastCacheService.php b/lib/Service/PodcastCacheService.php deleted file mode 100644 index 613afc3..0000000 --- a/lib/Service/PodcastCacheService.php +++ /dev/null @@ -1,77 +0,0 @@ -isLocalCacheAvailable()) { - $this->cache = $cacheFactory->createLocal('GPodderSync-Podcasts'); - } - $this->httpClient = $httpClientService->newClient(); - } - - public function getCachedOrFetchPodcastData(string $url) { - if ($this->cache == null) { - return $this->fetchPodcastData($url); - } - $oldData = $this->cache->get($url); - if ($oldData) { - return $oldData; - } - $newData = $this->fetchPodcastData($url); - $this->cache->set($url, $newData); - return $newData; - } - - public function fetchPodcastData(string $url) { - $resp = $this->httpClient->get($url); - $statusCode = $resp->getStatusCode(); - if ($statusCode < 200 || $statusCode >= 300) { - throw new ErrorException("Podcast RSS URL returned non-2xx status code: $statusCode"); - } - $body = $resp->getBody(); - $xml = new SimpleXMLElement($body); - $channel = $xml->channel; - return [ - 'title' => (string)$channel->title, - 'author' => self::getXPathContent($xml, '/rss/channel/itunes:author'), - 'link' => (string)$channel->link, - 'description' => (string)$channel->description, - 'image' => - self::getXPathContent($xml, '/rss/channel/image/url') - ?? self::getXPathAttribute($xml, '/rss/channel/itunes:image/@href'), - 'fetchedAtUnix' => (new DateTime())->getTimestamp(), - ]; - } - - private static function getXPathContent(SimpleXMLElement $xml, string $xpath) { - $match = $xml->xpath($xpath); - if ($match) { - return (string)$match[0]; - } - return null; - } - - private static function getXPathAttribute(SimpleXMLElement $xml, string $xpath) { - $match = $xml->xpath($xpath); - if ($match) { - return (string)$match[0][0]; - } - return null; - } -}