diff --git a/composer.json b/composer.json index 833ce59c5..13999e9df 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,6 @@ "league/oauth2-client": "^2.6", "league/plates": "^3.1", "lstrojny/fxmlrpc": "dev-master", - "marcw/rss-writer": "^0.4.0", "matomo/device-detector": "^6", "mezzio/mezzio-session": "^1.3", "mezzio/mezzio-session-cache": "^1.7", diff --git a/composer.lock b/composer.lock index 27fe31d0f..61eb32361 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5bca0c641ba21645d05ab830394898ae", + "content-hash": "77d8e4e6837ec47f02ea21d3f3cb767e", "packages": [ { "name": "aws/aws-crt-php", @@ -3505,64 +3505,6 @@ }, "time": "2023-08-22T06:06:43+00:00" }, - { - "name": "marcw/rss-writer", - "version": "0.4.0", - "source": { - "type": "git", - "url": "https://github.com/marcw/rss-writer.git", - "reference": "4bbd63aea62246fe43bec589a1e8bdda2f4ef219" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/marcw/rss-writer/zipball/4bbd63aea62246fe43bec589a1e8bdda2f4ef219", - "reference": "4bbd63aea62246fe43bec589a1e8bdda2f4ef219", - "shasum": "" - }, - "require": { - "ext-xmlwriter": "*" - }, - "require-dev": { - "phpunit/phpunit": "^5.4", - "symfony/debug": "^3.1", - "symfony/http-foundation": "^3.1", - "symfony/validator": "^3.1", - "symfony/var-dumper": "^3.1" - }, - "suggest": { - "symfony/http-foundation": "Enable streaming RSS response", - "symfony/validator": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "MarcW\\RssWriter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marc Weistroff", - "email": "marc@weistroff.net" - } - ], - "description": "A simple yet powerful RSS2 feed writer with RSS extensions support (like iTunes podcast tags)", - "keywords": [ - "feed", - "podcast", - "podcasting", - "rss", - "rss2" - ], - "support": { - "issues": "https://github.com/marcw/rss-writer/issues", - "source": "https://github.com/marcw/rss-writer/tree/master" - }, - "time": "2017-04-01T11:53:47+00:00" - }, { "name": "matomo/device-detector", "version": "6.3.0", diff --git a/src/Controller/Frontend/PublicPages/PodcastFeedAction.php b/src/Controller/Frontend/PublicPages/PodcastFeedAction.php index a672fddfa..58fc63388 100644 --- a/src/Controller/Frontend/PublicPages/PodcastFeedAction.php +++ b/src/Controller/Frontend/PublicPages/PodcastFeedAction.php @@ -7,32 +7,15 @@ namespace App\Controller\Frontend\PublicPages; use App\Controller\SingleActionInterface; use App\Entity\ApiGenerator\PodcastApiGenerator; use App\Entity\ApiGenerator\PodcastEpisodeApiGenerator; -use App\Entity\Podcast; use App\Entity\PodcastCategory; use App\Entity\PodcastEpisode; use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; -use App\Rss\PodcastNamespaceWriter; +use App\Xml\Writer; +use Carbon\CarbonImmutable; use DateTime; -use MarcW\RssWriter\Extension\Atom\AtomLink; -use MarcW\RssWriter\Extension\Atom\AtomWriter; -use MarcW\RssWriter\Extension\Core\Category as RssCategory; use MarcW\RssWriter\Extension\Core\Channel as RssChannel; -use MarcW\RssWriter\Extension\Core\CoreWriter; -use MarcW\RssWriter\Extension\Core\Enclosure as RssEnclosure; -use MarcW\RssWriter\Extension\Core\Guid as RssGuid; -use MarcW\RssWriter\Extension\Core\Image as RssImage; -use MarcW\RssWriter\Extension\Core\Item as RssItem; -use MarcW\RssWriter\Extension\Itunes\ItunesChannel; -use MarcW\RssWriter\Extension\Itunes\ItunesItem; -use MarcW\RssWriter\Extension\Itunes\ItunesOwner; -use MarcW\RssWriter\Extension\Itunes\ItunesWriter; -use MarcW\RssWriter\Extension\Slash\Slash; -use MarcW\RssWriter\Extension\Slash\SlashWriter; -use MarcW\RssWriter\Extension\Sy\Sy; -use MarcW\RssWriter\Extension\Sy\SyWriter; -use MarcW\RssWriter\RssWriter; use Psr\Http\Message\ResponseInterface; final class PodcastFeedAction implements SingleActionInterface @@ -62,33 +45,74 @@ final class PodcastFeedAction implements SingleActionInterface // Fetch podcast API feed. $podcastApi = $this->podcastApiGenerator->__invoke($podcast, $request); - $channel->setTitle($podcastApi->title); - $channel->setDescription($podcastApi->description); - $channel->setLink($podcastApi->link ?? $podcastApi->links['self']); - $channel->setLanguage($podcastApi->language); + $now = CarbonImmutable::now($station->getTimezoneObject()); - $channel->setCategories( - $podcast->getCategories()->map( + $rss = [ + '@xmlns:itunes' => 'http://www.itunes.com/dtds/podcast-1.0.dtd', + '@xmlns:sy' => 'http://purl.org/rss/1.0/modules/syndication/', + '@xmlns:slash' => 'http://purl.org/rss/1.0/modules/slash/', + '@xmlns:atom' => 'http://www.w3.org/2005/Atom', + '@xmlns:podcast' => 'https://podcastindex.org/namespace/1.0', + '@version' => '2.0', + ]; + + $channel = [ + 'title' => $podcastApi->title, + 'link' => $podcastApi->link ?? $podcastApi->links['self'], + 'description' => $podcastApi->description, + 'language' => $podcastApi->language, + 'lastBuildDate' => $now->toRssString(), + 'category' => $podcast->getCategories()->map( function (PodcastCategory $podcastCategory) { - $rssCategory = new RssCategory(); - if (null === $podcastCategory->getSubTitle()) { - $rssCategory->setTitle($podcastCategory->getTitle()); - } else { - $rssCategory->setTitle($podcastCategory->getSubTitle()); - } - return $rssCategory; + return (null === $podcastCategory->getSubTitle()) + ? $podcastCategory->getTitle() + : $podcastCategory->getSubTitle(); } - )->getValues() - ); + )->getValues(), + 'ttl' => 5, + 'image' => [ + 'url' => $podcastApi->art, + 'title' => $podcastApi->title, + ], + 'itunes:author' => $podcastApi->author, + 'itunes:owner' => [], + 'itunes:image' => [ + '@href' => $podcastApi->art, + ], + 'itunes:explicit' => 'false', + 'itunes:category' => $podcast->getCategories()->map( + function (PodcastCategory $podcastCategory) { + return (null === $podcastCategory->getSubTitle()) + ? [ + '@text' => $podcastCategory->getTitle(), + ] : [ + '@text' => $podcastCategory->getTitle(), + 'itunes:category' => [ + '@text' => $podcastCategory->getSubTitle(), + ], + ]; + } + )->getValues(), + 'atom:link' => [ + '@rel' => 'self', + '@type' => 'application/rss+xml', + '@href' => (string)$request->getUri(), + ], + 'item' => [], + ]; - $rssImage = new RssImage(); - $rssImage->setTitle($podcastApi->title); - $rssImage->setUrl($podcastApi->art); if (null !== $podcastApi->link) { - $rssImage->setLink($podcastApi->link); + $channel['image']['link'] = $podcastApi->link; } - $channel->setImage($rssImage); + if (empty($podcastApi->author) && empty($podcastApi->email)) { + unset($channel['itunes:owner']); + } else { + $channel['itunes:owner'] = [ + 'itunes:name' => $podcastApi->author, + 'itunes:email' => $podcastApi->email, + ]; + } // Iterate through episodes. $hasPublishedEpisode = false; @@ -105,53 +129,21 @@ final class PodcastFeedAction implements SingleActionInterface $hasExplicitEpisode = true; } - $channel->addItem($this->buildItemForEpisode($episode, $request)); + $channel['item'][] = $this->buildItemForEpisode($episode, $request); } if (!$hasPublishedEpisode) { throw NotFoundException::podcast(); } - $itunesChannel = new ItunesChannel(); - $itunesChannel->setExplicit($hasExplicitEpisode); - $itunesChannel->setImage($rssImage->getUrl()); - $itunesChannel->setCategories( - $podcast->getCategories()->map( - function (PodcastCategory $podcastCategory) { - return (null === $podcastCategory->getSubTitle()) - ? $podcastCategory->getTitle() - : [ - $podcastCategory->getTitle(), - $podcastCategory->getSubTitle(), - ]; - } - )->getValues() - ); + if ($hasExplicitEpisode) { + $channel['itunes:explicit'] = 'true'; + } - $itunesChannel->setOwner($this->buildItunesOwner($podcast)); - $itunesChannel->setAuthor($podcast->getAuthor()); - - $channel->addExtension($itunesChannel); - $channel->addExtension(new Sy()); - $channel->addExtension(new Slash()); - $channel->addExtension( - (new AtomLink()) - ->setRel('self') - ->setHref((string)$request->getUri()) - ->setType('application/rss+xml') - ); - - $rssWriter = new RssWriter(null, [ - new CoreWriter(), - new ItunesWriter(), - new SyWriter(), - new SlashWriter(), - new AtomWriter(), - new PodcastNamespaceWriter(), - ], true); + $rss['channel'] = $channel; $response->getBody()->write( - $rssWriter->writeChannel($channel) + Writer::toString($rss, 'rss') ); return $response @@ -159,48 +151,45 @@ final class PodcastFeedAction implements SingleActionInterface ->withHeader('X-Robots-Tag', 'index, nofollow'); } - private function buildItemForEpisode(PodcastEpisode $episode, ServerRequest $request): RssItem + private function buildItemForEpisode(PodcastEpisode $episode, ServerRequest $request): array { + $station = $request->getStation(); + $episodeApi = $this->episodeApiGenerator->__invoke($episode, $request); - $rssItem = new RssItem(); + $publishedAt = CarbonImmutable::createFromTimestamp($episodeApi->publish_at, $station->getTimezoneObject()); - $rssItem->setGuid((new RssGuid())->setGuid($episodeApi->id)); - $rssItem->setTitle($episodeApi->title); - $rssItem->setDescription($episodeApi->description); - $rssItem->setLink($episodeApi->link ?? $episodeApi->links['self']); - - $rssItem->setPubDate((new DateTime())->setTimestamp($episode->getPublishAt())); - - $rssEnclosure = new RssEnclosure(); - $rssEnclosure->setUrl($episodeApi->links['download']); + $item = [ + 'title' => $episodeApi->title, + 'link' => $episodeApi->link ?? $episodeApi->links['self'], + 'description' => $episodeApi->description, + 'enclosure' => [ + '@url' => $episodeApi->links['download'], + ], + 'guid' => [ + '@isPermaLink' => 'false', + '_' => $episodeApi->id, + ], + 'pubDate' => $publishedAt->toRssString(), + 'itunes:image' => [ + '@href' => $episodeApi->art, + ], + 'itunes:explicit' => $episodeApi->explicit ? 'true' : 'false', + ]; $podcastMedia = $episode->getMedia(); if (null !== $podcastMedia) { - $rssEnclosure->setType($podcastMedia->getMimeType()); - $rssEnclosure->setLength($podcastMedia->getLength()); - } - $rssItem->setEnclosure($rssEnclosure); - - $rssItem->addExtension( - (new ItunesItem()) - ->setExplicit($episode->getExplicit()) - ->setImage($episodeApi->art) - ); - - return $rssItem; - } - - private function buildItunesOwner(Podcast $podcast): ?ItunesOwner - { - if (empty($podcast->getAuthor()) && empty($podcast->getEmail())) { - return null; + $item['enclosure']['@length'] = $podcastMedia->getLength(); + $item['enclosure']['@type'] = $podcastMedia->getMimeType(); } - $itunesOwner = new ItunesOwner(); - $itunesOwner->setName($podcast->getAuthor()); - $itunesOwner->setEmail($podcast->getEmail()); + if (null !== $episodeApi->season_number) { + $item['itunes:season'] = (string)$episodeApi->season_number; + } + if (null !== $episodeApi->episode_number) { + $item['itunes:episode'] = (string)$episodeApi->episode_number; + } - return $itunesOwner; + return $item; } }