Skip to content
Snippets Groups Projects
Episode.php 20 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\Entities\Media\Audio;
    use App\Entities\Media\Chapters;
    use App\Entities\Media\Image;
    use App\Entities\Media\Transcript;
    
    use App\Models\EpisodeCommentModel;
    
    use CodeIgniter\HTTP\Files\UploadedFile;
    
    use League\CommonMark\Environment\Environment;
    use League\CommonMark\Extension\Autolink\AutolinkExtension;
    use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
    use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
    use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
    use League\CommonMark\MarkdownConverter;
    
    /**
     * @property int $id
     * @property int $podcast_id
     * @property Podcast $podcast
     * @property string $link
     * @property string $guid
     * @property string $slug
     * @property string $title
    
     * @property int $audio_id
     * @property Audio $audio
    
     * @property string $audio_analytics_url
     * @property string $audio_web_url
     * @property string $audio_opengraph_url
    
     * @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 int|null $transcript_id
    
     * @property Transcript|null $transcript
    
     * @property string|null $transcript_remote_url
     * @property int|null $chapters_id
    
     * @property Chapters|null $chapters
    
     * @property string|null $chapters_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 string $embed_url;
    
        protected ?Audio $audio = null;
    
        protected string $audio_analytics_url;
    
        protected string $audio_opengraph_url;
    
        protected string $embed_url;
    
        protected ?Image $cover = null;
    
        protected ?Transcript $transcript = null;
    
        protected ?Chapters $chapters = null;
    
        protected string $custom_rss_string;
    
        protected $dates = ['published_at', 'created_at', 'updated_at', 'deleted_at'];
    
            'guid' => 'string',
    
            'slug' => 'string',
            'title' => 'string',
    
            'description_markdown' => 'string',
            'description_html' => 'string',
    
            'cover_id' => '?integer',
            'transcript_id' => '?integer',
            'transcript_remote_url' => '?string',
            'chapters_id' => '?integer',
            'chapters_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',
    
        public function setCover(UploadedFile | File $file = null): self
    
            if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) {
    
            if (array_key_exists('cover_id', $this->attributes) && $this->attributes['cover_id'] !== null) {
                $this->getCover()
                    ->setFile($file);
                $this->getCover()
                    ->updated_by = (int) user_id();
                (new MediaModel('image'))->updateMedia($this->getCover());
            } else {
                $cover = new Image([
                    'file_name' => $this->attributes['slug'],
    
                    'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
    
                    'sizes' => config('Images')
                        ->podcastCoverSizes,
                    'uploaded_by' => user_id(),
                    'updated_by' => user_id(),
                ]);
                $cover->setFile($file);
    
                $this->attributes['cover_id'] = (new MediaModel('image'))->saveMedia($cover);
            }
    
            if ($this->cover instanceof Image) {
                return $this->cover;
    
            if ($this->cover_id === null) {
                $this->cover = $this->getPodcast()
                    ->getCover();
    
                return $this->cover;
            }
    
            $this->cover = (new MediaModel('image'))->getMediaById($this->cover_id);
    
    
        public function setAudio(UploadedFile | File $file = null): self
    
            if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) {
    
                $this->getAudio()
                    ->setFile($file);
                $this->getAudio()
                    ->updated_by = (int) user_id();
                (new MediaModel('audio'))->updateMedia($this->getAudio());
            } else {
    
                    'file_name' => $this->attributes['slug'],
    
                    'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
    
                    'language_code' => $this->getPodcast()
                        ->language_code,
    
                    'uploaded_by' => user_id(),
                    'updated_by' => user_id(),
                ]);
    
                $this->attributes['audio_id'] = (new MediaModel())->saveMedia($audio);
    
            if (! $this->audio instanceof Audio) {
    
                $this->audio = (new MediaModel('audio'))->getMediaById($this->audio_id);
            }
    
        public function setTranscript(UploadedFile | File $file = null): self
    
            if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) {
    
                return $this;
            }
    
            if ($this->getTranscript() !== null) {
                $this->getTranscript()
                    ->setFile($file);
                $this->getTranscript()
                    ->updated_by = (int) user_id();
                (new MediaModel('transcript'))->updateMedia($this->getTranscript());
            } else {
                $transcript = new Transcript([
                    'file_name' => $this->attributes['slug'] . '-transcript',
    
                    'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
    
                    'language_code' => $this->getPodcast()
                        ->language_code,
    
                    'uploaded_by' => user_id(),
                    'updated_by' => user_id(),
                ]);
                $transcript->setFile($file);
    
    
                $this->attributes['transcript_id'] = (new MediaModel('transcript'))->saveMedia($transcript);
    
            }
    
            return $this;
        }
    
        public function getTranscript(): ?Transcript
    
            if ($this->transcript_id !== null && $this->transcript === null) {
    
                $this->transcript = (new MediaModel('transcript'))->getMediaById($this->transcript_id);
    
        public function setChapters(UploadedFile | File $file = null): self
    
            if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) {
    
                return $this;
            }
    
            if ($this->getChapters() !== null) {
                $this->getChapters()
                    ->setFile($file);
                $this->getChapters()
                    ->updated_by = (int) user_id();
                (new MediaModel('chapters'))->updateMedia($this->getChapters());
            } else {
                $chapters = new Chapters([
                    'file_name' => $this->attributes['slug'] . '-chapters',
    
                    'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
    
                    'language_code' => $this->getPodcast()
                        ->language_code,
    
                    'uploaded_by' => user_id(),
                    'updated_by' => user_id(),
                ]);
                $chapters->setFile($file);
    
    
                $this->attributes['chapters_id'] = (new MediaModel('chapters'))->saveMedia($chapters);
    
            }
    
            return $this;
        }
    
        public function getChapters(): ?Chapters
    
            if ($this->chapters_id !== null && $this->chapters === null) {
    
                $this->chapters = (new MediaModel('chapters'))->getMediaById($this->chapters_id);
    
        public function getAudioAnalyticsUrl(): string
    
            // remove 'podcasts/' from audio file path
    
            $strippedAudioPath = substr($this->getAudio()->file_path, 9);
    
            return generate_episode_analytics_url(
                $this->podcast_id,
                $this->id,
    
                $strippedAudioPath,
    
                $this->audio->duration,
                $this->audio->file_size,
                $this->audio->header_size,
    
        public function getAudioWebUrl(): string
    
    Benjamin Bellamy's avatar
    Benjamin Bellamy committed
        {
    
            return $this->getAudioAnalyticsUrl() . '?_from=-+Website+-';
    
        public function getAudioOpengraphUrl(): string
    
            return $this->getAudioAnalyticsUrl() . '?_from=-+Open+Graph+-';
    
         * Gets transcript url from transcript file uri if it exists or returns the transcript_remote_url which can be null.
    
        public function getTranscriptUrl(): ?string
    
                return $this->transcript->file_url;
    
            return $this->transcript_remote_url;
    
         * Gets chapters file url from chapters file uri if it exists or returns the chapters_remote_url which can be null.
    
            if ($this->chapters !== null) {
                return $this->chapters->file_url;
    
        /**
         * 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);
    
                throw new RuntimeException('Episode must be created before getting soundbites.');
    
            if ($this->soundbites === null) {
                $this->soundbites = (new ClipModel())->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
    
            ];
    
            $environment = new Environment($config);
            $environment->addExtension(new CommonMarkCoreExtension());
            $environment->addExtension(new AutolinkExtension());
            $environment->addExtension(new SmartPunctExtension());
            $environment->addExtension(new DisallowedRawHtmlExtension());
    
            $converter = new MarkdownConverter($environment);
    
            $this->attributes['description_markdown'] = $descriptionMarkdown;
    
            $this->attributes['description_html'] = $converter->convert($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 : '');