4
0
mirror of https://github.com/AzuraCast/AzuraCast.git synced 2024-06-14 13:16:37 +00:00

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/oauth2-client": "^2.6",
"league/plates": "^3.1", "league/plates": "^3.1",
"lstrojny/fxmlrpc": "dev-master", "lstrojny/fxmlrpc": "dev-master",
"marcw/rss-writer": "^0.4.0",
"matomo/device-detector": "^6", "matomo/device-detector": "^6",
"mezzio/mezzio-session": "^1.3", "mezzio/mezzio-session": "^1.3",
"mezzio/mezzio-session-cache": "^1.7", "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "5bca0c641ba21645d05ab830394898ae", "content-hash": "77d8e4e6837ec47f02ea21d3f3cb767e",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -3505,64 +3505,6 @@
}, },
"time": "2023-08-22T06:06:43+00:00" "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", "name": "matomo/device-detector",
"version": "6.3.0", "version": "6.3.0",

View File

@ -7,32 +7,15 @@ namespace App\Controller\Frontend\PublicPages;
use App\Controller\SingleActionInterface; use App\Controller\SingleActionInterface;
use App\Entity\ApiGenerator\PodcastApiGenerator; use App\Entity\ApiGenerator\PodcastApiGenerator;
use App\Entity\ApiGenerator\PodcastEpisodeApiGenerator; use App\Entity\ApiGenerator\PodcastEpisodeApiGenerator;
use App\Entity\Podcast;
use App\Entity\PodcastCategory; use App\Entity\PodcastCategory;
use App\Entity\PodcastEpisode; use App\Entity\PodcastEpisode;
use App\Exception\NotFoundException; use App\Exception\NotFoundException;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Rss\PodcastNamespaceWriter; use App\Xml\Writer;
use Carbon\CarbonImmutable;
use DateTime; 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\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; use Psr\Http\Message\ResponseInterface;
final class PodcastFeedAction implements SingleActionInterface final class PodcastFeedAction implements SingleActionInterface
@ -62,33 +45,74 @@ final class PodcastFeedAction implements SingleActionInterface
// Fetch podcast API feed. // Fetch podcast API feed.
$podcastApi = $this->podcastApiGenerator->__invoke($podcast, $request); $podcastApi = $this->podcastApiGenerator->__invoke($podcast, $request);
$channel->setTitle($podcastApi->title); $now = CarbonImmutable::now($station->getTimezoneObject());
$channel->setDescription($podcastApi->description);
$channel->setLink($podcastApi->link ?? $podcastApi->links['self']);
$channel->setLanguage($podcastApi->language);
$channel->setCategories( $rss = [
$podcast->getCategories()->map( '@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) { function (PodcastCategory $podcastCategory) {
$rssCategory = new RssCategory(); return (null === $podcastCategory->getSubTitle())
if (null === $podcastCategory->getSubTitle()) { ? $podcastCategory->getTitle()
$rssCategory->setTitle($podcastCategory->getTitle()); : $podcastCategory->getSubTitle();
} else {
$rssCategory->setTitle($podcastCategory->getSubTitle());
}
return $rssCategory;
} }
)->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) { 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. // Iterate through episodes.
$hasPublishedEpisode = false; $hasPublishedEpisode = false;
@ -105,53 +129,21 @@ final class PodcastFeedAction implements SingleActionInterface
$hasExplicitEpisode = true; $hasExplicitEpisode = true;
} }
$channel->addItem($this->buildItemForEpisode($episode, $request)); $channel['item'][] = $this->buildItemForEpisode($episode, $request);
} }
if (!$hasPublishedEpisode) { if (!$hasPublishedEpisode) {
throw NotFoundException::podcast(); throw NotFoundException::podcast();
} }
$itunesChannel = new ItunesChannel(); if ($hasExplicitEpisode) {
$itunesChannel->setExplicit($hasExplicitEpisode); $channel['itunes:explicit'] = 'true';
$itunesChannel->setImage($rssImage->getUrl()); }
$itunesChannel->setCategories(
$podcast->getCategories()->map(
function (PodcastCategory $podcastCategory) {
return (null === $podcastCategory->getSubTitle())
? $podcastCategory->getTitle()
: [
$podcastCategory->getTitle(),
$podcastCategory->getSubTitle(),
];
}
)->getValues()
);
$itunesChannel->setOwner($this->buildItunesOwner($podcast)); $rss['channel'] = $channel;
$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);
$response->getBody()->write( $response->getBody()->write(
$rssWriter->writeChannel($channel) Writer::toString($rss, 'rss')
); );
return $response return $response
@ -159,48 +151,45 @@ final class PodcastFeedAction implements SingleActionInterface
->withHeader('X-Robots-Tag', 'index, nofollow'); ->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); $episodeApi = $this->episodeApiGenerator->__invoke($episode, $request);
$rssItem = new RssItem(); $publishedAt = CarbonImmutable::createFromTimestamp($episodeApi->publish_at, $station->getTimezoneObject());
$rssItem->setGuid((new RssGuid())->setGuid($episodeApi->id)); $item = [
$rssItem->setTitle($episodeApi->title); 'title' => $episodeApi->title,
$rssItem->setDescription($episodeApi->description); 'link' => $episodeApi->link ?? $episodeApi->links['self'],
$rssItem->setLink($episodeApi->link ?? $episodeApi->links['self']); 'description' => $episodeApi->description,
'enclosure' => [
$rssItem->setPubDate((new DateTime())->setTimestamp($episode->getPublishAt())); '@url' => $episodeApi->links['download'],
],
$rssEnclosure = new RssEnclosure(); 'guid' => [
$rssEnclosure->setUrl($episodeApi->links['download']); '@isPermaLink' => 'false',
'_' => $episodeApi->id,
],
'pubDate' => $publishedAt->toRssString(),
'itunes:image' => [
'@href' => $episodeApi->art,
],
'itunes:explicit' => $episodeApi->explicit ? 'true' : 'false',
];
$podcastMedia = $episode->getMedia(); $podcastMedia = $episode->getMedia();
if (null !== $podcastMedia) { if (null !== $podcastMedia) {
$rssEnclosure->setType($podcastMedia->getMimeType()); $item['enclosure']['@length'] = $podcastMedia->getLength();
$rssEnclosure->setLength($podcastMedia->getLength()); $item['enclosure']['@type'] = $podcastMedia->getMimeType();
}
$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;
} }
$itunesOwner = new ItunesOwner(); if (null !== $episodeApi->season_number) {
$itunesOwner->setName($podcast->getAuthor()); $item['itunes:season'] = (string)$episodeApi->season_number;
$itunesOwner->setEmail($podcast->getEmail()); }
if (null !== $episodeApi->episode_number) {
$item['itunes:episode'] = (string)$episodeApi->episode_number;
}
return $itunesOwner; return $item;
} }
} }