Update the Podcast RSS feed to write raw XML instead of using an RSS library; add iTunes season/episode.

This commit is contained in:
Buster Neece 2024-04-21 09:46:47 -05:00
parent ba4a71cd98
commit d03dc1f277
No known key found for this signature in database
3 changed files with 100 additions and 170 deletions

View File

@ -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",

60
composer.lock generated
View File

@ -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",

View File

@ -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;
}
}