mirror of
https://code.castopod.org/adaures/castopod.git
synced 2024-08-21 16:43:23 +02:00
- move and refactor Image.php from Libraries to Entities folder - update some database field names / types - update composer packages
580 lines
20 KiB
PHP
580 lines
20 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @copyright 2020 Podlibre
|
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
|
* @link https://castopod.org/
|
|
*/
|
|
|
|
use App\Libraries\SimpleRSSElement;
|
|
use CodeIgniter\I18n\Time;
|
|
use Config\Mimes;
|
|
use App\Entities\Podcast;
|
|
use App\Entities\Category;
|
|
|
|
if (!function_exists('get_rss_feed')) {
|
|
/**
|
|
* 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
|
|
* @return string rss feed as xml
|
|
*/
|
|
function get_rss_feed(Podcast $podcast, ?string $serviceSlug = null): string
|
|
{
|
|
$episodes = $podcast->episodes;
|
|
|
|
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
|
|
|
$podcast_namespace =
|
|
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md';
|
|
|
|
$rss = new SimpleRSSElement(
|
|
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='{$itunes_namespace}' xmlns:podcast='{$podcast_namespace}' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>",
|
|
);
|
|
|
|
$channel = $rss->addChild('channel');
|
|
|
|
$atom_link = $channel->addChild(
|
|
'atom:link',
|
|
null,
|
|
'http://www.w3.org/2005/Atom',
|
|
);
|
|
$atom_link->addAttribute('href', $podcast->feed_url);
|
|
$atom_link->addAttribute('rel', 'self');
|
|
$atom_link->addAttribute('type', 'application/rss+xml');
|
|
|
|
if ($podcast->new_feed_url !== null) {
|
|
$channel->addChild(
|
|
'new-feed-url',
|
|
$podcast->new_feed_url,
|
|
$itunes_namespace,
|
|
);
|
|
}
|
|
|
|
// the last build date corresponds to the creation of the feed.xml cache
|
|
$channel->addChild(
|
|
'lastBuildDate',
|
|
(new Time('now'))->format(DATE_RFC1123),
|
|
);
|
|
$channel->addChild(
|
|
'generator',
|
|
'Castopod Host - https://castopod.org/',
|
|
);
|
|
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
|
|
|
|
$channel->addChild('title', $podcast->title);
|
|
$channel->addChildWithCDATA('description', $podcast->description_html);
|
|
|
|
$itunes_image = $channel->addChild('image', null, $itunes_namespace);
|
|
|
|
// FIXME: This should be downsized to 1400x1400
|
|
$itunes_image->addAttribute('href', $podcast->image->url);
|
|
|
|
$channel->addChild('language', $podcast->language_code);
|
|
if ($podcast->location !== null) {
|
|
$locationElement = $channel->addChild(
|
|
'location',
|
|
htmlspecialchars($podcast->location->name),
|
|
$podcast_namespace,
|
|
);
|
|
if ($podcast->location->geo !== null) {
|
|
$locationElement->addAttribute('geo', $podcast->location->geo);
|
|
}
|
|
if ($podcast->location->osm_id !== null) {
|
|
$locationElement->addAttribute(
|
|
'osm',
|
|
$podcast->location->osm_id,
|
|
);
|
|
}
|
|
}
|
|
if ($podcast->payment_pointer !== null) {
|
|
$valueElement = $channel->addChild(
|
|
'value',
|
|
null,
|
|
$podcast_namespace,
|
|
);
|
|
$valueElement->addAttribute('type', 'webmonetization');
|
|
$valueElement->addAttribute('method', '');
|
|
$valueElement->addAttribute('suggested', '');
|
|
$recipientElement = $valueElement->addChild(
|
|
'valueRecipient',
|
|
null,
|
|
$podcast_namespace,
|
|
);
|
|
$recipientElement->addAttribute('name', $podcast->owner_name);
|
|
$recipientElement->addAttribute('type', 'ILP');
|
|
$recipientElement->addAttribute(
|
|
'address',
|
|
$podcast->payment_pointer,
|
|
);
|
|
$recipientElement->addAttribute('split', '100');
|
|
}
|
|
$channel
|
|
->addChild(
|
|
'locked',
|
|
$podcast->is_locked ? 'yes' : 'no',
|
|
$podcast_namespace,
|
|
)
|
|
->addAttribute('owner', $podcast->owner_email);
|
|
if ($podcast->imported_feed_url !== null) {
|
|
$channel->addChild(
|
|
'previousUrl',
|
|
$podcast->imported_feed_url,
|
|
$podcast_namespace,
|
|
);
|
|
}
|
|
|
|
foreach ($podcast->podcasting_platforms as $podcastingPlatform) {
|
|
$podcastingPlatformElement = $channel->addChild(
|
|
'id',
|
|
null,
|
|
$podcast_namespace,
|
|
);
|
|
$podcastingPlatformElement->addAttribute(
|
|
'platform',
|
|
$podcastingPlatform->slug,
|
|
);
|
|
if ($podcastingPlatform->link_content !== null) {
|
|
$podcastingPlatformElement->addAttribute(
|
|
'id',
|
|
$podcastingPlatform->link_content,
|
|
);
|
|
}
|
|
if ($podcastingPlatform->link_url !== null) {
|
|
$podcastingPlatformElement->addAttribute(
|
|
'url',
|
|
htmlspecialchars($podcastingPlatform->link_url),
|
|
);
|
|
}
|
|
}
|
|
|
|
foreach ($podcast->social_platforms as $socialPlatform) {
|
|
$socialPlatformElement = $channel->addChild(
|
|
'social',
|
|
$socialPlatform->link_content,
|
|
$podcast_namespace,
|
|
);
|
|
$socialPlatformElement->addAttribute(
|
|
'platform',
|
|
$socialPlatform->slug,
|
|
);
|
|
if ($socialPlatform->link_url !== null) {
|
|
$socialPlatformElement->addAttribute(
|
|
'url',
|
|
htmlspecialchars($socialPlatform->link_url),
|
|
);
|
|
}
|
|
}
|
|
|
|
foreach ($podcast->funding_platforms as $fundingPlatform) {
|
|
$fundingPlatformElement = $channel->addChild(
|
|
'funding',
|
|
$fundingPlatform->link_content,
|
|
$podcast_namespace,
|
|
);
|
|
$fundingPlatformElement->addAttribute(
|
|
'platform',
|
|
$fundingPlatform->slug,
|
|
);
|
|
if ($fundingPlatform->link_url !== null) {
|
|
$fundingPlatformElement->addAttribute(
|
|
'url',
|
|
htmlspecialchars($fundingPlatform->link_url),
|
|
);
|
|
}
|
|
}
|
|
|
|
foreach ($podcast->persons as $podcastPerson) {
|
|
$podcastPersonElement = $channel->addChild(
|
|
'person',
|
|
htmlspecialchars($podcastPerson->person->full_name),
|
|
$podcast_namespace,
|
|
);
|
|
|
|
if (
|
|
$podcastPerson->person_role !== null &&
|
|
$podcastPerson->person_group !== null
|
|
) {
|
|
$podcastPersonElement->addAttribute(
|
|
'role',
|
|
htmlspecialchars(
|
|
lang(
|
|
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
|
|
[],
|
|
'en',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if ($podcastPerson->person_group !== null) {
|
|
$podcastPersonElement->addAttribute(
|
|
'group',
|
|
htmlspecialchars(
|
|
lang(
|
|
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
|
|
[],
|
|
'en',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
$podcastPersonElement->addAttribute(
|
|
'img',
|
|
$podcastPerson->person->image->large_url,
|
|
);
|
|
|
|
if ($podcastPerson->person->information_url !== null) {
|
|
$podcastPersonElement->addAttribute(
|
|
'href',
|
|
$podcastPerson->person->information_url,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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',
|
|
$itunes_namespace,
|
|
);
|
|
|
|
$channel->addChild(
|
|
'author',
|
|
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
|
|
$itunes_namespace,
|
|
);
|
|
$channel->addChild('link', $podcast->link);
|
|
|
|
$owner = $channel->addChild('owner', null, $itunes_namespace);
|
|
|
|
$owner->addChild('name', $podcast->owner_name, $itunes_namespace);
|
|
|
|
$owner->addChild('email', $podcast->owner_email, $itunes_namespace);
|
|
|
|
$channel->addChild('type', $podcast->type, $itunes_namespace);
|
|
$podcast->copyright &&
|
|
$channel->addChild('copyright', $podcast->copyright);
|
|
$podcast->is_blocked &&
|
|
$channel->addChild('block', 'Yes', $itunes_namespace);
|
|
$podcast->is_completed &&
|
|
$channel->addChild('complete', 'Yes', $itunes_namespace);
|
|
|
|
$image = $channel->addChild('image');
|
|
$image->addChild('url', $podcast->image->feed_url);
|
|
$image->addChild('title', $podcast->title);
|
|
$image->addChild('link', $podcast->link);
|
|
|
|
if ($podcast->custom_rss !== null) {
|
|
array_to_rss(
|
|
[
|
|
'elements' => $podcast->custom_rss,
|
|
],
|
|
$channel,
|
|
);
|
|
}
|
|
|
|
foreach ($episodes as $episode) {
|
|
$item = $channel->addChild('item');
|
|
$item->addChild('title', $episode->title);
|
|
$enclosure = $item->addChild('enclosure');
|
|
|
|
$enclosure->addAttribute(
|
|
'url',
|
|
$episode->audio_file_analytics_url .
|
|
($serviceSlug === ''
|
|
? ''
|
|
: '?_from=' . urlencode($serviceSlug)),
|
|
);
|
|
$enclosure->addAttribute('length', $episode->audio_file_size);
|
|
$enclosure->addAttribute('type', $episode->audio_file_mimetype);
|
|
|
|
$item->addChild('guid', $episode->guid);
|
|
$item->addChild(
|
|
'pubDate',
|
|
$episode->published_at->format(DATE_RFC1123),
|
|
);
|
|
if ($episode->location !== null) {
|
|
$locationElement = $item->addChild(
|
|
'location',
|
|
htmlspecialchars($episode->location->name),
|
|
$podcast_namespace,
|
|
);
|
|
if ($episode->location->geo !== null) {
|
|
$locationElement->addAttribute(
|
|
'geo',
|
|
$episode->location->geo,
|
|
);
|
|
}
|
|
if ($episode->location->osm_id !== null) {
|
|
$locationElement->addAttribute(
|
|
'osm',
|
|
$episode->location->osm_id,
|
|
);
|
|
}
|
|
}
|
|
$item->addChildWithCDATA(
|
|
'description',
|
|
$episode->getDescriptionHtml($serviceSlug),
|
|
);
|
|
$item->addChild(
|
|
'duration',
|
|
$episode->audio_file_duration,
|
|
$itunes_namespace,
|
|
);
|
|
$item->addChild('link', $episode->link);
|
|
$episode_itunes_image = $item->addChild(
|
|
'image',
|
|
null,
|
|
$itunes_namespace,
|
|
);
|
|
$episode_itunes_image->addAttribute(
|
|
'href',
|
|
$episode->image->feed_url,
|
|
);
|
|
|
|
$episode->parental_advisory &&
|
|
$item->addChild(
|
|
'explicit',
|
|
$episode->parental_advisory === 'explicit'
|
|
? 'true'
|
|
: 'false',
|
|
$itunes_namespace,
|
|
);
|
|
|
|
$episode->number &&
|
|
$item->addChild('episode', $episode->number, $itunes_namespace);
|
|
$episode->season_number &&
|
|
$item->addChild(
|
|
'season',
|
|
$episode->season_number,
|
|
$itunes_namespace,
|
|
);
|
|
$item->addChild('episodeType', $episode->type, $itunes_namespace);
|
|
|
|
if ($episode->transcript_file_url) {
|
|
$transcriptElement = $item->addChild(
|
|
'transcript',
|
|
null,
|
|
$podcast_namespace,
|
|
);
|
|
$transcriptElement->addAttribute(
|
|
'url',
|
|
$episode->transcript_file_url,
|
|
);
|
|
$transcriptElement->addAttribute(
|
|
'type',
|
|
Mimes::guessTypeFromExtension(
|
|
pathinfo(
|
|
$episode->transcript_file_url,
|
|
PATHINFO_EXTENSION,
|
|
),
|
|
),
|
|
);
|
|
$transcriptElement->addAttribute(
|
|
'language',
|
|
$podcast->language_code,
|
|
);
|
|
}
|
|
|
|
if ($episode->chapters_file_url) {
|
|
$chaptersElement = $item->addChild(
|
|
'chapters',
|
|
null,
|
|
$podcast_namespace,
|
|
);
|
|
$chaptersElement->addAttribute(
|
|
'url',
|
|
$episode->chapters_file_url,
|
|
);
|
|
$chaptersElement->addAttribute(
|
|
'type',
|
|
'application/json+chapters',
|
|
);
|
|
}
|
|
|
|
foreach ($episode->soundbites as $soundbite) {
|
|
$soundbiteElement = $item->addChild(
|
|
'soundbite',
|
|
empty($soundbite->label) ? null : $soundbite->label,
|
|
$podcast_namespace,
|
|
);
|
|
$soundbiteElement->addAttribute(
|
|
'start_time',
|
|
$soundbite->start_time,
|
|
);
|
|
$soundbiteElement->addAttribute(
|
|
'duration',
|
|
$soundbite->duration,
|
|
);
|
|
}
|
|
|
|
foreach ($episode->persons as $episodePerson) {
|
|
$episodePersonElement = $item->addChild(
|
|
'person',
|
|
htmlspecialchars($episodePerson->person->full_name),
|
|
$podcast_namespace,
|
|
);
|
|
if (
|
|
!empty($episodePerson->person_role) &&
|
|
!empty($episodePerson->person_group)
|
|
) {
|
|
$episodePersonElement->addAttribute(
|
|
'role',
|
|
htmlspecialchars(
|
|
lang(
|
|
"PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label",
|
|
[],
|
|
'en',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
if (!empty($episodePerson->person_group)) {
|
|
$episodePersonElement->addAttribute(
|
|
'group',
|
|
htmlspecialchars(
|
|
lang(
|
|
"PersonsTaxonomy.persons.{$episodePerson->person_group}.label",
|
|
[],
|
|
'en',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
$episodePersonElement->addAttribute(
|
|
'img',
|
|
$episodePerson->person->image->large_url,
|
|
);
|
|
if (!empty($episodePerson->person->information_url)) {
|
|
$episodePersonElement->addAttribute(
|
|
'href',
|
|
$episodePerson->person->information_url,
|
|
);
|
|
}
|
|
}
|
|
|
|
$episode->is_blocked &&
|
|
$item->addChild('block', 'Yes', $itunes_namespace);
|
|
|
|
if (!empty($episode->custom_rss)) {
|
|
array_to_rss(
|
|
[
|
|
'elements' => $episode->custom_rss,
|
|
],
|
|
$item,
|
|
);
|
|
}
|
|
}
|
|
|
|
return $rss->asXML();
|
|
}
|
|
}
|
|
|
|
if (!function_exists('add_category_tag')) {
|
|
/**
|
|
* Adds <itunes:category> and <category> tags to node for a given category
|
|
*/
|
|
function add_category_tag(SimpleXMLElement $node, Category $category): void
|
|
{
|
|
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
|
|
|
$itunes_category = $node->addChild('category', '', $itunes_namespace);
|
|
$itunes_category->addAttribute(
|
|
'text',
|
|
$category->parent !== null
|
|
? $category->parent->apple_category
|
|
: $category->apple_category,
|
|
);
|
|
|
|
if ($category->parent !== null) {
|
|
$itunes_category_child = $itunes_category->addChild(
|
|
'category',
|
|
'',
|
|
$itunes_namespace,
|
|
);
|
|
$itunes_category_child->addAttribute(
|
|
'text',
|
|
$category->apple_category,
|
|
);
|
|
$node->addChild('category', $category->parent->apple_category);
|
|
}
|
|
$node->addChild('category', $category->apple_category);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('rss_to_array')) {
|
|
/**
|
|
* Converts XML to array
|
|
*
|
|
* FIXME: should be SimpleRSSElement
|
|
* @param SimpleXMLElement $xmlNode
|
|
*/
|
|
function rss_to_array(SimpleXMLElement $xmlNode): array
|
|
{
|
|
$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'] = $xmlNode->getName();
|
|
$arrayNode['namespace'] = $xmlNode->getNamespaces(false);
|
|
foreach ($xmlNode->attributes() as $key => $value) {
|
|
$arrayNode['attributes'][$key] = (string) $value;
|
|
}
|
|
$textcontent = trim((string) $xmlNode);
|
|
if (strlen($textcontent) > 0) {
|
|
$arrayNode['content'] = $textcontent;
|
|
}
|
|
foreach ($nameSpaces as $currentNameSpace) {
|
|
foreach ($xmlNode->children($currentNameSpace) as $childXmlNode) {
|
|
$arrayNode['elements'][] = rss_to_array($childXmlNode);
|
|
}
|
|
}
|
|
|
|
return $arrayNode;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('array_to_rss')) {
|
|
/**
|
|
* Inserts array (converted to XML node) in XML node
|
|
*
|
|
* @param SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
|
|
*/
|
|
function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode)
|
|
{
|
|
if (array_key_exists('elements', $arrayNode)) {
|
|
foreach ($arrayNode['elements'] as $childArrayNode) {
|
|
$childXmlNode = $xmlNode->addChild(
|
|
$childArrayNode['name'],
|
|
$childArrayNode['content'] ?? null,
|
|
empty($childArrayNode['namespace'])
|
|
? null
|
|
: current($childArrayNode['namespace']),
|
|
);
|
|
if (array_key_exists('attributes', $childArrayNode)) {
|
|
foreach (
|
|
$childArrayNode['attributes']
|
|
as $attributeKey => $attributeValue
|
|
) {
|
|
$childXmlNode->addAttribute(
|
|
$attributeKey,
|
|
$attributeValue,
|
|
);
|
|
}
|
|
}
|
|
array_to_rss($childArrayNode, $childXmlNode);
|
|
}
|
|
}
|
|
|
|
return $xmlNode;
|
|
}
|
|
}
|