refacto: Use custom PodcastData to fix redirections (close #33)
This commit is contained in:
parent
7375088700
commit
3d066d63c6
@ -4,10 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace OCA\RePod\Controller;
|
namespace OCA\RePod\Controller;
|
||||||
|
|
||||||
use OCA\GPodderSync\Core\PodcastData\PodcastData;
|
|
||||||
use OCA\GPodderSync\Core\PodcastData\PodcastDataReader;
|
|
||||||
use OCA\RePod\AppInfo\Application;
|
use OCA\RePod\AppInfo\Application;
|
||||||
use OCA\RePod\Service\UserService;
|
use OCA\RePod\Core\PodcastData\PodcastData;
|
||||||
use OCP\AppFramework\Controller;
|
use OCP\AppFramework\Controller;
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
use OCP\Http\Client\IClientService;
|
use OCP\Http\Client\IClientService;
|
||||||
@ -17,26 +15,16 @@ class PodcastController extends Controller
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
private IClientService $clientService,
|
private IClientService $clientService
|
||||||
private UserService $userService,
|
|
||||||
private PodcastDataReader $podcastDataReader
|
|
||||||
) {
|
) {
|
||||||
parent::__construct(Application::APP_ID, $request);
|
parent::__construct(Application::APP_ID, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(string $url): JSONResponse {
|
public function index(string $url): JSONResponse {
|
||||||
$podcast = $this->podcastDataReader->tryGetCachedPodcastData($url);
|
|
||||||
|
|
||||||
if ($podcast) {
|
|
||||||
return new JSONResponse($podcast);
|
|
||||||
}
|
|
||||||
|
|
||||||
$client = $this->clientService->newClient();
|
$client = $this->clientService->newClient();
|
||||||
$feed = $client->get($url);
|
$feed = $client->get($url);
|
||||||
|
|
||||||
$podcast = PodcastData::parseRssXml((string) $feed->getBody());
|
$podcast = PodcastData::parseRssXml((string) $feed->getBody());
|
||||||
$this->podcastDataReader->trySetCachedPodcastData($url, $podcast);
|
|
||||||
|
|
||||||
return new JSONResponse($podcast, $feed->getStatusCode());
|
return new JSONResponse($podcast->toArrayWithExtras(), $feed->getStatusCode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
|
|||||||
* @psalm-import-type EpisodeActionType from EpisodeAction
|
* @psalm-import-type EpisodeActionType from EpisodeAction
|
||||||
*
|
*
|
||||||
* @psalm-type EpisodeActionExtraDataType = array{
|
* @psalm-type EpisodeActionExtraDataType = array{
|
||||||
* podcast: string,
|
|
||||||
* url: ?string,
|
* url: ?string,
|
||||||
* name: string,
|
* name: string,
|
||||||
* link: ?string,
|
* link: ?string,
|
||||||
@ -31,7 +30,6 @@ use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
|
|||||||
class EpisodeActionExtraData implements \JsonSerializable
|
class EpisodeActionExtraData implements \JsonSerializable
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $podcast,
|
|
||||||
private ?string $url,
|
private ?string $url,
|
||||||
private string $name,
|
private string $name,
|
||||||
private ?string $link,
|
private ?string $link,
|
||||||
@ -50,10 +48,6 @@ class EpisodeActionExtraData implements \JsonSerializable
|
|||||||
return $this->url ?? '/no episodeUrl/';
|
return $this->url ?? '/no episodeUrl/';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPodcast(): string {
|
|
||||||
return $this->podcast;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUrl(): ?string {
|
public function getUrl(): ?string {
|
||||||
return $this->url;
|
return $this->url;
|
||||||
}
|
}
|
||||||
@ -108,7 +102,6 @@ class EpisodeActionExtraData implements \JsonSerializable
|
|||||||
public function toArray(): array {
|
public function toArray(): array {
|
||||||
return
|
return
|
||||||
[
|
[
|
||||||
'podcast' => $this->podcast,
|
|
||||||
'url' => $this->url,
|
'url' => $this->url,
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'link' => $this->link,
|
'link' => $this->link,
|
||||||
|
@ -4,21 +4,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace OCA\RePod\Core\EpisodeAction;
|
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\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
|
||||||
|
use OCA\RePod\Core\PodcastData\PodcastData;
|
||||||
use OCA\RePod\Service\UserService;
|
use OCA\RePod\Service\UserService;
|
||||||
|
|
||||||
class EpisodeActionReader
|
class EpisodeActionReader extends CoreEpisodeActionReader
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EpisodeActionRepository $episodeActionRepository,
|
private EpisodeActionRepository $episodeActionRepository,
|
||||||
private UserService $userService
|
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.
|
* 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.
|
* Specs : https://github.com/Podcast-Standards-Project/PSP-1-Podcast-RSS-Specification/blob/main/README.md.
|
||||||
@ -30,7 +27,6 @@ class EpisodeActionReader
|
|||||||
$episodes = [];
|
$episodes = [];
|
||||||
$xml = new \SimpleXMLElement($xmlString);
|
$xml = new \SimpleXMLElement($xmlString);
|
||||||
$channel = $xml->channel;
|
$channel = $xml->channel;
|
||||||
$podcast = (string) $channel->title;
|
|
||||||
|
|
||||||
// Find episode by url and add data for it
|
// Find episode by url and add data for it
|
||||||
/** @var \SimpleXMLElement $item */
|
/** @var \SimpleXMLElement $item */
|
||||||
@ -56,40 +52,40 @@ class EpisodeActionReader
|
|||||||
$name = (string) $item->title;
|
$name = (string) $item->title;
|
||||||
|
|
||||||
// Get episode link
|
// Get episode link
|
||||||
$link = $this->stringOrNull($item->link);
|
$link = PodcastData::stringOrNull($item->link);
|
||||||
|
|
||||||
// Get episode image
|
// Get episode image
|
||||||
$image = $this->stringOrNull($item->image->url);
|
$image = PodcastData::stringOrNull($item->image->url);
|
||||||
|
|
||||||
if (!$image && $iTunesItemChildren) {
|
if (!$image && $iTunesItemChildren) {
|
||||||
$imageAttributes = $iTunesItemChildren->image->attributes();
|
$imageAttributes = $iTunesItemChildren->image->attributes();
|
||||||
$image = $this->stringOrNull($imageAttributes ? (string) $imageAttributes->href : '');
|
$image = PodcastData::stringOrNull($imageAttributes ? (string) $imageAttributes->href : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$image) {
|
if (!$image) {
|
||||||
$image = $this->stringOrNull($channel->image->url);
|
$image = PodcastData::stringOrNull($channel->image->url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$image && $iTunesChannelChildren) {
|
if (!$image && $iTunesChannelChildren) {
|
||||||
$imageAttributes = $iTunesChannelChildren->image->attributes();
|
$imageAttributes = $iTunesChannelChildren->image->attributes();
|
||||||
$image = $this->stringOrNull($imageAttributes ? (string) $imageAttributes->href : '');
|
$image = PodcastData::stringOrNull($imageAttributes ? (string) $imageAttributes->href : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$image) {
|
if (!$image) {
|
||||||
preg_match('/<itunes:image\s+href="([^"]+)"/', $xmlString, $matches);
|
preg_match('/<itunes:image\s+href="([^"]+)"/', $xmlString, $matches);
|
||||||
$image = $this->stringOrNull($matches[1]);
|
$image = PodcastData::stringOrNull($matches[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get episode description
|
// Get episode description
|
||||||
$itemContent = $item->children('content', true);
|
$itemContent = $item->children('content', true);
|
||||||
if ($itemContent) {
|
if ($itemContent) {
|
||||||
$description = $this->stringOrNull($itemContent->encoded);
|
$description = PodcastData::stringOrNull($itemContent->encoded);
|
||||||
} else {
|
} else {
|
||||||
$description = $this->stringOrNull($item->description);
|
$description = PodcastData::stringOrNull($item->description);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$description && $iTunesItemChildren) {
|
if (!$description && $iTunesItemChildren) {
|
||||||
$description = $this->stringOrNull($iTunesItemChildren->summary);
|
$description = PodcastData::stringOrNull($iTunesItemChildren->summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove tags
|
// Remove tags
|
||||||
@ -97,9 +93,9 @@ class EpisodeActionReader
|
|||||||
|
|
||||||
// Get episode duration
|
// Get episode duration
|
||||||
if ($iTunesItemChildren) {
|
if ($iTunesItemChildren) {
|
||||||
$rawDuration = $this->stringOrNull($iTunesItemChildren->duration);
|
$rawDuration = PodcastData::stringOrNull($iTunesItemChildren->duration);
|
||||||
} else {
|
} else {
|
||||||
$rawDuration = $this->stringOrNull($item->duration);
|
$rawDuration = PodcastData::stringOrNull($item->duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
$splitDuration = array_reverse(explode(':', $rawDuration ?? ''));
|
$splitDuration = array_reverse(explode(':', $rawDuration ?? ''));
|
||||||
@ -108,11 +104,10 @@ class EpisodeActionReader
|
|||||||
$duration += !empty($splitDuration[2]) ? (int) $splitDuration[2] * 60 : 0;
|
$duration += !empty($splitDuration[2]) ? (int) $splitDuration[2] * 60 : 0;
|
||||||
|
|
||||||
// Get episode pubDate
|
// Get episode pubDate
|
||||||
$rawPubDate = $this->stringOrNull($item->pubDate);
|
$rawPubDate = PodcastData::stringOrNull($item->pubDate);
|
||||||
$pubDate = $rawPubDate ? new \DateTime($rawPubDate) : null;
|
$pubDate = $rawPubDate ? new \DateTime($rawPubDate) : null;
|
||||||
|
|
||||||
$episodes[] = new EpisodeActionExtraData(
|
$episodes[] = new EpisodeActionExtraData(
|
||||||
$podcast,
|
|
||||||
$url,
|
$url,
|
||||||
$name,
|
$name,
|
||||||
$link,
|
$link,
|
||||||
@ -130,15 +125,4 @@ class EpisodeActionReader
|
|||||||
|
|
||||||
return $episodes;
|
return $episodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param null|\SimpleXMLElement|string $value
|
|
||||||
*/
|
|
||||||
private function stringOrNull($value): ?string {
|
|
||||||
if ($value) {
|
|
||||||
return (string) $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
90
lib/Core/PodcastData/PodcastData.php
Normal file
90
lib/Core/PodcastData/PodcastData.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,8 @@
|
|||||||
:image-url="feed.imageUrl"
|
:image-url="feed.imageUrl"
|
||||||
:link="feed.link"
|
:link="feed.link"
|
||||||
:title="feed.title" />
|
:title="feed.title" />
|
||||||
<Episodes v-if="feed" />
|
<Episodes v-if="feed"
|
||||||
|
:title="feed.title" />
|
||||||
</NcAppContent>
|
</NcAppContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ import Episodes from '../components/Feed/Episodes.vue'
|
|||||||
import Loading from '../components/Atoms/Loading.vue'
|
import Loading from '../components/Atoms/Loading.vue'
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
import { toUrl } from '../utils/url.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Feed',
|
name: 'Feed',
|
||||||
@ -50,6 +52,11 @@ export default {
|
|||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const podcastData = await axios.get(generateUrl('/apps/repod/podcast?url={url}', { url: this.url }))
|
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
|
this.feed = podcastData.data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.failed = true
|
this.failed = true
|
||||||
|
Loading…
Reference in New Issue
Block a user