Newer
Older

Yassine Doghri
committed

Yassine Doghri
committed
declare(strict_types=1);
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use App\Entities\Category;
use App\Entities\Podcast;

Yassine Doghri
committed
use App\Libraries\SimpleRSSElement;
use CodeIgniter\I18n\Time;

Yassine Doghri
committed
if (! function_exists('get_rss_feed')) {

Yassine Doghri
committed
/**
* Generates the rss feed for a given podcast entity
*
* @param string $serviceSlug The name of the service that fetches the RSS feed for future reference when the audio file is eventually downloaded

Yassine Doghri
committed
* @return string rss feed as xml
*/

Yassine Doghri
committed
function get_rss_feed(Podcast $podcast, string $serviceSlug = ''): string

Yassine Doghri
committed
{
$episodes = $podcast->episodes;

Yassine Doghri
committed
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';

Yassine Doghri
committed

Yassine Doghri
committed
$podcastNamespace =

Yassine Doghri
committed
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md';
$atomNamespace = 'http://www.w3.org/2005/Atom';

Yassine Doghri
committed
$rss = new SimpleRSSElement(
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='{$itunesNamespace}' xmlns:podcast='{$podcastNamespace}' xmlns:atom='{$atomNamespace}' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>"

Yassine Doghri
committed
$channel = $rss->addChild('channel');
$atomLink = $channel->addChild('link', null, $atomNamespace);

Yassine Doghri
committed
$atomLink->addAttribute('href', $podcast->feed_url);
$atomLink->addAttribute('rel', 'self');
$atomLink->addAttribute('type', 'application/rss+xml');

Yassine Doghri
committed
// websub: add links to hubs defined in config
$websubHubs = config('WebSub')
->hubs;
foreach ($websubHubs as $websubHub) {
$atomLinkHub = $channel->addChild('link', null, $atomNamespace);
$atomLinkHub->addAttribute('href', $websubHub);
$atomLinkHub->addAttribute('rel', 'hub');
$atomLinkHub->addAttribute('type', 'application/rss+xml');
}
if ($podcast->new_feed_url !== null) {

Yassine Doghri
committed
$channel->addChild('new-feed-url', $podcast->new_feed_url, $itunesNamespace);
}

Yassine Doghri
committed
// the last build date corresponds to the creation of the feed.xml cache

Yassine Doghri
committed
$channel->addChild('lastBuildDate', (new Time('now'))->format(DATE_RFC1123));
$channel->addChild('generator', 'Castopod - https://castopod.org/');

Yassine Doghri
committed
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
$channel->addChild('guid', $podcast->guid, $podcastNamespace);
$channel->addChild('title', $podcast->title, null, false);

Yassine Doghri
committed
$channel->addChildWithCDATA('description', $podcast->description_html);

Yassine Doghri
committed
$itunesImage = $channel->addChild('image', null, $itunesNamespace);

Benjamin Bellamy
committed

Yassine Doghri
committed
$itunesImage->addAttribute('href', $podcast->cover->feed_url);

Yassine Doghri
committed
$channel->addChild('language', $podcast->language_code);
if ($podcast->location !== null) {

Yassine Doghri
committed
$locationElement = $channel->addChild('location', $podcast->location->name, $podcastNamespace);
if ($podcast->location->geo !== null) {
$locationElement->addAttribute('geo', $podcast->location->geo);

Yassine Doghri
committed

Yassine Doghri
committed
if ($podcast->location->osm !== null) {
$locationElement->addAttribute('osm', $podcast->location->osm);

Yassine Doghri
committed
if ($podcast->payment_pointer !== null) {

Yassine Doghri
committed
$valueElement = $channel->addChild('value', null, $podcastNamespace);

Yassine Doghri
committed
$valueElement->addAttribute('type', 'webmonetization');
$valueElement->addAttribute('method', '');
$valueElement->addAttribute('suggested', '');

Yassine Doghri
committed
$recipientElement = $valueElement->addChild('valueRecipient', null, $podcastNamespace);

Yassine Doghri
committed
$recipientElement->addAttribute('name', $podcast->owner_name);
$recipientElement->addAttribute('type', 'ILP');

Yassine Doghri
committed
$recipientElement->addAttribute('address', $podcast->payment_pointer);
$recipientElement->addAttribute('split', '100');

Yassine Doghri
committed
}

Yassine Doghri
committed

Yassine Doghri
committed
$channel
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', $podcastNamespace)

Yassine Doghri
committed
->addAttribute('owner', $podcast->owner_email);
if ($podcast->imported_feed_url !== null) {

Yassine Doghri
committed
$channel->addChild('previousUrl', $podcast->imported_feed_url, $podcastNamespace);
foreach ($podcast->podcasting_platforms as $podcastingPlatform) {

Yassine Doghri
committed
$podcastingPlatformElement = $channel->addChild('id', null, $podcastNamespace);
$podcastingPlatformElement->addAttribute('platform', $podcastingPlatform->slug);

Yassine Doghri
committed
if ($podcastingPlatform->account_id !== null) {
$podcastingPlatformElement->addAttribute('id', $podcastingPlatform->account_id);

Yassine Doghri
committed
}

Yassine Doghri
committed
if ($podcastingPlatform->link_url !== null) {

Yassine Doghri
committed
$podcastingPlatformElement->addAttribute('url', $podcastingPlatform->link_url);

Yassine Doghri
committed
}
}
$castopodSocialElement = $channel->addChild('social', null, $podcastNamespace);
$castopodSocialElement->addAttribute('priority', '1');
$castopodSocialElement->addAttribute('platform', 'castopod');
$castopodSocialElement->addAttribute('protocol', 'activitypub');
$castopodSocialElement->addAttribute('accountId', "@{$podcast->actor->username}@{$podcast->actor->domain}");
$castopodSocialElement->addAttribute('accountUrl', $podcast->link);
foreach ($podcast->social_platforms as $socialPlatform) {
$socialElement = $channel->addChild('social', null, $podcastNamespace,);
$socialElement->addAttribute('priority', '2');
$socialElement->addAttribute('platform', $socialPlatform->slug);
// TODO: get activitypub info somewhere else
if (in_array(
$socialPlatform->slug,
['mastodon', 'peertube', 'funkwhale', 'misskey', 'mobilizon', 'pixelfed', 'plume', 'writefreely'],
true
)) {
$socialElement->addAttribute('protocol', 'activitypub');
} else {
$socialElement->addAttribute('protocol', $socialPlatform->slug);
}
if ($socialPlatform->account_id !== null) {
$socialElement->addAttribute('accountId', esc($socialPlatform->account_id));
}

Yassine Doghri
committed
if ($socialPlatform->link_url !== null) {
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
$socialElement->addAttribute('accountUrl', esc($socialPlatform->link_url));
}
if ($socialPlatform->slug === 'mastodon') {
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, $podcastNamespace);
$socialSignUpelement->addAttribute('priority', '1');
$socialSignUpelement->addAttribute(
'homeUrl',
parse_url($socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
$socialPlatform->link_url,
PHP_URL_HOST
) . '/public'
);
$socialSignUpelement->addAttribute(
'signUpUrl',
parse_url($socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
$socialPlatform->link_url,
PHP_URL_HOST
) . '/auth/sign_up'
);
$castopodSocialSignUpelement = $castopodSocialElement->addChild(
'socialSignUp',
null,
$podcastNamespace
);
$castopodSocialSignUpelement->addAttribute('priority', '1');
$castopodSocialSignUpelement->addAttribute(
'homeUrl',
parse_url($socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
$socialPlatform->link_url,
PHP_URL_HOST
) . '/public'
);
$castopodSocialSignUpelement->addAttribute(
'signUpUrl',
parse_url($socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
$socialPlatform->link_url,
PHP_URL_HOST
) . '/auth/sign_up'
);

Yassine Doghri
committed
}
foreach ($podcast->funding_platforms as $fundingPlatform) {

Yassine Doghri
committed
$fundingPlatformElement = $channel->addChild(
'funding',

Yassine Doghri
committed
$fundingPlatform->account_id,

Yassine Doghri
committed
$podcastNamespace,

Yassine Doghri
committed
$fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug);
if ($fundingPlatform->link_url !== null) {

Yassine Doghri
committed
$fundingPlatformElement->addAttribute('url', $fundingPlatform->link_url);

Yassine Doghri
committed
}
foreach ($podcast->persons as $person) {
foreach ($person->roles as $role) {

Yassine Doghri
committed
$personElement = $channel->addChild('person', $person->full_name, $podcastNamespace,);

Yassine Doghri
committed
$personElement->addAttribute('img', $person->avatar->medium_url);
if ($person->information_url !== null) {

Yassine Doghri
committed
$personElement->addAttribute('href', $person->information_url);
}
$personElement->addAttribute(

Yassine Doghri
committed
lang("PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label", [], 'en'),
$personElement->addAttribute(

Yassine Doghri
committed
lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en'),
);
}
}

Yassine Doghri
committed
// set main category first, then other categories as apple
add_category_tag($channel, $podcast->category);
foreach ($podcast->other_categories as $other_category) {
add_category_tag($channel, $other_category);
}
$channel->addChild(
'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',

Yassine Doghri
committed
$itunesNamespace,

Yassine Doghri
committed
);
$channel->addChild(
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,

Yassine Doghri
committed
$itunesNamespace,

Yassine Doghri
committed
);
$channel->addChild('link', $podcast->link);

Yassine Doghri
committed
$owner = $channel->addChild('owner', null, $itunesNamespace);

Yassine Doghri
committed

Yassine Doghri
committed
$owner->addChild('name', $podcast->owner_name, $itunesNamespace);

Yassine Doghri
committed

Yassine Doghri
committed
$owner->addChild('email', $podcast->owner_email, $itunesNamespace);

Yassine Doghri
committed

Yassine Doghri
committed
$channel->addChild('type', $podcast->type, $itunesNamespace);

Yassine Doghri
committed
$podcast->copyright &&
$channel->addChild('copyright', $podcast->copyright);

Yassine Doghri
committed
$channel->addChild('block', 'Yes', $itunesNamespace);

Yassine Doghri
committed

Yassine Doghri
committed
$channel->addChild('complete', 'Yes', $itunesNamespace);

Yassine Doghri
committed
$image = $channel->addChild('image');

Yassine Doghri
committed
$image->addChild('url', $podcast->cover->feed_url);
$image->addChild('title', $podcast->title, null, false);

Yassine Doghri
committed
$image->addChild('link', $podcast->link);

Benjamin Bellamy
committed
if ($podcast->custom_rss !== null) {
array_to_rss([
'elements' => $podcast->custom_rss,

Yassine Doghri
committed
], $channel);

Benjamin Bellamy
committed
}

Yassine Doghri
committed
foreach ($episodes as $episode) {
$item = $channel->addChild('item');
$item->addChild('title', $episode->title, null, false);

Yassine Doghri
committed
$enclosure = $item->addChild('enclosure');
$enclosure->addAttribute(
'url',
$episode->audio_analytics_url .
($serviceSlug === ''

Yassine Doghri
committed
? ''
: '?_from=' . urlencode($serviceSlug)),
);
$enclosure->addAttribute('length', (string) $episode->audio->file_size);
$enclosure->addAttribute('type', $episode->audio->file_mimetype);

Yassine Doghri
committed
$item->addChild('guid', $episode->guid);

Yassine Doghri
committed
$item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123));
if ($episode->location !== null) {

Yassine Doghri
committed
$locationElement = $item->addChild('location', $episode->location->name, $podcastNamespace,);
if ($episode->location->geo !== null) {

Yassine Doghri
committed
$locationElement->addAttribute('geo', $episode->location->geo);

Yassine Doghri
committed
}

Yassine Doghri
committed

Yassine Doghri
committed
if ($episode->location->osm !== null) {

Yassine Doghri
committed
$locationElement->addAttribute('osm', $episode->location->osm);

Yassine Doghri
committed
}
}

Yassine Doghri
committed

Yassine Doghri
committed
$item->addChildWithCDATA('description', $episode->getDescriptionHtml($serviceSlug));
$item->addChild('duration', (string) $episode->audio->duration, $itunesNamespace);

Yassine Doghri
committed
$item->addChild('link', $episode->link);

Yassine Doghri
committed
$episodeItunesImage = $item->addChild('image', null, $itunesNamespace);

Yassine Doghri
committed
$episodeItunesImage->addAttribute('href', $episode->cover->feed_url);

Yassine Doghri
committed
$episode->parental_advisory &&
$item->addChild(
'explicit',
$episode->parental_advisory === 'explicit'
? 'true'
: 'false',

Yassine Doghri
committed
$itunesNamespace,

Yassine Doghri
committed
);
$episode->number &&
$item->addChild('episode', (string) $episode->number, $itunesNamespace);

Yassine Doghri
committed
$episode->season_number &&
$item->addChild('season', (string) $episode->season_number, $itunesNamespace);

Yassine Doghri
committed
$item->addChild('episodeType', $episode->type, $itunesNamespace);

Yassine Doghri
committed
// add link to episode comments as podcast-activity format
$comments = $item->addChild('comments', null, $podcastNamespace);
$comments->addAttribute('uri', url_to('episode-comments', $podcast->handle, $episode->slug));
$comments->addAttribute('contentType', 'application/podcast-activity+json');
if ($episode->getPosts()) {

Yassine Doghri
committed
$socialInteractUri = $episode->getPosts()[0]
->uri;

Yassine Doghri
committed
$socialInteractElement = $item->addChild('socialInteract', null, $podcastNamespace);
$socialInteractElement->addAttribute('uri', $socialInteractUri);
$socialInteractElement->addAttribute('priority', '1');
$socialInteractElement->addAttribute('platform', 'castopod');
$socialInteractElement->addAttribute('protocol', 'activitypub');
$socialInteractElement->addAttribute(
'accountId',
"@{$podcast->actor->username}@{$podcast->actor->domain}"
);
$socialInteractElement->addAttribute(
'pubDate',
$episode->getPosts()[0]
->published_at->format(DateTime::ISO8601)
);
}

Yassine Doghri
committed
if ($episode->transcript !== null) {

Yassine Doghri
committed
$transcriptElement = $item->addChild('transcript', null, $podcastNamespace);
$transcriptElement->addAttribute('url', $episode->transcript->file_url);

Yassine Doghri
committed
$transcriptElement->addAttribute(
'type',

Yassine Doghri
committed
Mimes::guessTypeFromExtension(
pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION)

Yassine Doghri
committed
) ?? 'text/html',

Yassine Doghri
committed
);

Yassine Doghri
committed
$transcriptElement->addAttribute('language', $podcast->language_code);

Yassine Doghri
committed
}

Yassine Doghri
committed
if ($episode->getChapters() !== null) {

Yassine Doghri
committed
$chaptersElement = $item->addChild('chapters', null, $podcastNamespace);
$chaptersElement->addAttribute('url', $episode->chapters->file_url);

Yassine Doghri
committed
$chaptersElement->addAttribute('type', 'application/json+chapters');

Yassine Doghri
committed
}

Yassine Doghri
committed
foreach ($episode->soundbites as $soundbite) {
// TODO: differentiate video from soundbites?

Yassine Doghri
committed
$soundbiteElement = $item->addChild('soundbite', $soundbite->title, $podcastNamespace);

Yassine Doghri
committed
$soundbiteElement->addAttribute('start_time', (string) $soundbite->start_time);
$soundbiteElement->addAttribute('duration', (string) $soundbite->duration);

Yassine Doghri
committed
}
foreach ($episode->persons as $person) {
foreach ($person->roles as $role) {
$personElement = $item->addChild('person', esc($person->full_name), $podcastNamespace,);
$personElement->addAttribute(

Yassine Doghri
committed
'role',
esc(lang("PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label", [], 'en'),),

Yassine Doghri
committed
);
$personElement->addAttribute(

Yassine Doghri
committed
'group',
esc(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),

Yassine Doghri
committed
);

Yassine Doghri
committed
$personElement->addAttribute('img', $person->avatar->medium_url);
if ($person->information_url !== null) {

Yassine Doghri
committed
$personElement->addAttribute('href', $person->information_url);
}

Yassine Doghri
committed
}
}

Yassine Doghri
committed
$item->addChild('block', 'Yes', $itunesNamespace);

Yassine Doghri
committed
if ($episode->custom_rss !== null) {
array_to_rss([
'elements' => $episode->custom_rss,

Yassine Doghri
committed
], $item);

Yassine Doghri
committed
}
}
return $rss->asXML();
}
if (! function_exists('add_category_tag')) {

Yassine Doghri
committed
/**
* Adds <itunes:category> and <category> tags to node for a given category
*/
function add_category_tag(SimpleXMLElement $node, Category $category): void
{

Yassine Doghri
committed
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';

Yassine Doghri
committed
$itunesCategory = $node->addChild('category', null, $itunesNamespace);

Yassine Doghri
committed
$itunesCategory->addAttribute(

Yassine Doghri
committed
'text',
$category->parent !== null
? $category->parent->apple_category
: $category->apple_category,

Yassine Doghri
committed
if ($category->parent !== null) {
$itunesCategoryChild = $itunesCategory->addChild('category', null, $itunesNamespace);

Yassine Doghri
committed
$itunesCategoryChild->addAttribute('text', $category->apple_category);

Yassine Doghri
committed
$node->addChild('category', $category->parent->apple_category);
}

Yassine Doghri
committed

Yassine Doghri
committed
$node->addChild('category', $category->apple_category);
}
}

Benjamin Bellamy
committed
if (! function_exists('rss_to_array')) {

Yassine Doghri
committed
/**
* Converts XML to array
*
* FIXME: param should be SimpleRSSElement
*
* @return array<string, mixed>

Yassine Doghri
committed
*/
function rss_to_array(SimpleXMLElement $rssNode): array

Yassine Doghri
committed
{
$nameSpaces = [
'',
'http://www.itunes.com/dtds/podcast-1.0.dtd',
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
];
$arrayNode = [];
$arrayNode['name'] = $rssNode->getName();
$arrayNode['namespace'] = $rssNode->getNamespaces(false);
foreach ($rssNode->attributes() as $key => $value) {

Benjamin Bellamy
committed
$arrayNode['attributes'][$key] = (string) $value;
}

Yassine Doghri
committed
$textcontent = trim((string) $rssNode);

Yassine Doghri
committed
if (strlen($textcontent) > 0) {
$arrayNode['content'] = $textcontent;

Benjamin Bellamy
committed
}

Yassine Doghri
committed

Yassine Doghri
committed
foreach ($nameSpaces as $currentNameSpace) {
foreach ($rssNode->children($currentNameSpace) as $childXmlNode) {

Yassine Doghri
committed
$arrayNode['elements'][] = rss_to_array($childXmlNode);
}
}
return $arrayNode;

Benjamin Bellamy
committed
}
}
if (! function_exists('array_to_rss')) {

Yassine Doghri
committed
/**
* Inserts array (converted to XML node) in XML node
*
* @param array<string, mixed> $arrayNode

Yassine Doghri
committed
* @param SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
*/
function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode): SimpleRSSElement
{

Yassine Doghri
committed
if (array_key_exists('elements', $arrayNode)) {
foreach ($arrayNode['elements'] as $childArrayNode) {
$childXmlNode = $xmlNode->addChild(
$childArrayNode['name'],
$childArrayNode['content'] ?? null,
$childArrayNode['namespace'] === []
? null
: current($childArrayNode['namespace'])

Yassine Doghri
committed
);
if (array_key_exists('attributes', $childArrayNode)) {
foreach (
$childArrayNode['attributes']
as $attributeKey => $attributeValue
) {

Yassine Doghri
committed
$childXmlNode->addAttribute($attributeKey, $attributeValue);

Yassine Doghri
committed
}

Benjamin Bellamy
committed
}

Yassine Doghri
committed

Yassine Doghri
committed
array_to_rss($childArrayNode, $childXmlNode);

Benjamin Bellamy
committed
}
}

Yassine Doghri
committed
return $xmlNode;