EpisodeActionExtra done
All checks were successful
repod / nextcloud (push) Successful in 1m51s
repod / nodejs (push) Successful in 2m43s

This commit is contained in:
Michel Roux 2023-08-24 12:48:10 +02:00
parent 3aae3c012f
commit 3f9bc372ce
10 changed files with 392 additions and 3 deletions

View File

@ -13,7 +13,7 @@ declare(strict_types=1);
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'fetch#index', 'url' => '/fetch', 'verb' => 'GET'],
['name' => 'podcast#index', 'url' => '/podcast', 'verb' => 'GET'],
['name' => 'search#index', 'url' => '/search', 'verb' => 'GET'],
['name' => 'toplist#index', 'url' => '/toplist', 'verb' => 'GET'],
],

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace OCA\RePod\Controller;
use OCA\RePod\AppInfo\Application;
use OCP\AppFramework\Controller;
use OCP\IRequest;
class EpisodesController extends Controller
{
public function __construct(IRequest $request)
{
parent::__construct(Application::APP_ID, $request);
}
}

View File

@ -13,7 +13,7 @@ use OCP\AppFramework\Http\JSONResponse;
use OCP\Http\Client\IClientService;
use OCP\IRequest;
class FetchController extends Controller
class PodcastController extends Controller
{
public function __construct(
IRequest $request,

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace OCA\RePod\Core\EpisodeAction;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
/**
* https://github.com/pbek/nextcloud-nextpod/blob/main/lib/Core/EpisodeAction/EpisodeActionExtraData.php.
*
* @psalm-import-type EpisodeActionType from EpisodeAction
*
* @psalm-type EpisodeActionExtraDataType = array{
* episodeUrl: ?string,
* podcastName: ?string,
* episodeName: ?string,
* episodeLink: ?string,
* episodeImage: ?string,
* episodeDescription: ?string,
* fetchedAtUnix: int,
* episodeAction: ?EpisodeActionType
* }
*/
class EpisodeActionExtraData implements \JsonSerializable
{
public function __construct(
private ?string $episodeUrl,
private ?string $podcastName,
private ?string $episodeName,
private ?string $episodeLink,
private ?string $episodeImage,
private ?string $episodeDescription,
private int $fetchedAtUnix,
private ?EpisodeAction $episodeAction
) {
$this->episodeUrl = $episodeUrl;
$this->podcastName = $podcastName;
$this->episodeName = $episodeName;
$this->episodeLink = $episodeLink;
$this->episodeImage = $episodeImage;
$this->episodeDescription = $episodeDescription;
$this->fetchedAtUnix = $fetchedAtUnix;
$this->episodeAction = $episodeAction;
}
public function __toString(): string
{
return $this->episodeUrl ?? '/no episodeUrl/';
}
public function getEpisodeAction(): ?EpisodeAction
{
return $this->episodeAction;
}
public function getEpisodeUrl(): ?string
{
return $this->episodeUrl;
}
/**
* @return EpisodeActionExtraDataType
*/
public function toArray(): array
{
return
[
'podcastName' => $this->podcastName,
'episodeUrl' => $this->episodeUrl,
'episodeName' => $this->episodeName,
'episodeLink' => $this->episodeLink,
'episodeImage' => $this->episodeImage,
'episodeDescription' => $this->episodeDescription,
'fetchedAtUnix' => $this->fetchedAtUnix,
'episodeAction' => $this->episodeAction ? $this->episodeAction->toArray() : null,
];
}
/**
* @return EpisodeActionExtraDataType
*/
public function jsonSerialize(): mixed
{
return $this->toArray();
}
public function getPodcastName(): ?string
{
return $this->podcastName;
}
public function getEpisodeName(): ?string
{
return $this->episodeName;
}
public function getEpisodeLink(): ?string
{
return $this->episodeLink;
}
public function getFetchedAtUnix(): int
{
return $this->fetchedAtUnix;
}
public function getEpisodeImage(): ?string
{
return $this->episodeImage;
}
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace OCA\RePod\Core\EpisodeAction;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCA\RePod\Service\UserService;
class EpisodeActionReader
{
public function __construct(
private EpisodeActionRepository $episodeActionRepository,
private UserService $userService
) {
}
/**
* https://github.com/pbek/nextcloud-nextpod/blob/main/lib/Core/EpisodeAction/EpisodeActionExtraData.php#L119.
*
* @throws \Exception if the XML data could not be parsed
*/
public function parseRssXml(string $xmlString, string $episodeUrl, ?int $fetchedAtUnix = null): array
{
$episodes = [];
$xml = new \SimpleXMLElement($xmlString);
$channel = $xml->channel;
$episodeName = null;
$episodeLink = null;
$episodeImage = null;
$episodeDescription = null;
$episodeUrlPath = parse_url($episodeUrl, PHP_URL_PATH);
// Find episode by url and add data for it
/** @var \SimpleXMLElement $item */
foreach ($channel->item as $item) {
$url = (string) $item->enclosure['url'];
// First try to match the url directly
if (false === strpos($episodeUrl, $url)) {
// Then try to match the path only
// The podcast http://feeds.feedburner.com/abcradio/10percenthappier has a "rss_browser" query parameter
// for every item that changed all the time, so we can't match the full url
$path = parse_url($url, PHP_URL_PATH);
if ($episodeUrlPath !== $path) {
continue;
}
}
// Get episode action
$episodeAction = $this->episodeActionRepository->findByEpisodeUrl($url, $this->userService->getUserUID());
// Get episode name
$episodeName = $this->stringOrNull($item->title);
// Get episode link
$episodeLink = $this->stringOrNull($item->link);
// Get episode image
$episodeImageChildren = $item->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
if ($episodeImageChildren) {
$episodeImageAttributes = (array) $episodeImageChildren->image->attributes();
$episodeImage = $this->stringOrNull(array_key_exists('href', $episodeImageAttributes) ? (string) $episodeImageAttributes['href'] : '');
$iTunesChildren = $item->children('itunes', true);
if ($iTunesChildren && !$episodeImage) {
$episodeImage = $this->stringOrNull((string) $iTunesChildren->image['href']);
}
if (!$episodeImage) {
$episodeImage = $this->stringOrNull($channel->image->url);
}
if ($iTunesChildren && !$episodeImage) {
$episodeImage = $this->stringOrNull((string) $iTunesChildren->image['href']);
}
if (!$episodeImage) {
$channelImageChildren = $channel->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
if ($channelImageChildren) {
$episodeImageAttributes = (array) $channelImageChildren->image->attributes();
$episodeImage = $this->stringOrNull(array_key_exists('href', $episodeImageAttributes) ? (string) $episodeImageAttributes['href'] : '');
}
}
if (!$episodeImage) {
preg_match('/<itunes:image\s+href="([^"]+)"/', $xmlString, $matches);
$episodeImage = $this->stringOrNull($matches[1]);
}
}
// Get episode description
$episodeContentChildren = $item->children('content', true);
if ($episodeContentChildren) {
$episodeDescription = $this->stringOrNull($episodeContentChildren->encoded);
}
if (!$episodeDescription) {
$episodeDescription = $this->stringOrNull($item->description);
}
// Open links in new browser window/tab
$episodeDescription = str_replace('<a ', '<a class="description-link" target="_blank" ', $episodeDescription ?? '');
$episodes[] = new EpisodeActionExtraData(
$episodeUrl,
$this->stringOrNull($channel->title),
$episodeName,
$episodeLink,
$episodeImage,
$episodeDescription,
$fetchedAtUnix ?? (new \DateTime())->getTimestamp(),
$episodeAction
);
}
return $episodes;
}
private function stringOrNull(mixed $value): ?string
{
if ($value) {
return (string) $value;
}
return null;
}
}

View File

@ -44,7 +44,7 @@ export default {
},
async mounted() {
try {
const podcastData = await axios.get(generateUrl('/apps/repod/fetch?url={url}', { url: this.url }))
const podcastData = await axios.get(generateUrl('/apps/repod/podcast?url={url}', { url: this.url }))
this.feed = podcastData.data.data
} catch (e) {
this.failed = true

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\SubscriptionChange;
use OCP\AppFramework\Db\Entity;
class SubscriptionChangeEntity extends Entity implements \JsonSerializable
{
/**
* @return array<string,mixed>
*/
public function jsonSerialize(): mixed
{
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\SubscriptionChange;
use OCP\AppFramework\Db\QBMapper;
use OCP\IDBConnection;
/**
* @template-extends QBMapper<SubscriptionChangeEntity>
*/
class SubscriptionChangeMapper extends QBMapper
{
public function __construct(IDBConnection $db)
{
}
/**
* @return SubscriptionChangeEntity[]
*/
public function findAll(string $userId)
{
}
/**
* @return ?SubscriptionChangeEntity
*/
public function findByUrl(string $url, string $userId)
{
}
public function remove(SubscriptionChangeEntity $subscriptionChangeEntity): void
{
}
/**
* @return SubscriptionChangeEntity[]
*/
public function findAllSubscriptionState(bool $subscribed, \DateTime $sinceTimestamp, string $userId)
{
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\SubscriptionChange;
class SubscriptionChangeRepository
{
public function __construct(private SubscriptionChangeMapper $subscriptionChangeMapper)
{
}
/**
* @return SubscriptionChangeEntity[]
*/
public function findAll()
{
}
/**
* @return ?SubscriptionChangeEntity
*/
public function findByUrl(string $episode, string $userId)
{
}
/**
* @return SubscriptionChangeEntity[]
*/
public function findAllSubscribed(\DateTime $sinceTimestamp, string $userId)
{
}
/**
* @return SubscriptionChangeEntity[]
*/
public function findAllUnSubscribed(\DateTime $sinceTimestamp, string $userId)
{
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\SubscriptionChange;
class SubscriptionChangeWriter
{
public function __construct(private SubscriptionChangeMapper $subscriptionChangeMapper)
{
}
public function purge(): void
{
}
/**
* @return SubscriptionChangeEntity
*/
public function create(SubscriptionChangeEntity $subscriptionChangeEntity)
{
}
/**
* @return SubscriptionChangeEntity
*/
public function update(SubscriptionChangeEntity $subscriptionChangeEntity)
{
}
}