refacto: Use custom PodcastData to fix redirections (close #33)

This commit is contained in:
Michel Roux 2024-01-13 00:10:37 +01:00
parent 7375088700
commit 3d066d63c6
5 changed files with 116 additions and 54 deletions

View File

@ -4,10 +4,8 @@ declare(strict_types=1);
namespace OCA\RePod\Controller;
use OCA\GPodderSync\Core\PodcastData\PodcastData;
use OCA\GPodderSync\Core\PodcastData\PodcastDataReader;
use OCA\RePod\AppInfo\Application;
use OCA\RePod\Service\UserService;
use OCA\RePod\Core\PodcastData\PodcastData;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Http\Client\IClientService;
@ -17,26 +15,16 @@ class PodcastController extends Controller
{
public function __construct(
IRequest $request,
private IClientService $clientService,
private UserService $userService,
private PodcastDataReader $podcastDataReader
private IClientService $clientService
) {
parent::__construct(Application::APP_ID, $request);
}
public function index(string $url): JSONResponse {
$podcast = $this->podcastDataReader->tryGetCachedPodcastData($url);
if ($podcast) {
return new JSONResponse($podcast);
}
$client = $this->clientService->newClient();
$feed = $client->get($url);
$podcast = PodcastData::parseRssXml((string) $feed->getBody());
$this->podcastDataReader->trySetCachedPodcastData($url, $podcast);
return new JSONResponse($podcast, $feed->getStatusCode());
return new JSONResponse($podcast->toArrayWithExtras(), $feed->getStatusCode());
}
}

View File

@ -13,7 +13,6 @@ use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
* @psalm-import-type EpisodeActionType from EpisodeAction
*
* @psalm-type EpisodeActionExtraDataType = array{
* podcast: string,
* url: ?string,
* name: string,
* link: ?string,
@ -31,7 +30,6 @@ use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
class EpisodeActionExtraData implements \JsonSerializable
{
public function __construct(
private string $podcast,
private ?string $url,
private string $name,
private ?string $link,
@ -50,10 +48,6 @@ class EpisodeActionExtraData implements \JsonSerializable
return $this->url ?? '/no episodeUrl/';
}
public function getPodcast(): string {
return $this->podcast;
}
public function getUrl(): ?string {
return $this->url;
}
@ -108,7 +102,6 @@ class EpisodeActionExtraData implements \JsonSerializable
public function toArray(): array {
return
[
'podcast' => $this->podcast,
'url' => $this->url,
'name' => $this->name,
'link' => $this->link,

View File

@ -4,21 +4,18 @@ declare(strict_types=1);
namespace OCA\RePod\Core\EpisodeAction;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeActionReader as CoreEpisodeActionReader;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCA\RePod\Core\PodcastData\PodcastData;
use OCA\RePod\Service\UserService;
class EpisodeActionReader
class EpisodeActionReader extends CoreEpisodeActionReader
{
public function __construct(
private EpisodeActionRepository $episodeActionRepository,
private UserService $userService
) {}
public function findByEpisodeUrl(string $episodeUrl): ?EpisodeAction {
return $this->episodeActionRepository->findByEpisodeUrl($episodeUrl, $this->userService->getUserUID());
}
/**
* Base: https://github.com/pbek/nextcloud-nextpod/blob/main/lib/Core/EpisodeAction/EpisodeActionExtraData.php#L119.
* Specs : https://github.com/Podcast-Standards-Project/PSP-1-Podcast-RSS-Specification/blob/main/README.md.
@ -30,7 +27,6 @@ class EpisodeActionReader
$episodes = [];
$xml = new \SimpleXMLElement($xmlString);
$channel = $xml->channel;
$podcast = (string) $channel->title;
// Find episode by url and add data for it
/** @var \SimpleXMLElement $item */
@ -56,40 +52,40 @@ class EpisodeActionReader
$name = (string) $item->title;
// Get episode link
$link = $this->stringOrNull($item->link);
$link = PodcastData::stringOrNull($item->link);
// Get episode image
$image = $this->stringOrNull($item->image->url);
$image = PodcastData::stringOrNull($item->image->url);
if (!$image && $iTunesItemChildren) {
$imageAttributes = $iTunesItemChildren->image->attributes();
$image = $this->stringOrNull($imageAttributes ? (string) $imageAttributes->href : '');
$image = PodcastData::stringOrNull($imageAttributes ? (string) $imageAttributes->href : '');
}
if (!$image) {
$image = $this->stringOrNull($channel->image->url);
$image = PodcastData::stringOrNull($channel->image->url);
}
if (!$image && $iTunesChannelChildren) {
$imageAttributes = $iTunesChannelChildren->image->attributes();
$image = $this->stringOrNull($imageAttributes ? (string) $imageAttributes->href : '');
$image = PodcastData::stringOrNull($imageAttributes ? (string) $imageAttributes->href : '');
}
if (!$image) {
preg_match('/<itunes:image\s+href="([^"]+)"/', $xmlString, $matches);
$image = $this->stringOrNull($matches[1]);
$image = PodcastData::stringOrNull($matches[1]);
}
// Get episode description
$itemContent = $item->children('content', true);
if ($itemContent) {
$description = $this->stringOrNull($itemContent->encoded);
$description = PodcastData::stringOrNull($itemContent->encoded);
} else {
$description = $this->stringOrNull($item->description);
$description = PodcastData::stringOrNull($item->description);
}
if (!$description && $iTunesItemChildren) {
$description = $this->stringOrNull($iTunesItemChildren->summary);
$description = PodcastData::stringOrNull($iTunesItemChildren->summary);
}
// Remove tags
@ -97,9 +93,9 @@ class EpisodeActionReader
// Get episode duration
if ($iTunesItemChildren) {
$rawDuration = $this->stringOrNull($iTunesItemChildren->duration);
$rawDuration = PodcastData::stringOrNull($iTunesItemChildren->duration);
} else {
$rawDuration = $this->stringOrNull($item->duration);
$rawDuration = PodcastData::stringOrNull($item->duration);
}
$splitDuration = array_reverse(explode(':', $rawDuration ?? ''));
@ -108,11 +104,10 @@ class EpisodeActionReader
$duration += !empty($splitDuration[2]) ? (int) $splitDuration[2] * 60 : 0;
// Get episode pubDate
$rawPubDate = $this->stringOrNull($item->pubDate);
$rawPubDate = PodcastData::stringOrNull($item->pubDate);
$pubDate = $rawPubDate ? new \DateTime($rawPubDate) : null;
$episodes[] = new EpisodeActionExtraData(
$podcast,
$url,
$name,
$link,
@ -130,15 +125,4 @@ class EpisodeActionReader
return $episodes;
}
/**
* @param null|\SimpleXMLElement|string $value
*/
private function stringOrNull($value): ?string {
if ($value) {
return (string) $value;
}
return null;
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace OCA\RePod\Core\PodcastData;
use OCA\GPodderSync\Core\PodcastData\PodcastData as CorePodcastData;
class PodcastData extends CorePodcastData implements \JsonSerializable
{
public function __construct(
?string $title,
?string $author,
?string $link,
?string $description,
?string $imageUrl,
int $fetchedAtUnix,
?string $imageBlob = null,
private ?string $atomLink
) {
parent::__construct(
$title,
$author,
$link,
$description,
$imageUrl,
$fetchedAtUnix,
$imageBlob
);
}
/**
* @throws \Exception if the XML data could not be parsed
*/
public static function parseRssXml(string $xmlString, ?int $fetchedAtUnix = null): PodcastData {
$xml = new \SimpleXMLElement($xmlString);
$channel = $xml->channel;
return new PodcastData(
self::stringOrNull($channel->title),
self::getXPathContent($xml, '/rss/channel/itunes:author'),
self::stringOrNull($channel->link),
self::stringOrNull($channel->description),
self::getXPathContent($xml, '/rss/channel/image/url')
?? self::getXPathAttribute($xml, '/rss/channel/itunes:image/@href'),
$fetchedAtUnix ?? (new \DateTime())->getTimestamp(),
null,
self::getXPathContent($xml, '/rss/channel/atom:link/@href')
);
}
/**
* @param null|\SimpleXMLElement|string $value
*/
public static function stringOrNull($value): ?string {
if ($value) {
return (string) $value;
}
return null;
}
public function getAtomLink(): ?string {
return $this->atomLink;
}
public function toArrayWithExtras() {
return array_merge(parent::toArray(), [
'atomLink' => $this->atomLink,
]);
}
private static function getXPathContent(\SimpleXMLElement $xml, string $xpath): ?string {
$match = $xml->xpath($xpath);
if ($match) {
return (string) $match[0];
}
return null;
}
private static function getXPathAttribute(\SimpleXMLElement $xml, string $xpath): ?string {
$match = $xml->xpath($xpath);
if ($match) {
return (string) $match[0][0];
}
return null;
}
}

View File

@ -12,7 +12,8 @@
:image-url="feed.imageUrl"
:link="feed.link"
:title="feed.title" />
<Episodes v-if="feed" />
<Episodes v-if="feed"
:title="feed.title" />
</NcAppContent>
</template>
@ -24,6 +25,7 @@ import Episodes from '../components/Feed/Episodes.vue'
import Loading from '../components/Atoms/Loading.vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { toUrl } from '../utils/url.js'
export default {
name: 'Feed',
@ -50,6 +52,11 @@ export default {
async mounted() {
try {
const podcastData = await axios.get(generateUrl('/apps/repod/podcast?url={url}', { url: this.url }))
if (podcastData.data.atomLink !== this.url) {
this.$router.push(toUrl(podcastData.data.atomLink))
}
this.feed = podcastData.data
} catch (e) {
this.failed = true