diff --git a/lib/Controller/EpisodesController.php b/lib/Controller/EpisodesController.php index 8b76c16..48134fc 100644 --- a/lib/Controller/EpisodesController.php +++ b/lib/Controller/EpisodesController.php @@ -30,7 +30,7 @@ class EpisodesController extends Controller $episodes = $this->episodeActionReader->parseRssXml((string) $feed->getBody()); usort($episodes, fn (EpisodeActionExtraData $a, EpisodeActionExtraData $b) => $b->getFetchedAtUnix() <=> $a->getFetchedAtUnix()); - $episodes = array_intersect_key($episodes, array_unique(array_map(fn (EpisodeActionExtraData $episode) => $episode->getEpisodeGuid(), $episodes))); + $episodes = array_intersect_key($episodes, array_unique(array_map(fn (EpisodeActionExtraData $episode) => $episode->getGuid(), $episodes))); return new JSONResponse($episodes, $feed->getStatusCode()); } diff --git a/lib/Core/EpisodeAction/EpisodeActionExtraData.php b/lib/Core/EpisodeAction/EpisodeActionExtraData.php index a1775bf..e82acfd 100644 --- a/lib/Core/EpisodeAction/EpisodeActionExtraData.php +++ b/lib/Core/EpisodeAction/EpisodeActionExtraData.php @@ -7,81 +7,99 @@ namespace OCA\RePod\Core\EpisodeAction; use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction; /** - * https://github.com/pbek/nextcloud-nextpod/blob/main/lib/Core/EpisodeAction/EpisodeActionExtraData.php. + * Base: https://github.com/pbek/nextcloud-nextpod/blob/main/lib/Core/EpisodeAction/EpisodeActionExtraData.php. + * Specs: https://github.com/Podcast-Standards-Project/PSP-1-Podcast-RSS-Specification/blob/main/README.md#required-item-elements. * * @psalm-import-type EpisodeActionType from EpisodeAction * * @psalm-type EpisodeActionExtraDataType = array{ - * episodeUrl: ?string, - * podcastName: ?string, - * episodeName: ?string, - * episodeLink: ?string, - * episodeImage: ?string, - * episodeDescription: ?string, + * podcast: string, + * url: ?string, + * name: string, + * link: ?string, + * image: ?string, + * description: ?string, * fetchedAtUnix: int, - * episodeGuid: string, - * episodePubDate: ?\DateTime, - * episodeFilesize: ?int, - * episodeDuration: ?int, - * episodeAction: ?EpisodeActionType + * guid: string, + * type: ?string, + * size: ?int, + * pubDate: ?\DateTime, + * duration: ?int, + * action: ?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 string $podcast, + private ?string $url, + private string $name, + private ?string $link, + private ?string $image, + private ?string $description, private int $fetchedAtUnix, - private string $episodeGuid, - private ?\DateTime $episodePubDate, - private ?int $episodeFilesize, - private ?int $episodeDuration, - 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->episodeGuid = $episodeGuid; - $this->episodePubDate = $episodePubDate; - $this->episodeFilesize = $episodeFilesize; - $this->episodeDuration = $episodeDuration; - $this->episodeAction = $episodeAction; - } + private string $guid, + private ?string $type, + private ?int $size, + private ?\DateTime $pubDate, + private ?int $duration, + private ?EpisodeAction $action + ) {} public function __toString(): string { - return $this->episodeUrl ?? '/no episodeUrl/'; + return $this->url ?? '/no episodeUrl/'; } - public function getEpisodeGuid(): string { - return $this->episodeGuid; + public function getPodcast(): string { + return $this->podcast; } - public function getEpisodePubDate(): ?\DateTime { - return $this->episodePubDate; + public function getUrl(): ?string { + return $this->url; } - public function getEpisodeFilesize(): ?int { - return $this->episodeFilesize; + public function getName(): string { + return $this->name; } - public function getEpisodeDuration(): ?int { - return $this->episodeDuration; + public function getLink(): ?string { + return $this->link; } - public function getEpisodeAction(): ?EpisodeAction { - return $this->episodeAction; + public function getImage(): ?string { + return $this->image; } - public function getEpisodeUrl(): ?string { - return $this->episodeUrl; + public function getDescription(): ?string { + return $this->description; + } + + public function getFetchedAtUnix(): int { + return $this->fetchedAtUnix; + } + + public function getGuid(): string { + return $this->guid; + } + + public function getType(): ?string { + return $this->type; + } + + public function getSize(): ?int { + return $this->size; + } + + public function getPubDate(): ?\DateTime { + return $this->pubDate; + } + + public function getDuration(): ?int { + return $this->duration; + } + + public function getAction(): ?EpisodeAction { + return $this->action; } /** @@ -90,18 +108,19 @@ class EpisodeActionExtraData implements \JsonSerializable public function toArray(): array { return [ - 'podcastName' => $this->podcastName, - 'episodeUrl' => $this->episodeUrl, - 'episodeName' => $this->episodeName, - 'episodeLink' => $this->episodeLink, - 'episodeImage' => $this->episodeImage, - 'episodeDescription' => $this->episodeDescription, + 'podcast' => $this->podcast, + 'url' => $this->url, + 'name' => $this->name, + 'link' => $this->link, + 'image' => $this->image, + 'description' => $this->description, 'fetchedAtUnix' => $this->fetchedAtUnix, - 'episodeGuid' => $this->episodeGuid, - 'episodePubDate' => $this->episodePubDate, - 'episodeFilesize' => $this->episodeFilesize, - 'episodeDuration' => $this->episodeDuration, - 'episodeAction' => $this->episodeAction ? $this->episodeAction->toArray() : null, + 'guid' => $this->guid, + 'type' => $this->type, + 'size' => $this->size, + 'pubDate' => $this->pubDate, + 'duration' => $this->duration, + 'action' => $this->action ? $this->action->toArray() : null, ]; } @@ -111,24 +130,4 @@ class EpisodeActionExtraData implements \JsonSerializable 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; - } } diff --git a/lib/Core/EpisodeAction/EpisodeActionReader.php b/lib/Core/EpisodeAction/EpisodeActionReader.php index dad7366..81a6136 100644 --- a/lib/Core/EpisodeAction/EpisodeActionReader.php +++ b/lib/Core/EpisodeAction/EpisodeActionReader.php @@ -20,7 +20,8 @@ class EpisodeActionReader } /** - * 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. * * @return EpisodeActionExtraData[] * @throws \Exception if the XML data could not be parsed @@ -29,101 +30,104 @@ 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 */ foreach ($channel->item as $item) { - $episodeUrl = (string) $item->enclosure['url']; - - // Get episode guid - $episodeGuid = (string) $item->guid; - - // Get episode filesize - $episodeFilesize = (int) $item->enclosure['length']; + $url = (string) $item->enclosure['url']; + $type = (string) $item->enclosure['type']; + $size = (int) $item->enclosure['length']; + $guid = (string) $item->guid; + $rawDuration = $this->stringOrNull($item->duration); // Get episode action - $episodeAction = $this->episodeActionRepository->findByGuid($episodeGuid, $this->userService->getUserUID()); + $action = $this->episodeActionRepository->findByGuid($guid, $this->userService->getUserUID()); - if ($episodeAction) { - $episodeUrl = $episodeAction->getEpisode(); + if ($action) { + $url = $action->getEpisode(); } else { - $episodeAction = $this->episodeActionRepository->findByEpisodeUrl($episodeUrl, $this->userService->getUserUID()); + $action = $this->episodeActionRepository->findByEpisodeUrl($url, $this->userService->getUserUID()); } // Get episode name - $episodeName = $this->stringOrNull($item->title); + $name = (string) $item->title; // Get episode link - $episodeLink = $this->stringOrNull($item->link); + $link = $this->stringOrNull($item->link); // Get episode image - $episodeImage = $this->stringOrNull($channel->image->url); + $image = $this->stringOrNull($channel->image->url); - $episodeChildren = $item->children('http://www.itunes.com/dtds/podcast-1.0.dtd'); - if ($episodeChildren) { - $episodeImageAttributes = (array) $episodeChildren->image->attributes(); - $episodeImage = $this->stringOrNull(array_key_exists('href', $episodeImageAttributes) ? (string) $episodeImageAttributes['href'] : ''); - $iTunesChildren = $item->children('itunes', true); + $itemChildren = $item->children('http://www.itunes.com/dtds/podcast-1.0.dtd'); + if ($itemChildren) { + $imageAttributes = (array) $itemChildren->image->attributes(); + $image = $this->stringOrNull(array_key_exists('href', $imageAttributes) ? (string) $imageAttributes['href'] : ''); + $iTunesItemChildren = $item->children('itunes', true); + $iTunesChannelChildren = $channel->children('itunes', true); // Get episode duration - if ($iTunesChildren) { - $rawDuration = $this->stringOrNull((string) $iTunesChildren->duration); - $splitDuration = array_reverse(explode(':', $rawDuration ?? '')); - $episodeDuration = (int) $splitDuration[0]; - $episodeDuration += !empty($splitDuration[1]) ? (int) $splitDuration[1] * 60 : 0; - $episodeDuration += !empty($splitDuration[2]) ? (int) $splitDuration[2] * 60 : 0; + if ($iTunesItemChildren) { + $rawDuration = $this->stringOrNull($rawDuration ?? $iTunesItemChildren->duration); } - if ($iTunesChildren && !$episodeImage) { - $episodeImage = $this->stringOrNull((string) $iTunesChildren->image['href']); + if ($iTunesItemChildren && !$image) { + $image = $this->stringOrNull($iTunesItemChildren->image['href']); } - if ($iTunesChildren && !$episodeImage) { - $episodeImage = $this->stringOrNull((string) $iTunesChildren->image['href']); + if ($iTunesChannelChildren && !$image) { + $image = $this->stringOrNull($iTunesChannelChildren->image['href']); } - if (!$episodeImage) { + if (!$image) { $channelChildren = $channel->children('http://www.itunes.com/dtds/podcast-1.0.dtd'); if ($channelChildren) { - $episodeImageAttributes = (array) $channelChildren->image->attributes(); - $episodeImage = $this->stringOrNull(array_key_exists('href', $episodeImageAttributes) ? (string) $episodeImageAttributes['href'] : ''); + $imageAttributes = (array) $channelChildren->image->attributes(); + $image = $this->stringOrNull(array_key_exists('href', $imageAttributes) ? (string) $imageAttributes['href'] : ''); } } - if (!$episodeImage) { + if (!$image) { preg_match('/stringOrNull($matches[1]); + $image = $this->stringOrNull($matches[1]); } } // Get episode description - $episodeDescription = $this->stringOrNull($item->description); - - $episodeContentChildren = $item->children('content', true); - if ($episodeContentChildren) { - $episodeDescription = $this->stringOrNull($episodeContentChildren->encoded); + $itemContent = $item->children('content', true); + if ($itemContent) { + $description = $this->stringOrNull($itemContent->encoded); + } else { + $description = $this->stringOrNull($item->description); } // Remove tags - $episodeDescription = strip_tags($episodeDescription ?? ''); + $description = strip_tags($description ?? ''); // Get episode pubDate $rawPubDate = $this->stringOrNull($item->pubDate); - $episodePubDate = $rawPubDate ? new \DateTime($rawPubDate) : null; + $pubDate = $rawPubDate ? new \DateTime($rawPubDate) : null; + + // Get episode duration + $splitDuration = array_reverse(explode(':', $rawDuration ?? '')); + $duration = (int) $splitDuration[0]; + $duration += !empty($splitDuration[1]) ? (int) $splitDuration[1] * 60 : 0; + $duration += !empty($splitDuration[2]) ? (int) $splitDuration[2] * 60 : 0; $episodes[] = new EpisodeActionExtraData( - $episodeUrl, - $this->stringOrNull($channel->title), - $episodeName, - $episodeLink, - $episodeImage, - $episodeDescription, + $podcast, + $url, + $name, + $link, + $image, + $description, $fetchedAtUnix ?? (new \DateTime())->getTimestamp(), - $episodeGuid, - $episodePubDate, - $episodeFilesize, - $episodeDuration ?? null, - $episodeAction + $guid, + $type, + $size, + $pubDate, + $duration, + $action ); } diff --git a/src/components/Feed/Episodes.vue b/src/components/Feed/Episodes.vue index ffd4656..b1f4da0 100644 --- a/src/components/Feed/Episodes.vue +++ b/src/components/Feed/Episodes.vue @@ -3,21 +3,21 @@ @@ -92,7 +92,7 @@ export default { try { this.loading = true const episodes = await axios.get(generateUrl('/apps/repod/episodes/list?url={url}', { url: this.url })) - this.episodes = [...episodes.data].sort((a, b) => a.episodePubDate.date < b.episodePubDate.date) + this.episodes = episodes.data } catch (e) { console.error(e) showError(t('repod', 'Could not fetch episodes')) @@ -104,7 +104,7 @@ export default { formatTimer, formatDistanceToNow, isCurrentEpisode(episode) { - return this.currentEpisode && this.currentEpisode.episodeUrl === episode.episodeUrl + return this.currentEpisode && this.currentEpisode.url === episode.url }, load(episode) { this.$store.dispatch('player/load', episode) diff --git a/src/components/Feed/Modal.vue b/src/components/Feed/Modal.vue index 1fb854a..e70f003 100644 --- a/src/components/Feed/Modal.vue +++ b/src/components/Feed/Modal.vue @@ -1,20 +1,20 @@