Skip to content
Snippets Groups Projects
Episode.php 17.8 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 App\Models\EpisodeCommentModel;
    
    use CodeIgniter\Entity\Entity;
    use CodeIgniter\Files\File;
    use CodeIgniter\HTTP\Files\UploadedFile;
    
    use League\CommonMark\CommonMarkConverter;
    
    /**
     * @property int $id
     * @property int $podcast_id
     * @property Podcast $podcast
     * @property string $link
     * @property string $guid
     * @property string $slug
     * @property string $title
     * @property File $audio_file
     * @property string $audio_file_url
     * @property string $audio_file_analytics_url
     * @property string $audio_file_web_url
     * @property string $audio_file_opengraph_url
     * @property string $audio_file_path
     * @property double $audio_file_duration
     * @property string $audio_file_mimetype
     * @property int $audio_file_size
     * @property int $audio_file_header_size
    
     * @property string|null $description Holds text only description, striped of any markdown or html special characters
    
     * @property string $description_markdown
     * @property string $description_html
     * @property Image $image
     * @property string|null $image_path
     * @property string|null $image_mimetype
     * @property File|null $transcript_file
     * @property string|null $transcript_file_url
     * @property string|null $transcript_file_path
     * @property string|null $transcript_file_remote_url
     * @property File|null $chapters_file
     * @property string|null $chapters_file_url
     * @property string|null $chapters_file_path
     * @property string|null $chapters_file_remote_url
     * @property string|null $parental_advisory
     * @property int $number
     * @property int $season_number
     * @property string $type
     * @property bool $is_blocked
    
     * @property string|null $location_name
     * @property string|null $location_geo
    
     * @property array|null $custom_rss
     * @property string $custom_rss_string
    
     * @property int $posts_count
     * @property int $comments_count
    
     * @property int $created_by
     * @property int $updated_by
     * @property string $publication_status;
     * @property Time|null $published_at;
     * @property Time $created_at;
     * @property Time $updated_at;
     * @property Time|null $deleted_at;
     *
    
     * @property Person[] $persons;
    
     * @property Soundbite[] $soundbites;
    
     * @property string $embed_url;
    
        protected string $audio_file_url;
    
        protected string $audio_file_analytics_url;
    
        protected string $audio_file_web_url;
    
        protected string $audio_file_opengraph_url;
    
        protected string $embed_url;
    
        protected File $transcript_file;
    
        protected File $chapters_file;
    
        protected ?array $soundbites = null;
    
        protected string $custom_rss_string;
    
        protected $dates = ['published_at', 'created_at', 'updated_at', 'deleted_at'];
    
            'guid' => 'string',
    
            'slug' => 'string',
            'title' => 'string',
    
            'audio_file_duration' => 'double',
    
            'audio_file_mimetype' => 'string',
            'audio_file_size' => 'integer',
            'audio_file_header_size' => 'integer',
    
            'description_markdown' => 'string',
            'description_html' => 'string',
    
            'transcript_file_path' => '?string',
            'transcript_file_remote_url' => '?string',
            'chapters_file_path' => '?string',
            'chapters_file_remote_url' => '?string',
    
            'parental_advisory' => '?string',
    
            'number' => '?integer',
    
            'season_number' => '?integer',
            'type' => 'string',
    
            'location_name' => '?string',
            'location_geo' => '?string',
    
            'posts_count' => 'integer',
            'comments_count' => 'integer',
    
            'created_by' => 'integer',
            'updated_by' => 'integer',
    
        /**
         * Saves an episode image
         */
    
        public function setImage(?Image $image = null): static
    
            if ($image === null) {
                return $this;
    
            $image->saveImage('podcasts/' . $this->getPodcast()->handle, $this->attributes['slug']);
    
    
            $this->attributes['image_mimetype'] = $image->mimetype;
            $this->attributes['image_path'] = $image->path;
    
    
            if ($imagePath = $this->attributes['image_path']) {
    
                return new Image(null, $imagePath, $this->attributes['image_mimetype']);
    
        public function setAudioFile(UploadedFile | File $audioFile): static
    
            $this->attributes['audio_file_path'] = save_media(
                $audioFile,
    
                'podcasts/' . $this->getPodcast()->handle,
    
                $this->attributes['slug'],
            );
            $this->attributes['audio_file_duration'] =
    
                $audioMetadata['playtime_seconds'];
            $this->attributes['audio_file_mimetype'] = $audioMetadata['mime_type'];
            $this->attributes['audio_file_size'] = $audioMetadata['filesize'];
    
            $this->attributes['audio_file_header_size'] =
    
         * Saves an episode transcript file
    
        public function setTranscriptFile(UploadedFile | File $transcriptFile): static
    
            $this->attributes['transcript_file_path'] = save_media(
                $transcriptFile,
    
                'podcasts/' . $this->getPodcast()
    
                $this->attributes['slug'] . '-transcript',
            );
    
         * Saves an episode chapters file
    
        public function setChaptersFile(UploadedFile | File $chaptersFile): static
    
            $this->attributes['chapters_file_path'] = save_media(
                $chaptersFile,
    
                'podcasts/' . $this->getPodcast()
    
                $this->attributes['slug'] . '-chapters',
            );
    
        public function getAudioFile(): File
    
            return new File(media_path($this->audio_file_path));
    
        public function getTranscriptFile(): ?File
    
            if ($this->attributes['transcript_file_path']) {
                helper('media');
    
                return new File(media_path($this->attributes['transcript_file_path']));
    
        public function getChaptersFile(): ?File
    
            if ($this->attributes['chapters_file_path']) {
                helper('media');
    
                return new File(media_path($this->attributes['chapters_file_path']));
    
        public function getAudioFileUrl(): string
    
        public function getAudioFileAnalyticsUrl(): string
    
            // remove 'podcasts/' from audio file path
            $strippedAudioFilePath = substr($this->audio_file_path, 9);
    
    
            return generate_episode_analytics_url(
                $this->podcast_id,
                $this->id,
    
                $this->audio_file_duration,
                $this->audio_file_size,
                $this->audio_file_header_size,
    
        public function getAudioFileWebUrl(): string
    
    Benjamin Bellamy's avatar
    Benjamin Bellamy committed
        {
    
            return $this->getAudioFileAnalyticsUrl() . '?_from=-+Website+-';
    
        public function getAudioFileOpengraphUrl(): string
    
            return $this->getAudioFileAnalyticsUrl() . '?_from=-+Open+Graph+-';
    
         * Gets transcript url from transcript file uri if it exists or returns the transcript_file_remote_url which can be
         * null.
    
        public function getTranscriptFileUrl(): ?string
    
            if ($this->attributes['transcript_file_path']) {
    
                return media_base_url($this->attributes['transcript_file_path']);
    
            return $this->attributes['transcript_file_remote_url'];
    
         * Gets chapters file url from chapters file uri if it exists or returns the chapters_file_remote_url which can be
         * null.
    
            if ($this->chapters_file_path) {
                return media_base_url($this->chapters_file_path);
    
        /**
         * Returns the episode's persons
         *
    
        public function getPersons(): array
    
                throw new RuntimeException('Episode must be created before getting persons.');
    
                $this->persons = (new PersonModel())->getEpisodePersons($this->podcast_id, $this->id);
    
        /**
         * Returns the episode’s soundbites
         *
    
        public function getSoundbites(): array
    
                throw new RuntimeException('Episode must be created before getting soundbites.');
    
                $this->soundbites = (new SoundbiteModel())->getEpisodeSoundbites($this->getPodcast() ->id, $this->id);
    
                throw new RuntimeException('Episode must be created before getting posts.');
    
            if ($this->posts === null) {
                $this->posts = (new PostModel())->getEpisodePosts($this->id);
    
         */
        public function getComments(): array
        {
            if ($this->id === null) {
                throw new RuntimeException('Episode must be created before getting comments.');
            }
    
            if ($this->comments === null) {
    
                $this->comments = (new EpisodeCommentModel())->getEpisodeComments($this->id);
    
        public function getLink(): string
    
            return url_to('episode', $this->getPodcast()->handle, $this->attributes['slug']);
    
        public function getEmbedUrl(string $theme = null): string
    
                    ? route_to('embed-theme', $this->getPodcast() ->handle, $this->attributes['slug'], $theme,)
                    : route_to('embed', $this->getPodcast()->handle, $this->attributes['slug']),
    
        public function setGuid(?string $guid = null): static
    
            $this->attributes['guid'] = $guid === null ? $this->getLink() : $guid;
    
        public function getPodcast(): ?Podcast
    
            return (new PodcastModel())->getPodcastById($this->podcast_id);
    
        public function setDescriptionMarkdown(string $descriptionMarkdown): static
    
            $converter = new CommonMarkConverter([
                'html_input' => 'strip',
                'allow_unsafe_links' => false,
            ]);
    
            $this->attributes['description_markdown'] = $descriptionMarkdown;
    
            $this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown);
    
        public function getDescriptionHtml(?string $serviceSlug = null): string
    
                $this->getPodcast()
                    ->partner_id !== null &&
                $this->getPodcast()
                    ->partner_link_url !== null &&
                $this->getPodcast()
                    ->partner_image_url !== null
    
                $descriptionHtml .= "<div><a href=\"{$this->getPartnerLink(
                    $serviceSlug,
                )}\" rel=\"sponsored noopener noreferrer\" target=\"_blank\"><img src=\"{$this->getPartnerImageUrl(
                    $serviceSlug,
    
                )}\" alt=\"Partner image\" /></a></div>";
            }
    
            $descriptionHtml .= $this->attributes['description_html'];
    
            if ($this->getPodcast()->episode_description_footer_html) {
    
                $descriptionHtml .= "<footer>{$this->getPodcast()
                    ->episode_description_footer_html}</footer>";
    
        public function getDescription(): string
    
            if ($this->description === null) {
                $this->description = trim(
    
                    preg_replace('~\s+~', ' ', strip_tags($this->attributes['description_html'])),
    
        public function getPublicationStatus(): string
    
            if ($this->publication_status === null) {
                if ($this->published_at === null) {
                    $this->publication_status = 'not_published';
                } elseif ($this->published_at->isBefore(Time::now())) {
                    $this->publication_status = 'published';
                } else {
                    $this->publication_status = 'scheduled';
                }
    
    
        /**
         * Saves the location name and fetches OpenStreetMap info
         */
    
        public function setLocation(?Location $location = null): static
    
                $this->attributes['location_name'] = null;
                $this->attributes['location_geo'] = null;
    
                ! isset($this->attributes['location_name']) ||
    
                $this->attributes['location_name'] !== $location->name
    
                $this->attributes['location_name'] = $location->name;
                $this->attributes['location_geo'] = $location->geo;
                $this->attributes['location_osm'] = $location->osm;
    
        public function getLocation(): ?Location
        {
            if ($this->location_name === null) {
                return null;
            }
    
            if ($this->location === null) {
    
                $this->location = new Location($this->location_name, $this->location_geo, $this->location_osm);
    
        public function getCustomRssString(): string
    
            if ($this->custom_rss === null) {
    
    
            helper('rss');
    
            $xmlNode = (new SimpleRSSElement(
                '<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
            ))
                ->addChild('channel')
                ->addChild('item');
    
            array_to_rss([
                'elements' => $this->custom_rss,
    
            return str_replace(['<item>', '</item>'], '', $xmlNode->asXML());
    
        public function setCustomRssString(?string $customRssString = null): static
    
            if ($customRssString === '') {
                $this->attributes['custom_rss'] = null;
    
            helper('rss');
            $customRssArray = rss_to_array(
                simplexml_load_string(
                    '<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><item>' .
                        $customRssString .
    
            if (array_key_exists('elements', $customRssArray)) {
    
                $this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
    
        public function getPartnerLink(?string $serviceSlug = null): string
    
            $partnerLink =
                rtrim($this->getPodcast()->partner_link_url, '/') .
    
                urlencode($this->attributes['guid']);
    
            if ($serviceSlug !== null) {
                $partnerLink .= '&_from=' . $serviceSlug;
            }
    
            return $partnerLink;
    
        public function getPartnerImageUrl(string $serviceSlug = null): string
    
            return rtrim($this->getPodcast()->partner_image_url, '/') .
    
                urlencode($this->attributes['guid']) .
                ($serviceSlug !== null ? '&_from=' . $serviceSlug : '');