Skip to content
Snippets Groups Projects
EpisodeModel.php 13.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/
     */
    
    
    namespace App\Models;
    
    use CodeIgniter\Model;
    
    class EpisodeModel extends Model
    {
        protected $table = 'episodes';
        protected $primaryKey = 'id';
    
        protected $allowedFields = [
    
            'enclosure_duration',
            'enclosure_mimetype',
            'enclosure_filesize',
    
            'description_markdown',
            'description_html',
    
            'transcript_uri',
            'chapters_uri',
    
            'location_name',
            'location_geo',
            'location_osmid',
    
            'favourites_total',
            'reblogs_total',
            'notes_total',
    
            'published_at',
            'created_by',
            'updated_by',
    
        protected $returnType = \App\Entities\Episode::class;
    
        protected $useSoftDeletes = true;
    
        protected $useTimestamps = true;
    
        protected $validationRules = [
            'podcast_id' => 'required',
            'title' => 'required',
            'slug' => 'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]',
            'enclosure_uri' => 'required',
    
            'description_markdown' => 'required',
    
            'number' => 'is_natural_no_zero|permit_empty',
            'season_number' => 'is_natural_no_zero|permit_empty',
    
            'published_at' => 'valid_date|permit_empty',
            'created_by' => 'required',
            'updated_by' => 'required',
    
        protected $afterInsert = ['writeEnclosureMetadata', 'clearCache'];
    
        // clear cache beforeUpdate because if slug changes, so will the episode link
        protected $beforeUpdate = ['clearCache'];
        protected $afterUpdate = ['writeEnclosureMetadata'];
    
        protected $beforeDelete = ['clearCache'];
    
        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;',
                'background' => 'transparent',
                'text' => '#000',
                'inverted' => '#fff',
            ],
            'light' => [
                'style' => 'background-color: #fff;',
                'background' => '#fff',
                'text' => '#000',
                'inverted' => '#fff',
            ],
            'dark-transparent' => [
                '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;',
                'background' => 'transparent',
                'text' => '#fff',
                'inverted' => '#000',
            ],
            'dark' => [
                'style' => 'background-color: #001f1a;',
                'background' => '#001f1a',
                'text' => '#fff',
                'inverted' => '#000',
            ],
        ];
    
    
        /**
         *
         * @param int|string $podcastId Podcast Id or name
         * @param mixed $episodeSlug
         * @return mixed
         */
    
        public function getEpisodeBySlug($podcastId, $episodeSlug)
        {
    
            if (!($found = cache("podcast@{$podcastId}_episode@{$episodeSlug}"))) {
                $builder = $this->select('episodes.*')
                    ->where('slug', $episodeSlug)
                    ->where('`published_at` <= NOW()', null, false);
    
                if (is_numeric($podcastId)) {
                    // passed argument is the podcast id
                    $builder->where('podcast_id', $podcastId);
                } else {
                    // passed argument is the podcast name, must perform join
                    $builder
                        ->join('podcasts', 'podcasts.id = episodes.podcast_id')
                        ->where('podcasts.name', $podcastId);
                }
    
                $found = $builder->first();
    
    
                cache()->save(
                    "podcast{$podcastId}_episode@{$episodeSlug}",
                    $found,
    
        public function getEpisodeById($episodeId)
    
            if (!($found = cache("podcast_episode{$episodeId}"))) {
                $builder = $this->where([
    
                $found = $builder->first();
    
                cache()->save("podcast_episode{$episodeId}", $found, DECADE);
    
        public function getPublishedEpisodeById($episodeId, $podcastId = null)
    
            if (!($found = cache("podcast{$podcastId}_episode{$episodeId}"))) {
                $builder = $this->where([
                    'id' => $episodeId,
                ])->where('`published_at` <= NOW()', null, false);
    
                if ($podcastId) {
                    $builder->where('podcast_id', $podcastId);
                }
    
                $found = $builder->first();
    
                cache()->save(
                    "podcast{$podcastId}_episode{$episodeId}",
                    $found,
                    DECADE,
                );
            }
    
            return $found;
    
         * 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;
                }
    
                if ($podcastType == 'serial') {
                    // podcast is serial
                    $found = $this->where($where)
    
                        ->orderBy('season_number DESC, number ASC')
                        ->findAll();
                } else {
                    $found = $this->where($where)
    
                        ->orderBy('published_at', 'DESC')
                        ->findAll();
                }
    
    
                $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode(
    
                );
    
                cache()->save(
                    $cacheName,
                    $found,
                    $secondsToNextUnpublishedEpisode
                        ? $secondsToNextUnpublishedEpisode
    
            }
    
            return $found;
        }
    
        public function getYears(int $podcastId): array
        {
            if (!($found = cache("podcast{$podcastId}_years"))) {
                $found = $this->select(
    
                    'YEAR(published_at) as year, count(*) as number_of_episodes',
    
                    ->where([
                        'podcast_id' => $podcastId,
                        'season_number' => null,
                        $this->deletedField => null,
                    ])
    
                    ->groupBy('year')
                    ->orderBy('year', 'DESC')
                    ->get()
                    ->getResultArray();
    
    
                $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode(
    
                );
    
                cache()->save(
                    "podcast{$podcastId}_years",
                    $found,
                    $secondsToNextUnpublishedEpisode
                        ? $secondsToNextUnpublishedEpisode
    
            }
    
            return $found;
        }
    
        public function getSeasons(int $podcastId): array
    
            if (!($found = cache("podcast{$podcastId}_seasons"))) {
                $found = $this->select(
    
                    'season_number, count(*) as number_of_episodes',
    
                )
                    ->where([
                        'podcast_id' => $podcastId,
                        'season_number is not' => null,
    
                    ->groupBy('season_number')
                    ->orderBy('season_number', 'ASC')
                    ->get()
                    ->getResultArray();
    
                $secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode(
    
                );
    
                cache()->save(
                    "podcast{$podcastId}_seasons",
                    $found,
                    $secondsToNextUnpublishedEpisode
                        ? $secondsToNextUnpublishedEpisode
    
    
        /**
         * Returns the default query for displaying the episode list on the podcast page
    
         */
        public function getDefaultQuery(int $podcastId)
        {
            if (!($defaultQuery = cache("podcast{$podcastId}_defaultQuery"))) {
                $seasons = $this->getSeasons($podcastId);
    
                if (!empty($seasons)) {
                    // get latest season
                    $defaultQuery = ['type' => 'season', 'data' => end($seasons)];
                } else {
                    $years = $this->getYears($podcastId);
                    if (!empty($years)) {
                        // get most recent year
                        $defaultQuery = ['type' => 'year', 'data' => $years[0]];
                    } else {
                        $defaultQuery = null;
                    }
                }
    
                cache()->save(
                    "podcast{$podcastId}_defaultQuery",
                    $defaultQuery,
    
        /**
         * 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
         *
         * @param int $podcastId
         *
         * @return int|false seconds
         */
        public function getSecondsToNextUnpublishedEpisode(int $podcastId)
        {
            $result = $this->select(
    
                'TIMESTAMPDIFF(SECOND, NOW(), `published_at`) as timestamp_diff',
    
            )
                ->where([
                    'podcast_id' => $podcastId,
                ])
                ->where('`published_at` > NOW()', null, false)
                ->orderBy('published_at', 'asc')
                ->get()
                ->getResultArray();
    
            return (int) $result ? $result[0]['timestamp_diff'] : false;
        }
    
        protected function writeEnclosureMetadata(array $data)
        {
            helper('id3');
    
            $episode = (new EpisodeModel())->find(
    
                is_array($data['id']) ? $data['id'][0] : $data['id'],
    
        public function clearCache(array $data)
    
                is_array($data['id']) ? $data['id'][0] : $data['id'],
    
            cache()->delete("podcast{$episode->podcast_id}_feed");
            foreach (\Opawg\UserAgentsPhp\UserAgentsRSS::$db as $service) {
                cache()->delete(
    
                    "podcast{$episode->podcast_id}_feed_{$service['slug']}",
    
            // delete model requests cache
            cache()->delete("podcast{$episode->podcast_id}_episodes");
    
                "podcast{$episode->podcast_id}_episode@{$episode->slug}",
    
            cache()->delete("podcast_episode{$episode->id}");
    
    
            // delete episode lists cache per year / season for a podcast
            // and localized pages
    
            $years = $episodeModel->getYears($episode->podcast_id);
            $seasons = $episodeModel->getSeasons($episode->podcast_id);
            $supportedLocales = config('App')->supportedLocales;
    
            foreach ($supportedLocales as $locale) {
                cache()->delete(
    
                    "page_podcast{$episode->podcast->id}_episode{$episode->id}_{$locale}",
    
                cache()->delete("credits_{$locale}");
    
                    "podcast{$episode->podcast_id}_{$year['year']}_episodes",
    
                );
                foreach ($supportedLocales as $locale) {
                    cache()->delete(
    
                        "page_podcast{$episode->podcast_id}_{$year['year']}_{$locale}",
    
                    "podcast{$episode->podcast_id}_season{$season['season_number']}_episodes",
    
                );
                foreach ($supportedLocales as $locale) {
                    cache()->delete(
    
                        "page_podcast{$episode->podcast_id}_season{$season['season_number']}_{$locale}",
    
            foreach (array_keys(self::$themes) as $themeKey) {
                foreach ($supportedLocales as $locale) {
                    cache()->delete(
    
                        "page_podcast{$episode->podcast_id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}",
    
            // delete query cache
            cache()->delete("podcast{$episode->podcast_id}_defaultQuery");
            cache()->delete("podcast{$episode->podcast_id}_years");
            cache()->delete("podcast{$episode->podcast_id}_seasons");
    
            return $data;