#7009 -- Make RSS feed use API generator, add extension to download for iTunes.
This commit is contained in:
parent
3410442f24
commit
b144860d63
|
@ -118,7 +118,7 @@ return static function (RouteCollectorProxy $group) {
|
|||
->add(new Middleware\Cache\SetStaticFileCache());
|
||||
|
||||
$group->get(
|
||||
'/download',
|
||||
'/download[.{extension}]',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Media\GetMediaAction::class
|
||||
)->setName('api:stations:public:podcast:episode:download');
|
||||
}
|
||||
|
|
|
@ -69,7 +69,8 @@ return static function (RouteCollectorProxy $app) {
|
|||
}
|
||||
|
||||
$group->get('/podcast/{podcast_id}/feed', Controller\Frontend\PublicPages\PodcastFeedAction::class)
|
||||
->setName('public:podcast:feed');
|
||||
->setName('public:podcast:feed')
|
||||
->add(Middleware\GetAndRequirePodcast::class);
|
||||
}
|
||||
)
|
||||
->add(Middleware\EnableView::class)
|
||||
|
|
|
@ -5,19 +5,15 @@ declare(strict_types=1);
|
|||
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\Entity\Repository\PodcastRepository;
|
||||
use App\Entity\Repository\StationRepository;
|
||||
use App\Entity\Station;
|
||||
use App\Exception\NotFoundException;
|
||||
use App\Flysystem\StationFilesystems;
|
||||
use App\Http\Response;
|
||||
use App\Http\RouterInterface;
|
||||
use App\Http\ServerRequest;
|
||||
use DateTime;
|
||||
use GuzzleHttp\Psr7\UriResolver;
|
||||
use MarcW\RssWriter\Extension\Atom\AtomLink;
|
||||
use MarcW\RssWriter\Extension\Atom\AtomWriter;
|
||||
use MarcW\RssWriter\Extension\Core\Category as RssCategory;
|
||||
|
@ -40,12 +36,9 @@ use Psr\Http\Message\ResponseInterface;
|
|||
|
||||
final class PodcastFeedAction implements SingleActionInterface
|
||||
{
|
||||
private RouterInterface $router;
|
||||
|
||||
public function __construct(
|
||||
private readonly StationRepository $stationRepository,
|
||||
private readonly PodcastRepository $podcastRepository,
|
||||
private readonly StationFilesystems $stationFilesystems
|
||||
private readonly PodcastApiGenerator $podcastApiGenerator,
|
||||
private readonly PodcastEpisodeApiGenerator $episodeApiGenerator
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -54,112 +47,125 @@ final class PodcastFeedAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string $podcastId */
|
||||
$podcastId = $params['podcast_id'];
|
||||
|
||||
$this->router = $request->getRouter();
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!$station->getEnablePublicPage()) {
|
||||
throw NotFoundException::station();
|
||||
}
|
||||
|
||||
$podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId);
|
||||
$podcast = $request->getPodcast();
|
||||
|
||||
if ($podcast === null) {
|
||||
throw NotFoundException::podcast();
|
||||
}
|
||||
|
||||
if (!$this->checkHasPublishedEpisodes($podcast)) {
|
||||
throw NotFoundException::podcast();
|
||||
}
|
||||
|
||||
$generatedRss = $this->generateRssFeed($podcast, $station, $request);
|
||||
|
||||
$response->getBody()->write($generatedRss);
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/rss+xml')
|
||||
->withHeader('X-Robots-Tag', 'index, nofollow');
|
||||
}
|
||||
|
||||
private function checkHasPublishedEpisodes(Podcast $podcast): bool
|
||||
{
|
||||
/** @var PodcastEpisode $episode */
|
||||
foreach ($podcast->getEpisodes() as $episode) {
|
||||
if ($episode->isPublished()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function generateRssFeed(
|
||||
Podcast $podcast,
|
||||
Station $station,
|
||||
ServerRequest $serverRequest
|
||||
): string {
|
||||
$rssWriter = $this->createRssWriter();
|
||||
|
||||
$channel = $this->buildRssChannelForPodcast($podcast, $station, $serverRequest);
|
||||
|
||||
return $rssWriter->writeChannel($channel);
|
||||
}
|
||||
|
||||
private function createRssWriter(): RssWriter
|
||||
{
|
||||
// Create RSS Writer
|
||||
$rssWriter = new RssWriter(null, [], true);
|
||||
|
||||
$rssWriter->registerWriter(new CoreWriter());
|
||||
$rssWriter->registerWriter(new ItunesWriter());
|
||||
$rssWriter->registerWriter(new SyWriter());
|
||||
$rssWriter->registerWriter(new SlashWriter());
|
||||
$rssWriter->registerWriter(new AtomWriter());
|
||||
|
||||
return $rssWriter;
|
||||
}
|
||||
|
||||
private function buildRssChannelForPodcast(
|
||||
Podcast $podcast,
|
||||
Station $station,
|
||||
ServerRequest $serverRequest
|
||||
): RssChannel {
|
||||
$channel = new RssChannel();
|
||||
|
||||
$channel->setTtl(5);
|
||||
$channel->setLastBuildDate(new DateTime());
|
||||
|
||||
$channel->setTitle($podcast->getTitle());
|
||||
$channel->setDescription($podcast->getDescription());
|
||||
// Fetch podcast API feed.
|
||||
$podcastApi = $this->podcastApiGenerator->__invoke($podcast, $request);
|
||||
|
||||
$channelLink = $podcast->getLink();
|
||||
if (empty($channelLink)) {
|
||||
$channelLink = $serverRequest->getRouter()->fromHere(
|
||||
routeName: 'public:podcast',
|
||||
absolute: true
|
||||
);
|
||||
$channel->setTitle($podcastApi->title);
|
||||
$channel->setDescription($podcastApi->description);
|
||||
$channel->setLink($podcastApi->link ?? $podcastApi->links['self']);
|
||||
$channel->setLanguage($podcastApi->language);
|
||||
|
||||
$channel->setCategories(
|
||||
$podcast->getCategories()->map(
|
||||
function (PodcastCategory $podcastCategory) {
|
||||
$rssCategory = new RssCategory();
|
||||
if (null === $podcastCategory->getSubTitle()) {
|
||||
$rssCategory->setTitle($podcastCategory->getTitle());
|
||||
} else {
|
||||
$rssCategory->setTitle($podcastCategory->getSubTitle());
|
||||
}
|
||||
return $rssCategory;
|
||||
}
|
||||
)->getValues()
|
||||
);
|
||||
|
||||
$rssImage = new RssImage();
|
||||
$rssImage->setTitle($podcastApi->title);
|
||||
$rssImage->setUrl($podcastApi->art);
|
||||
if (null !== $podcastApi->link) {
|
||||
$rssImage->setLink($podcastApi->link);
|
||||
}
|
||||
$channel->setLink($channelLink);
|
||||
|
||||
$channel->setLanguage($podcast->getLanguage());
|
||||
|
||||
$categories = $this->buildRssCategoriesForPodcast($podcast);
|
||||
$channel->setCategories($categories);
|
||||
|
||||
$rssImage = $this->buildRssImageForPodcast($podcast, $station);
|
||||
$channel->setImage($rssImage);
|
||||
|
||||
$rssItems = $this->buildRssItemsForPodcast($podcast, $station);
|
||||
// Iterate through episodes.
|
||||
$rssItems = [];
|
||||
|
||||
$hasPublishedEpisode = false;
|
||||
$hasExplicitEpisode = false;
|
||||
|
||||
/** @var PodcastEpisode $episode */
|
||||
foreach ($podcast->getEpisodes() as $episode) {
|
||||
if (!$episode->isPublished()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasPublishedEpisode = true;
|
||||
if ($episode->getExplicit()) {
|
||||
$hasExplicitEpisode = true;
|
||||
}
|
||||
|
||||
$episodeApi = $this->episodeApiGenerator->__invoke($episode, $request);
|
||||
|
||||
$rssItem = new RssItem();
|
||||
|
||||
$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']);
|
||||
|
||||
$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)
|
||||
);
|
||||
|
||||
$rssItems[] = $rssItem;
|
||||
}
|
||||
|
||||
if (!$hasPublishedEpisode) {
|
||||
throw NotFoundException::podcast();
|
||||
}
|
||||
|
||||
$channel->setItems($rssItems);
|
||||
|
||||
$containsExplicitContent = $this->rssItemsContainsExplicitContent($rssItems);
|
||||
|
||||
$itunesChannel = new ItunesChannel();
|
||||
$itunesChannel->setExplicit($containsExplicitContent);
|
||||
$itunesChannel->setExplicit($hasExplicitEpisode);
|
||||
$itunesChannel->setImage($rssImage->getUrl());
|
||||
$itunesChannel->setCategories($this->buildItunesCategoriesForPodcast($podcast));
|
||||
$itunesChannel->setCategories(
|
||||
$podcast->getCategories()->map(
|
||||
function (PodcastCategory $podcastCategory) {
|
||||
return (null === $podcastCategory->getSubTitle())
|
||||
? $podcastCategory->getTitle()
|
||||
: [
|
||||
$podcastCategory->getTitle(),
|
||||
$podcastCategory->getSubTitle(),
|
||||
];
|
||||
}
|
||||
)->getValues()
|
||||
);
|
||||
|
||||
$itunesChannel->setOwner($this->buildItunesOwner($podcast));
|
||||
$itunesChannel->setAuthor($podcast->getAuthor());
|
||||
|
||||
|
@ -169,46 +175,17 @@ final class PodcastFeedAction implements SingleActionInterface
|
|||
$channel->addExtension(
|
||||
(new AtomLink())
|
||||
->setRel('self')
|
||||
->setHref((string)$serverRequest->getUri())
|
||||
->setHref((string)$request->getUri())
|
||||
->setType('application/rss+xml')
|
||||
);
|
||||
|
||||
return $channel;
|
||||
}
|
||||
$response->getBody()->write(
|
||||
$rssWriter->writeChannel($channel)
|
||||
);
|
||||
|
||||
/**
|
||||
* @return RssCategory[]
|
||||
*/
|
||||
private function buildRssCategoriesForPodcast(Podcast $podcast): array
|
||||
{
|
||||
return $podcast->getCategories()->map(
|
||||
function (PodcastCategory $podcastCategory) {
|
||||
$rssCategory = new RssCategory();
|
||||
if (null === $podcastCategory->getSubTitle()) {
|
||||
$rssCategory->setTitle($podcastCategory->getTitle());
|
||||
} else {
|
||||
$rssCategory->setTitle($podcastCategory->getSubTitle());
|
||||
}
|
||||
return $rssCategory;
|
||||
}
|
||||
)->getValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function buildItunesCategoriesForPodcast(Podcast $podcast): array
|
||||
{
|
||||
return $podcast->getCategories()->map(
|
||||
function (PodcastCategory $podcastCategory) {
|
||||
return (null === $podcastCategory->getSubTitle())
|
||||
? $podcastCategory->getTitle()
|
||||
: [
|
||||
$podcastCategory->getTitle(),
|
||||
$podcastCategory->getSubTitle(),
|
||||
];
|
||||
}
|
||||
)->getValues();
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/rss+xml')
|
||||
->withHeader('X-Robots-Tag', 'index, nofollow');
|
||||
}
|
||||
|
||||
private function buildItunesOwner(Podcast $podcast): ?ItunesOwner
|
||||
|
@ -223,171 +200,4 @@ final class PodcastFeedAction implements SingleActionInterface
|
|||
|
||||
return $itunesOwner;
|
||||
}
|
||||
|
||||
private function buildRssImageForPodcast(Podcast $podcast, Station $station): RssImage
|
||||
{
|
||||
$podcastsFilesystem = $this->stationFilesystems->getPodcastsFilesystem($station);
|
||||
|
||||
$rssImage = new RssImage();
|
||||
|
||||
$podcastArtworkSrc = (string)UriResolver::resolve(
|
||||
$this->router->getBaseUrl(),
|
||||
$this->stationRepository->getDefaultAlbumArtUrl($station)
|
||||
);
|
||||
|
||||
if ($podcastsFilesystem->fileExists(Podcast::getArtPath($podcast->getIdRequired()))) {
|
||||
$routeParams = [
|
||||
'podcast_id' => $podcast->getIdRequired(),
|
||||
];
|
||||
if (0 !== $podcast->getArtUpdatedAt()) {
|
||||
$routeParams['timestamp'] = $podcast->getArtUpdatedAt();
|
||||
}
|
||||
|
||||
$podcastArtworkSrc = $this->router->fromHere(
|
||||
routeName: 'api:stations:public:podcast:art',
|
||||
routeParams: $routeParams,
|
||||
absolute: true
|
||||
);
|
||||
}
|
||||
|
||||
$rssImage->setUrl($podcastArtworkSrc);
|
||||
|
||||
if (null !== $podcast->getLink()) {
|
||||
$rssImage->setLink($podcast->getLink());
|
||||
}
|
||||
|
||||
$rssImage->setTitle($podcast->getTitle());
|
||||
|
||||
return $rssImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RssItem[]
|
||||
*/
|
||||
private function buildRssItemsForPodcast(Podcast $podcast, Station $station): array
|
||||
{
|
||||
$rssItems = [];
|
||||
|
||||
/** @var PodcastEpisode $episode */
|
||||
foreach ($podcast->getEpisodes() as $episode) {
|
||||
if (!$episode->isPublished()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rssItem = new RssItem();
|
||||
|
||||
$rssGuid = new RssGuid();
|
||||
$rssGuid->setGuid($episode->getIdRequired());
|
||||
|
||||
$rssItem->setGuid($rssGuid);
|
||||
$rssItem->setTitle($episode->getTitle());
|
||||
$rssItem->setDescription($episode->getDescription());
|
||||
|
||||
$episodeLink = $episode->getLink();
|
||||
if (empty($episodeLink)) {
|
||||
$episodeLink = $this->router->fromHere(
|
||||
routeName: 'public:podcast:episode',
|
||||
routeParams: ['episode_id' => $episode->getId()],
|
||||
absolute: true
|
||||
);
|
||||
}
|
||||
|
||||
$rssItem->setLink($episodeLink);
|
||||
|
||||
$publishAtDateTime = (new DateTime())->setTimestamp($episode->getCreatedAt());
|
||||
|
||||
if ($episode->getPublishAt() !== null) {
|
||||
$publishAtDateTime = (new DateTime())->setTimestamp($episode->getPublishAt());
|
||||
}
|
||||
|
||||
$rssItem->setPubDate($publishAtDateTime);
|
||||
|
||||
$rssEnclosure = $this->buildRssEnclosureForPodcastMedia(
|
||||
$episode,
|
||||
$station
|
||||
);
|
||||
$rssItem->setEnclosure($rssEnclosure);
|
||||
|
||||
$itunesImage = $this->buildItunesImageForEpisode($episode, $station);
|
||||
$rssItem->addExtension(
|
||||
(new ItunesItem())
|
||||
->setExplicit($episode->getExplicit())
|
||||
->setImage($itunesImage)
|
||||
);
|
||||
|
||||
$rssItems[] = $rssItem;
|
||||
}
|
||||
|
||||
return $rssItems;
|
||||
}
|
||||
|
||||
private function buildRssEnclosureForPodcastMedia(
|
||||
PodcastEpisode $episode,
|
||||
Station $station
|
||||
): RssEnclosure {
|
||||
$rssEnclosure = new RssEnclosure();
|
||||
|
||||
$podcastMediaPlayUrl = $this->router->fromHere(
|
||||
routeName: 'api:stations:public:podcast:episode:download',
|
||||
routeParams: ['episode_id' => $episode->getId()],
|
||||
absolute: true
|
||||
);
|
||||
|
||||
$rssEnclosure->setUrl($podcastMediaPlayUrl);
|
||||
|
||||
$podcastMedia = $episode->getMedia();
|
||||
if (null !== $podcastMedia) {
|
||||
$rssEnclosure->setType($podcastMedia->getMimeType());
|
||||
$rssEnclosure->setLength($podcastMedia->getLength());
|
||||
}
|
||||
|
||||
return $rssEnclosure;
|
||||
}
|
||||
|
||||
private function buildItunesImageForEpisode(PodcastEpisode $episode, Station $station): string
|
||||
{
|
||||
$podcastsFilesystem = $this->stationFilesystems->getPodcastsFilesystem($station);
|
||||
|
||||
$episodeArtworkSrc = (string)UriResolver::resolve(
|
||||
$this->router->getBaseUrl(),
|
||||
$this->stationRepository->getDefaultAlbumArtUrl($station)
|
||||
);
|
||||
|
||||
if ($podcastsFilesystem->fileExists(PodcastEpisode::getArtPath($episode->getIdRequired()))) {
|
||||
$routeParams = [
|
||||
'episode_id' => $episode->getId(),
|
||||
];
|
||||
if (0 !== $episode->getArtUpdatedAt()) {
|
||||
$routeParams['timestamp'] = $episode->getArtUpdatedAt();
|
||||
}
|
||||
|
||||
$episodeArtworkSrc = $this->router->fromHere(
|
||||
routeName: 'api:stations:podcast:episode:art',
|
||||
routeParams: $routeParams,
|
||||
absolute: true
|
||||
);
|
||||
}
|
||||
|
||||
return $episodeArtworkSrc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RssItem[] $rssItems
|
||||
*/
|
||||
private function rssItemsContainsExplicitContent(array $rssItems): bool
|
||||
{
|
||||
foreach ($rssItems as $rssItem) {
|
||||
foreach ($rssItem->getExtensions() as $extension) {
|
||||
if (!($extension instanceof ItunesItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($extension->getExplicit()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ final class PodcastEpisode
|
|||
#[OA\Property]
|
||||
public string $title;
|
||||
|
||||
#[OA\Property]
|
||||
public ?string $link = null;
|
||||
|
||||
#[OA\Property]
|
||||
public string $description;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use App\Entity\PodcastMedia;
|
|||
use App\Entity\StationMedia;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Utilities\Strings;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
|
||||
final class PodcastEpisodeApiGenerator
|
||||
{
|
||||
|
@ -34,6 +35,8 @@ final class PodcastEpisodeApiGenerator
|
|||
$return->id = $record->getIdRequired();
|
||||
$return->title = $record->getTitle();
|
||||
|
||||
$return->link = $record->getLink();
|
||||
|
||||
$return->description = $record->getDescription();
|
||||
$return->description_short = Strings::truncateText($return->description, 100);
|
||||
|
||||
|
@ -41,6 +44,8 @@ final class PodcastEpisodeApiGenerator
|
|||
$return->created_at = $record->getCreatedAt();
|
||||
$return->publish_at = $record->getPublishAt();
|
||||
|
||||
$mediaExtension = '';
|
||||
|
||||
switch ($podcast->getSource()) {
|
||||
case PodcastSources::Playlist:
|
||||
$return->media = null;
|
||||
|
@ -51,6 +56,8 @@ final class PodcastEpisodeApiGenerator
|
|||
|
||||
$return->playlist_media = $this->songApiGen->__invoke($playlistMediaRow);
|
||||
$return->playlist_media_id = $playlistMediaRow->getUniqueId();
|
||||
|
||||
$mediaExtension = Path::getExtension($playlistMediaRow->getPath());
|
||||
} else {
|
||||
$return->has_media = false;
|
||||
|
||||
|
@ -75,6 +82,8 @@ final class PodcastEpisodeApiGenerator
|
|||
|
||||
$return->has_media = true;
|
||||
$return->media = $media;
|
||||
|
||||
$mediaExtension = Path::getExtension($mediaRow->getPath());
|
||||
} else {
|
||||
$return->has_media = false;
|
||||
$return->media = null;
|
||||
|
@ -98,6 +107,11 @@ final class PodcastEpisodeApiGenerator
|
|||
$artRouteParams['timestamp'] = $return->art_updated_at;
|
||||
}
|
||||
|
||||
$downloadRouteParams = $baseRouteParams;
|
||||
if ('' !== $mediaExtension) {
|
||||
$downloadRouteParams['extension'] = $mediaExtension;
|
||||
}
|
||||
|
||||
$return->art = $router->named(
|
||||
routeName: 'api:stations:public:podcast:episode:art',
|
||||
routeParams: $artRouteParams,
|
||||
|
@ -117,7 +131,7 @@ final class PodcastEpisodeApiGenerator
|
|||
),
|
||||
'download' => $router->fromHere(
|
||||
routeName: 'api:stations:public:podcast:episode:download',
|
||||
routeParams: $baseRouteParams,
|
||||
routeParams: $downloadRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
];
|
||||
|
|
Loading…
Reference in New Issue