Refactored code out to Core ns and data classes

This commit is contained in:
Kalle Fagerberg 2022-08-28 20:44:30 +02:00 committed by thrillfall
parent 4e5547e1f8
commit 6b0550f14b
7 changed files with 394 additions and 154 deletions

View File

@ -3,42 +3,27 @@ declare(strict_types=1);
namespace OCA\GPodderSync\Controller; namespace OCA\GPodderSync\Controller;
use DateTime; use OCA\GPodderSync\Core\PodcastData\PodcastMetrics;
use OCA\GPodderSync\Core\PodcastData\PodcastMetricsReader;
use OCA\GPodderSync\Service\PodcastCacheService;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeEntity;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest; use OCP\IRequest;
use Psr\Log\LoggerInterface;
class PersonalSettingsController extends Controller { class PersonalSettingsController extends Controller {
private LoggerInterface $logger;
private string $userId; private string $userId;
private SubscriptionChangeRepository $subscriptionChangeRepository; private PodcastMetricsReader $metricsReader;
private EpisodeActionRepository $episodeActionRepository;
private PodcastCacheService $podcastCacheService;
public function __construct( public function __construct(
string $AppName, string $AppName,
IRequest $request, IRequest $request,
LoggerInterface $logger,
string $UserId, string $UserId,
SubscriptionChangeRepository $subscriptionChangeRepository, PodcastMetricsReader $metricsReader,
EpisodeActionRepository $episodeActionRepository,
PodcastCacheService $podcastCacheService,
) { ) {
parent::__construct($AppName, $request); parent::__construct($AppName, $request);
$this->logger = $logger;
$this->userId = $UserId ?? ''; $this->userId = $UserId ?? '';
$this->subscriptionChangeRepository = $subscriptionChangeRepository; $this->metricsReader = $metricsReader;
$this->episodeActionRepository = $episodeActionRepository;
$this->podcastCacheService = $podcastCacheService;
} }
/** /**
@ -49,65 +34,13 @@ class PersonalSettingsController extends Controller {
* @return JSONResponse * @return JSONResponse
*/ */
public function metrics(): JSONResponse { public function metrics(): JSONResponse {
$sinceDatetime = (new DateTime)->setTimestamp(0); $metrics = $this->metricsReader->metrics($this->userId);
$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);
$metricsArrays = array_map(function (PodcastMetrics $metric) {
return $metric->toArray();
}, $metrics);
return new JSONResponse([ 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,
],
];
}
} }

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\PodcastData;
class PodcastActionCounts {
private int $delete = 0;
private int $download = 0;
private int $flattr = 0;
private int $new = 0;
private int $play = 0;
/**
* @param string $action
*/
public function incrementAction(string $action): void {
switch ($action) {
case 'delete': $this->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,
];
}
}

View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\PodcastData;
use DateTime;
use SimpleXMLElement;
class PodcastData {
private string $title;
private string $author;
private string $link;
private string $description;
private string $image;
private int $fetchedAtUnix;
public function __construct(
string $title,
string $author,
string $link,
string $description,
string $image,
int $fetchedAtUnix,
) {
$this->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,
];
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\PodcastData;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\ICache;
use OCP\ICacheFactory;
class PodcastDataCache {
private ?ICache $cache = null;
private IClient $httpClient;
public function __construct(
ICacheFactory $cacheFactory,
IClientService $httpClientService,
) {
if ($cacheFactory->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());
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\PodcastData;
class PodcastMetrics {
private string $url;
private int $listenedSeconds;
private PodcastActionCounts $actionCounts;
private ?PodcastData $podcastData;
public function __construct(
string $url,
int $listenedSeconds = 0,
?PodcastActionCounts $actionCounts = null,
?PodcastData $podcastData = null,
) {
$this->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(),
];
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\PodcastData;
use DateTime;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeEntity;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository;
use Psr\Log\LoggerInterface;
class PodcastMetricsReader {
private LoggerInterface $logger;
private SubscriptionChangeRepository $subscriptionChangeRepository;
private EpisodeActionRepository $episodeActionRepository;
private PodcastDataCache $cache;
public function __construct(
LoggerInterface $logger,
SubscriptionChangeRepository $subscriptionChangeRepository,
EpisodeActionRepository $episodeActionRepository,
PodcastDataCache $cache,
) {
$this->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),
);
}
}

View File

@ -1,77 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Service;
use DateTime;
use SimpleXMLElement;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\ICache;
use OCP\ICacheFactory;
class PodcastCacheService {
private ?ICache $cache = null;
private IClient $httpClient;
public function __construct(
ICacheFactory $cacheFactory,
IClientService $httpClientService,
) {
if ($cacheFactory->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;
}
}