Skip to content
Snippets Groups Projects
rss_helper.php 19.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • /**
     * @copyright  2020 Podlibre
     * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
     * @link       https://castopod.org/
     */
    
    
    use CodeIgniter\I18n\Time;
    
    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 $service 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, $serviceSlug = ''): 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>",
    
            $atom_link->addAttribute('href', $podcast->feed_url);
            $atom_link->addAttribute('rel', 'self');
            $atom_link->addAttribute('type', 'application/rss+xml');
    
            if (!empty($podcast->new_feed_url)) {
                $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);
    
            $itunes_image->addAttribute('href', $podcast->image->original_url);
    
            $channel->addChild('language', $podcast->language_code);
            if (!empty($podcast->location_name)) {
                $locationElement = $channel->addChild(
    
                if (!empty($podcast->location_geo)) {
                    $locationElement->addAttribute('geo', $podcast->location_geo);
    
                if (!empty($podcast->location_osmid)) {
                    $locationElement->addAttribute('osm', $podcast->location_osmid);
    
            if (!empty($podcast->payment_pointer)) {
                $valueElement = $channel->addChild(
                    'value',
    
                $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 (!empty($podcast->imported_feed_url)) {
                $channel->addChild(
                    'previousUrl',
                    $podcast->imported_feed_url,
                    $podcast_namespace,
    
            foreach ($podcast->podcastingPlatforms as $podcastingPlatform) {
                $podcastingPlatformElement = $channel->addChild(
                    'id',
    
                $podcastingPlatformElement->addAttribute(
                    'platform',
                    $podcastingPlatform->slug,
                );
                if (!empty($podcastingPlatform->link_content)) {
                    $podcastingPlatformElement->addAttribute(
                        'id',
                        $podcastingPlatform->link_content,
                    );
                }
                if (!empty($podcastingPlatform->link_url)) {
                    $podcastingPlatformElement->addAttribute(
                        'url',
                        htmlspecialchars($podcastingPlatform->link_url),
                    );
                }
            }
    
            foreach ($podcast->socialPlatforms as $socialPlatform) {
                $socialPlatformElement = $channel->addChild(
                    'social',
                    $socialPlatform->link_content,
                    $podcast_namespace,
                );
                $socialPlatformElement->addAttribute(
                    'platform',
                    $socialPlatform->slug,
                );
                if (!empty($socialPlatform->link_url)) {
                    $socialPlatformElement->addAttribute(
                        'url',
                        htmlspecialchars($socialPlatform->link_url),
                    );
                }
    
            foreach ($podcast->fundingPlatforms as $fundingPlatform) {
                $fundingPlatformElement = $channel->addChild(
                    'funding',
                    $fundingPlatform->link_content,
    
                $fundingPlatformElement->addAttribute(
                    'platform',
                    $fundingPlatform->slug,
    
                if (!empty($socialPlatform->link_url)) {
                    $fundingPlatformElement->addAttribute(
                        'url',
                        htmlspecialchars($fundingPlatform->link_url),
                    );
                }
    
            foreach ($podcast->persons as $podcastPerson) {
                $podcastPersonElement = $channel->addChild(
    
                    htmlspecialchars($podcastPerson->person->full_name),
    
                    !empty($podcastPerson->person_role) &&
                    !empty($podcastPerson->person_group)
    
                                "PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
    
                if (!empty($podcastPerson->person_group)) {
                    $podcastPersonElement->addAttribute(
    
                                "PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
    
                if (!empty($podcastPerson->person->information_url)) {
                    $podcastPersonElement->addAttribute(
    
            // 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);
    
            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 .
                        (empty($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 (!empty($episode->location_name)) {
                    $locationElement = $item->addChild(
                        'location',
                        htmlspecialchars($episode->location_name),
                        $podcast_namespace,
                    );
                    if (!empty($episode->location_geo)) {
                        $locationElement->addAttribute(
                            'geo',
                            $episode->location_geo,
                        );
                    }
                    if (!empty($episode->location_osmid)) {
                        $locationElement->addAttribute(
                            'osm',
                            $episode->location_osmid,
                        );
                    }
                }
                $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', null, $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',
                    null,
                    $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,
                            );
                        }