Skip to content
Snippets Groups Projects
EpisodeModel.php 16.6 KiB
Newer Older
  • Learn to ignore specific revisions
  •  * @copyright  2020 Ad Aures
    
     * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
     * @link       https://castopod.org/
     */
    
    use CodeIgniter\Database\BaseResult;
    
    use CodeIgniter\I18n\Time;
    
    use Michalsn\Uuid\UuidModel;
    use Ramsey\Uuid\Lazy\LazyUuidFromString;
    
         * @var array<string, array<string, string>>
         */
        public static $themes = [
            'light-transparent' => [
    
                'style'      => 'background-color: #fff; background-image: linear-gradient(45deg, #ccc 12.5%, transparent 12.5%, transparent 50%, #ccc 50%, #ccc 62.5%, transparent 62.5%, transparent 100%); background-size: 5.66px 5.66px;',
    
                'text'       => '#000',
                'inverted'   => '#fff',
    
                'style'      => 'background-color: #fff;',
    
                'text'       => '#000',
                'inverted'   => '#fff',
    
                'style'      => 'background-color: #001f1a; background-image: linear-gradient(45deg, #888 12.5%, transparent 12.5%, transparent 50%, #888 50%, #888 62.5%, transparent 62.5%, transparent 100%); background-size: 5.66px 5.66px;',
    
                'text'       => '#fff',
                'inverted'   => '#000',
    
                'style'      => 'background-color: #001f1a;',
    
                'text'       => '#fff',
                'inverted'   => '#000',
    
        /**
         * @var string[]
         */
        protected $uuidFields = ['preview_id'];
    
    
            'description_markdown',
            'description_html',
    
            'transcript_id',
            'transcript_remote_url',
            'chapters_id',
            'chapters_remote_url',
    
            'location_name',
            'location_geo',
    
            'published_at',
            'created_by',
            'updated_by',
    
            'podcast_id'            => 'required',
            'title'                 => 'required',
            'slug'                  => 'required|regex_match[/^[a-zA-Z0-9\-]{1,128}$/]',
            'audio_id'              => 'required',
            'description_markdown'  => 'required',
            'number'                => 'is_natural_no_zero|permit_empty',
            'season_number'         => 'is_natural_no_zero|permit_empty',
            'type'                  => 'required',
    
            'transcript_remote_url' => 'valid_url_strict|permit_empty',
    
            'chapters_remote_url'   => 'valid_url_strict|permit_empty',
            'published_at'          => 'valid_date|permit_empty',
            'created_by'            => 'required',
            'updated_by'            => 'required',
    
        protected $afterInsert = ['writeEnclosureMetadata', 'clearCache'];
    
        protected $afterUpdate = ['clearCache', 'writeEnclosureMetadata'];
    
        public function getEpisodeBySlug(string $podcastHandle, string $episodeSlug): ?Episode
    
            $cacheName = "podcast-{$podcastHandle}_episode-{$episodeSlug}";
    
                $found = $this->select('episodes.*')
                    ->join('podcasts', 'podcasts.id = episodes.podcast_id')
    
                    ->where('podcasts.handle', $podcastHandle)
    
                    ->where('`' . $this->db->getPrefix() . 'episodes`.`published_at` <= UTC_TIMESTAMP()', null, false)
    
                cache()
                    ->save($cacheName, $found, DECADE);
    
        public function getEpisodeById(int $episodeId): ?Episode
    
            // TODO: episode id should be a composite key. The cache should include podcast_id.
    
            $cacheName = "podcast_episode#{$episodeId}";
    
                cache()
                    ->save($cacheName, $found, DECADE);
    
        public function getPublishedEpisodeById(int $podcastId, int $episodeId): ?Episode
    
            $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_published";
    
                    ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
    
                cache()
                    ->save($cacheName, $found, DECADE);
    
        public function getEpisodeByPreviewId(string $previewId): ?Episode
        {
            $cacheName = "podcast_episode#preview-{$previewId}";
            if (! ($found = cache($cacheName))) {
                $builder = $this->where([
                    'preview_id' => $this->uuid->fromString($previewId)
                        ->getBytes(),
                ]);
    
                $found = $builder->first();
    
                cache()
                    ->save($cacheName, $found, DECADE);
            }
    
            return $found;
        }
    
        public function setEpisodePreviewId(int $episodeId): string|false
        {
            /** @var LazyUuidFromString $uuid */
            $uuid = $this->uuid->{$this->uuidVersion}();
    
            if (! $this->update($episodeId, [
                'preview_id' => $uuid,
            ])) {
                return false;
            }
    
            return (string) $uuid;
        }
    
    
         * Gets all episodes for a podcast ordered according to podcast type Filtered depending on year or season
    
        public function getPodcastEpisodes(
            int $podcastId,
            string $podcastType,
            string $year = null,
            string $season = null
        ): array {
            $cacheName = implode(
                '_',
    
                array_filter(["podcast#{$podcastId}", $year, $season ? 'season' . $season : null, 'episodes']),
    
                if ($year) {
                    $where['YEAR(published_at)'] = $year;
                    $where['season_number'] = null;
                }
    
                if ($season) {
                    $where['season_number'] = $season;
                }
    
    
                    // podcast is serial
                    $found = $this->where($where)
    
                        ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
    
                        ->orderBy('season_number DESC, number ASC')
                        ->findAll();
                } else {
                    $found = $this->where($where)
    
                        ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
    
                        ->orderBy('published_at', 'DESC')
                        ->findAll();
                }
    
    
                $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode($podcastId);
    
                cache()
                    ->save(
                        $cacheName,
                        $found,
                        $secondsToNextUnpublishedEpisode
    
        /**
         * Returns number of episodes of a podcast
         */
        public function getPodcastEpisodesCount(int $podcastId): int|string
        {
            return $this
                ->where([
                    'podcast_id' => $podcastId,
                ])
                ->countAllResults();
        }
    
    
         * Returns the timestamp difference in seconds between the next episode to publish and the current timestamp Returns
         * false if there's no episode to publish
    
        public function getSecondsToNextUnpublishedEpisode(int $podcastId): int | false
    
            $result = $this->builder()
                ->select('TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP(), `published_at`) as timestamp_diff')
    
                ->where('`published_at` > UTC_TIMESTAMP()', null, false)
    
                ? (int) $result[0]['timestamp_diff']
                : false;
    
        public function getCurrentSeasonNumber(int $podcastId): ?int
        {
    
            $result = $this->builder()
    
                ->selectMax('season_number', 'current_season_number')
    
                    'published_at IS NOT' => null,
    
                ])
                ->get()
                ->getResultArray();
    
            return $result[0]['current_season_number'] ? (int) $result[0]['current_season_number'] : null;
        }
    
        public function getNextEpisodeNumber(int $podcastId, ?int $seasonNumber): int
        {
    
            $result = $this->builder()
    
                ->selectMax('number', 'next_episode_number')
    
                    'podcast_id'          => $podcastId,
                    'season_number'       => $seasonNumber,
    
                    'published_at IS NOT' => null,
    
                ])->get()
                ->getResultArray();
    
            return (int) $result[0]['next_episode_number'] + 1;
        }
    
    
         * @return array{number_of_seasons: int, number_of_episodes: int, first_published_at?: Time}
    
         */
        public function getPodcastStats(int $podcastId): array
        {
    
            $result = $this->builder()
                ->select(
                    'COUNT(DISTINCT season_number) as number_of_seasons, COUNT(*) as number_of_episodes, MIN(published_at) as first_published_at'
                )
    
                    'published_at IS NOT' => null,
                ])->get()
                ->getResultArray();
    
            $stats = [
    
                'number_of_seasons'  => (int) $result[0]['number_of_seasons'],
    
                'number_of_episodes' => (int) $result[0]['number_of_episodes'],
            ];
    
            if ($result[0]['first_published_at'] !== null) {
                $stats['first_published_at'] = new Time($result[0]['first_published_at']);
            }
    
            return $stats;
        }
    
    
        public function resetCommentsCount(): int | false
        {
    
            $episodeCommentsCount = (new EpisodeCommentModel())->builder()
                ->select('episode_id, COUNT(*) as `comments_count`')
    
            $episodePostsRepliesCount = (new PostModel())->builder()
    
                ->select('fediverse_posts.episode_id as episode_id, COUNT(*) as `comments_count`')
                ->join('fediverse_posts as fp', 'fediverse_posts.id = fp.in_reply_to_id')
                ->where('fediverse_posts.in_reply_to_id', null)
                ->groupBy('fediverse_posts.episode_id')
    
            /** @var BaseResult $query */
    
                'SELECT `episode_id` as `id`, SUM(`comments_count`) as `comments_count` FROM (' . $episodeCommentsCount . ' UNION ALL ' . $episodePostsRepliesCount . ') x GROUP BY `episode_id`'
    
            );
    
            $countsPerEpisodeId = $query->getResultArray();
    
            if ($countsPerEpisodeId !== []) {
    
                return (new self())->updateBatch($countsPerEpisodeId, 'id');
    
            }
    
            return 0;
        }
    
        public function resetPostsCount(): int | false
        {
    
            $episodePostsCount = $this->builder()
                ->select('episodes.id, COUNT(*) as `posts_count`')
    
                ->join('fediverse_posts', 'episodes.id = fediverse_posts.episode_id')
    
                ->where('in_reply_to_id', null)
                ->groupBy('episodes.id')
                ->get()
                ->getResultArray();
    
            if ($episodePostsCount !== []) {
                return $this->updateBatch($episodePostsCount, 'id');
            }
    
            return 0;
        }
    
    
        public function clearCache(array $data): array
    
            /** @var int|null $episodeId */
            $episodeId = is_array($data['id']) ? $data['id'][0] : $data['id'];
    
            if ($episodeId === null) {
                // Multiple episodes have been updated, do nothing
                return $data;
            }
    
            /** @var ?Episode $episode */
            $episode = (new self())->find($episodeId);
    
            if (! $episode instanceof Episode) {
                return $data;
            }
    
                ->deleteMatching("podcast#{$episode->podcast_id}*");
    
                ->deleteMatching("podcast-{$episode->podcast->handle}*");
    
            cache()
                ->delete("podcast_episode#{$episode->id}");
            cache()
    
                ->deleteMatching("page_podcast#{$episode->podcast_id}*");
    
            cache()
                ->deleteMatching('page_credits_*');
    
            cache()
                ->delete('episodes_markers');
    
        public function doesPodcastHavePremiumEpisodes(int $podcastId): bool
        {
            return $this->builder()
                ->where([
                    'podcast_id' => $podcastId,
                    'is_premium' => true,
    
                ])
                ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
                ->countAllResults() > 0;
    
        public function fullTextSearch(string $query): ?BaseBuilder
        {
            $prefix = $this->db->getPrefix();
            $episodeTable = $prefix . $this->builder()->getTable();
    
            $podcastModel = (new PodcastModel());
    
            $podcastTable = $podcastModel->db->getPrefix() . $podcastModel->builder()->getTable();
    
            $this->builder()
                ->select('' . $episodeTable . '.*')
                ->select('
                    ' . $this->getFullTextMatchClauseForEpisodes($episodeTable, $query) . ' as episodes_score,
                    ' . $podcastModel->getFullTextMatchClauseForPodcasts($podcastTable, $query) . ' as podcasts_score,
                 ')
                ->select("{$podcastTable}.created_at AS podcast_created_at")
                ->select(
                    "{$podcastTable}.title as podcast_title, {$podcastTable}.handle as podcast_handle, {$podcastTable}.description_markdown as podcast_description_markdown"
                )
                ->join($podcastTable, "{$podcastTable} on {$podcastTable}.id = {$episodeTable}.podcast_id")
                ->where('
                    (' .
                        $this->getFullTextMatchClauseForEpisodes($episodeTable, $query)
                        . 'OR' .
                        $podcastModel->getFullTextMatchClauseForPodcasts($podcastTable, $query)
                    . ')
                ');
    
            return $this->builder;
        }
    
    
        public function getFullTextMatchClauseForEpisodes(string $table, string $value): string
        {
            return '
                    MATCH (
                        ' . $table . '.title,
                        ' . $table . '.description_markdown,
                        ' . $table . '.slug,
                        ' . $table . '.location_name
                    )
                    AGAINST(' . $this->db->escape($value) . ')
                ';
        }
    
    
         */
        protected function writeEnclosureMetadata(array $data): array
        {
    
            /** @var int|null $episodeId */
            $episodeId = is_array($data['id']) ? $data['id'][0] : $data['id'];
    
            if ($episodeId === null) {
                // Multiple episodes have been updated, do nothing
                return $data;
            }
    
            /** @var ?Episode $episode */
            $episode = (new self())->find($episodeId);
    
            if (! $episode instanceof Episode) {
                return $data;
            }
    
            helper('id3');