diff --git a/lib/Controller/PersonalSettingsController.php b/lib/Controller/PersonalSettingsController.php index 4cb07f4..44de8b9 100644 --- a/lib/Controller/PersonalSettingsController.php +++ b/lib/Controller/PersonalSettingsController.php @@ -4,34 +4,43 @@ 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 OCP\AppFramework\Http\TemplateResponse; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; use OCP\IL10N; use OCP\IRequest; use OCP\Settings\ISettings; -class EpisodeActionController extends Controller { +use Psr\Log\LoggerInterface; - private IL10N $l; +class PersonalSettingsController extends Controller { + + private LoggerInterface $logger; + private string $userId; private SubscriptionChangeRepository $subscriptionChangeRepository; private EpisodeActionRepository $episodeActionRepository; - private string $userId; + private PodcastCacheService $podcastCacheService; public function __construct( string $AppName, IRequest $request, - $UserId, - IL10N $l, + LoggerInterface $logger, + string $UserId, SubscriptionChangeRepository $subscriptionChangeRepository, EpisodeActionRepository $episodeActionRepository, + PodcastCacheService $podcastCacheService, ) { parent::__construct($AppName, $request); - $this->l = $l; + $this->logger = $logger; + $this->userId = $UserId ?? ''; $this->subscriptionChangeRepository = $subscriptionChangeRepository; $this->episodeActionRepository = $episodeActionRepository; - $this->userId = $UserId ?? ''; + $this->podcastCacheService = $podcastCacheService; } /** @@ -43,28 +52,64 @@ class EpisodeActionController extends Controller { */ public function metrics(): JSONResponse { $sinceDatetime = (new DateTime)->setTimestamp(0); - $subscriptions = $this->extractUrlList($this->subscriptionChangeRepository->findAllSubscribed($sinceDatetime, $this->userId)); + $subscriptionChanges = $this->subscriptionChangeRepository->findAllSubscribed($sinceDatetime, $this->userId); $episodeActions = $this->episodeActionRepository->findAll(0, $this->userId); + $subStats = array(); - foreach ($episodeActions as $action) { - $pod = $action->getPodcast(); - $sub = $subStats[$pod] ?? array(); - $sub['started']++; - $subStats[$pod] = $sub; + foreach ($episodeActions as $ep) { + $url = $ep->getPodcast(); + $stats = $subStats[$url] ?? [ + 'listenedSeconds' => 0, + 'actionCounts' => $this->defaultActionCounts(), + ]; + $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] ?? array(); + $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); + return new JSONResponse([ 'subscriptions' => $subscriptions, - 'subStats' => $subStats, ]); } - /** - * @param array $allSubscribed - * @return mixed - */ - private function extractUrlList(array $allSubscribed): array { - return array_map(static function (SubscriptionChangeEntity $subscription) { - return $subscription->getUrl(); - }, $allSubscribed); + private function defaultActionCounts(): array { + return [ + 'download' => 0, + 'delete' => 0, + 'play' => 0, + 'new' => 0, + 'flattr' => 0, + ]; } } diff --git a/lib/Service/PodcastCacheService.php b/lib/Service/PodcastCacheService.php new file mode 100644 index 0000000..a3eceb6 --- /dev/null +++ b/lib/Service/PodcastCacheService.php @@ -0,0 +1,77 @@ +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; + } +} diff --git a/src/personalSettings.js b/src/personalSettings.js index ac45176..140a35e 100644 --- a/src/personalSettings.js +++ b/src/personalSettings.js @@ -8,6 +8,10 @@ __webpack_public_path__ = generateFilePath(appName, '', 'js/') Vue.mixin({ methods: { t, n } }) +// https://nextcloud-vue-components.netlify.app/#/Introduction +Vue.prototype.OC = window.OC +Vue.prototype.OCA = window.OCA + export default new Vue({ el: '#personal_settings', render: h => h(PersonalSettingsPage), diff --git a/src/views/PersonalSettingsPage.vue b/src/views/PersonalSettingsPage.vue index b617ad8..c031d09 100644 --- a/src/views/PersonalSettingsPage.vue +++ b/src/views/PersonalSettingsPage.vue @@ -3,17 +3,60 @@ Hello world :) +