Added fetched podcast data
This commit is contained in:
parent
226054a634
commit
33dd043dcb
@ -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);
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
77
lib/Service/PodcastCacheService.php
Normal file
77
lib/Service/PodcastCacheService.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user