Added fetched podcast data

This commit is contained in:
Kalle Fagerberg 2022-07-17 18:23:16 +02:00 committed by thrillfall
parent 226054a634
commit 33dd043dcb
4 changed files with 193 additions and 24 deletions

View File

@ -4,34 +4,43 @@ declare(strict_types=1);
namespace OCA\GPodderSync\Controller; namespace OCA\GPodderSync\Controller;
use DateTime; use DateTime;
use OCA\GPodderSync\Service\PodcastCacheService;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository; use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeEntity; use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeEntity;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository; 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\IL10N;
use OCP\IRequest; use OCP\IRequest;
use OCP\Settings\ISettings; 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 SubscriptionChangeRepository $subscriptionChangeRepository;
private EpisodeActionRepository $episodeActionRepository; private EpisodeActionRepository $episodeActionRepository;
private string $userId; private PodcastCacheService $podcastCacheService;
public function __construct( public function __construct(
string $AppName, string $AppName,
IRequest $request, IRequest $request,
$UserId, LoggerInterface $logger,
IL10N $l, string $UserId,
SubscriptionChangeRepository $subscriptionChangeRepository, SubscriptionChangeRepository $subscriptionChangeRepository,
EpisodeActionRepository $episodeActionRepository, EpisodeActionRepository $episodeActionRepository,
PodcastCacheService $podcastCacheService,
) { ) {
parent::__construct($AppName, $request); parent::__construct($AppName, $request);
$this->l = $l; $this->logger = $logger;
$this->userId = $UserId ?? '';
$this->subscriptionChangeRepository = $subscriptionChangeRepository; $this->subscriptionChangeRepository = $subscriptionChangeRepository;
$this->episodeActionRepository = $episodeActionRepository; $this->episodeActionRepository = $episodeActionRepository;
$this->userId = $UserId ?? ''; $this->podcastCacheService = $podcastCacheService;
} }
/** /**
@ -43,28 +52,64 @@ class EpisodeActionController extends Controller {
*/ */
public function metrics(): JSONResponse { public function metrics(): JSONResponse {
$sinceDatetime = (new DateTime)->setTimestamp(0); $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); $episodeActions = $this->episodeActionRepository->findAll(0, $this->userId);
$subStats = array(); $subStats = array();
foreach ($episodeActions as $action) { foreach ($episodeActions as $ep) {
$pod = $action->getPodcast(); $url = $ep->getPodcast();
$sub = $subStats[$pod] ?? array(); $stats = $subStats[$url] ?? [
$sub['started']++; 'listenedSeconds' => 0,
$subStats[$pod] = $sub; '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([ return new JSONResponse([
'subscriptions' => $subscriptions, 'subscriptions' => $subscriptions,
'subStats' => $subStats,
]); ]);
} }
/** private function defaultActionCounts(): array {
* @param array $allSubscribed return [
* @return mixed 'download' => 0,
*/ 'delete' => 0,
private function extractUrlList(array $allSubscribed): array { 'play' => 0,
return array_map(static function (SubscriptionChangeEntity $subscription) { 'new' => 0,
return $subscription->getUrl(); 'flattr' => 0,
}, $allSubscribed); ];
} }
} }

View File

@ -0,0 +1,77 @@
<?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;
}
}

View File

@ -8,6 +8,10 @@ __webpack_public_path__ = generateFilePath(appName, '', 'js/')
Vue.mixin({ methods: { t, n } }) 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({ export default new Vue({
el: '#personal_settings', el: '#personal_settings',
render: h => h(PersonalSettingsPage), render: h => h(PersonalSettingsPage),

View File

@ -3,17 +3,60 @@
<SettingsSection :title="t('gpoddersync', 'Synced subscriptions')" <SettingsSection :title="t('gpoddersync', 'Synced subscriptions')"
:description="t('gpoddersync', 'Podcast subscriptions that has so far been synchronized with this Nextcloud account.')"> :description="t('gpoddersync', 'Podcast subscriptions that has so far been synchronized with this Nextcloud account.')">
<span>Hello <span class="red_text">world</span> :)</span> <span>Hello <span class="red_text">world</span> :)</span>
<ul>
<ListItem v-for="sub in subscriptions"
:key="sub.url"
:title="sub.podcast?.title ?? sub.url">
<template #icon>
<Avatar :size="44"
:url="sub.podcast?.image"
:display-name="sub.podcast?.author" />
</template>
<template #subtitle>
{{ sub.podcast?.description }}
</template>
</ListItem>
</ul>
</SettingsSection> </SettingsSection>
</div> </div>
</template> </template>
<script> <script>
import { SettingsSection } from '@nextcloud/vue' import Avatar from '@nextcloud/vue/dist/Components/Avatar'
import ListItem from '@nextcloud/vue/dist/Components/ListItem'
import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
export default { export default {
name: 'PersonalSettingsPage', name: 'PersonalSettingsPage',
components: { components: {
Avatar,
ListItem,
SettingsSection, SettingsSection,
}, },
data() {
return {
subscriptions: [],
isLoading: true,
}
},
async mounted() {
try {
const resp = await axios.get(generateUrl('/apps/gpoddersync/personal_settings/metrics'))
if (!Array.isArray(resp.data.subscriptions)) {
throw new Error('expected subscriptions array in metrics response')
}
this.subscriptions = resp.data.subscriptions
} catch (e) {
console.error(e)
showError(t('gpoddersync', 'Could not fetch podcast synchronization stats'))
} finally {
this.isLoading = false
}
},
} }
</script> </script>