Newer
Older
<?php

Yassine Doghri
committed

Yassine Doghri
committed
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use App\Entities\Media\Audio;
use App\Entities\Media\Chapters;
use App\Entities\Media\Image;
use App\Entities\Media\Transcript;

Yassine Doghri
committed
use App\Libraries\SimpleRSSElement;
use App\Models\ClipsModel;
use App\Models\EpisodeCommentModel;
use App\Models\MediaModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\PostModel;

Yassine Doghri
committed
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;

Yassine Doghri
committed
use CodeIgniter\I18n\Time;

Yassine Doghri
committed
use League\CommonMark\CommonMarkConverter;

Yassine Doghri
committed
use RuntimeException;
/**
* @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_file_analytics_url
* @property string $audio_file_web_url
* @property string $audio_file_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 $cover_id

Yassine Doghri
committed
* @property Image $cover
* @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

Yassine Doghri
committed
* @property Location|null $location
* @property string|null $location_name
* @property string|null $location_geo

Yassine Doghri
committed
* @property string|null $location_osm
* @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 Clip[] $clips;
* @property string $embed_url;
class Episode extends Entity
{
protected Podcast $podcast;
protected string $link;
protected ?Audio $audio = null;
protected string $audio_url;
protected string $audio_analytics_url;
protected string $audio_web_url;
protected string $audio_opengraph_url;
protected string $embed_url;
protected ?Image $cover = null;

Yassine Doghri
committed
protected ?string $description = null;
protected ?Transcript $transcript = null;
protected ?Chapters $chapters = null;
* @var Person[]|null
protected ?array $persons = null;
* @var Clip[]|null
protected ?array $clips = null;
/**
* @var Post[]|null
*/
protected ?array $posts = null;

Yassine Doghri
committed
/**
* @var EpisodeComment[]|null

Yassine Doghri
committed
*/
protected ?array $comments = null;

Yassine Doghri
committed
protected ?Location $location = null;
protected string $custom_rss_string;

Yassine Doghri
committed
protected ?string $publication_status = null;

Yassine Doghri
committed

Benjamin Bellamy
committed
/**
* @var string[]

Benjamin Bellamy
committed
*/
protected $dates = ['published_at', 'created_at', 'updated_at', 'deleted_at'];
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
'slug' => 'string',
'title' => 'string',
'audio_id' => 'integer',
'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',
'season_number' => '?integer',
'type' => 'string',
'is_blocked' => 'boolean',
'location_name' => '?string',
'location_geo' => '?string',

Yassine Doghri
committed
'location_osm' => '?string',

Benjamin Bellamy
committed
'custom_rss' => '?json-array',
'posts_count' => 'integer',
'comments_count' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
public function setCover(?UploadedFile $file): self
if ($file === null || ! $file->isValid()) {
return $this;
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->attributes['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);
}

Yassine Doghri
committed
public function getCover(): Image
if (! $this->cover instanceof Image) {
$this->cover = (new MediaModel('image'))->getMediaById($this->cover_id);
return $this->cover;
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
public function setAudio(?UploadedFile $file): self
{
if ($file === null || ! $file->isValid()) {
return $this;
}
if ($this->audio_id !== null) {
$this->getAudio()
->setFile($file);
$this->getAudio()
->updated_by = (int) user_id();
(new MediaModel('audio'))->updateMedia($this->getAudio());
} else {
$transcript = new Audio([
'file_name' => $this->attributes['slug'],
'file_directory' => 'podcasts/' . $this->attributes['handle'],
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
$transcript->setFile($file);
$this->attributes['transcript_id'] = (new MediaModel())->saveMedia($transcript);
}
return $this;
}
public function getAudio(): Audio
if (! $this->audio instanceof Audio) {
$this->audio = (new MediaModel('audio'))->getMediaById($this->audio_id);
}
return $this->audio;
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
public function setTranscript(?UploadedFile $file): self
{
if ($file === null || ! $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->attributes['handle'],
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
$transcript->setFile($file);
$this->attributes['transcript_id'] = (new MediaModel())->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);
return $this->transcript;
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
public function setChapters(?UploadedFile $file): self
{
if ($file === null || ! $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->attributes['handle'],
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
$chapters->setFile($file);
$this->attributes['chapters_id'] = (new MediaModel())->saveMedia($chapters);
}
return $this;
}
public function getChapters(): ?Chapters
if ($this->chapters_id !== null && $this->chapters === null) {
$this->chapters = (new MediaModel('document'))->getMediaById($this->chapters_id);
}
return $this->chapters;
public function getAudioFileUrl(): string
{
helper('media');
return media_base_url($this->audio->file_path);
public function getAudioFileAnalyticsUrl(): string

Benjamin Bellamy
committed
helper('analytics');
// remove 'podcasts/' from audio file path
$strippedAudioFilePath = substr($this->audio->file_path, 9);

Yassine Doghri
committed
return generate_episode_analytics_url(
$this->podcast_id,
$this->id,
$strippedAudioFilePath,
$this->audio->duration,
$this->audio->file_size,
$this->audio->header_size,

Yassine Doghri
committed
$this->published_at,
public function getAudioFileWebUrl(): string
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_remote_url which can be null.
public function getTranscriptUrl(): ?string
if ($this->transcript !== null) {
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.

Yassine Doghri
committed
public function getChaptersFileUrl(): ?string
if ($this->chapters !== null) {
return $this->chapters->file_url;

Yassine Doghri
committed
return $this->chapters_remote_url;
/**
* Returns the episode's persons
*
* @return Person[]
public function getPersons(): array
if ($this->id === null) {

Yassine Doghri
committed
throw new RuntimeException('Episode must be created before getting persons.');
if ($this->persons === null) {

Yassine Doghri
committed
$this->persons = (new PersonModel())->getEpisodePersons($this->podcast_id, $this->id);
return $this->persons;
* Returns the episode’s clips
* @return Clip[]
public function getClips(): array
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting clips.');
if ($this->clips === null) {
$this->clips = (new ClipsModel())->getEpisodeClips($this->getPodcast() ->id, $this->id);
return $this->clips;
* @return Post[]
public function getPosts(): array
{
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting posts.');
}
if ($this->posts === null) {
$this->posts = (new PostModel())->getEpisodePosts($this->id);
}
return $this->posts;
}

Yassine Doghri
committed
/**
* @return EpisodeComment[]

Yassine Doghri
committed
*/
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);

Yassine Doghri
committed
}
return $this->comments;
}
public function getLink(): string
return url_to('episode', $this->getPodcast()->handle, $this->attributes['slug']);
public function getEmbedUrl(string $theme = null): string
{
return base_url(
$theme
? 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;
return $this;
public function getPodcast(): ?Podcast

Yassine Doghri
committed
return (new PodcastModel())->getPodcastById($this->podcast_id);
public function setDescriptionMarkdown(string $descriptionMarkdown): static

Yassine Doghri
committed
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
$this->attributes['description_markdown'] = $descriptionMarkdown;

Yassine Doghri
committed
$this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown);
return $this;
}
public function getDescriptionHtml(?string $serviceSlug = null): string
$descriptionHtml = '';
if (
$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>";
}
return $descriptionHtml;
public function getDescription(): string
if ($this->description === null) {
$this->description = trim(
preg_replace('~\s+~', ' ', strip_tags($this->attributes['description_html'])),
return $this->description;
public function getPublicationStatus(): string

Yassine Doghri
committed
{

Yassine Doghri
committed
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';
}

Yassine Doghri
committed
}

Yassine Doghri
committed

Yassine Doghri
committed
return $this->publication_status;

Yassine Doghri
committed
}
/**
* Saves the location name and fetches OpenStreetMap info
*/

Yassine Doghri
committed
public function setLocation(?Location $location = null): static

Yassine Doghri
committed
if ($location === null) {
$this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null;

Yassine Doghri
committed
$this->attributes['location_osm'] = null;

Yassine Doghri
committed
return $this;
}
! isset($this->attributes['location_name']) ||

Yassine Doghri
committed
$this->attributes['location_name'] !== $location->name

Yassine Doghri
committed
$location->fetchOsmLocation();

Yassine Doghri
committed
$this->attributes['location_name'] = $location->name;
$this->attributes['location_geo'] = $location->geo;
$this->attributes['location_osm'] = $location->osm;

Benjamin Bellamy
committed
public function getLocation(): ?Location
{
if ($this->location_name === null) {
return null;
}
if ($this->location === null) {

Yassine Doghri
committed
$this->location = new Location($this->location_name, $this->location_geo, $this->location_osm);
}
return $this->location;
}

Benjamin Bellamy
committed
/**
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string

Benjamin Bellamy
committed
{
if ($this->custom_rss === null) {

Benjamin Bellamy
committed
return '';
}

Yassine Doghri
committed
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,

Yassine Doghri
committed
], $xmlNode);

Yassine Doghri
committed
return str_replace(['<item>', '</item>'], '', $xmlNode->asXML());

Benjamin Bellamy
committed
}
/**
* Saves custom rss tag into json
*/
public function setCustomRssString(?string $customRssString = null): static

Benjamin Bellamy
committed
{
if ($customRssString === '') {
$this->attributes['custom_rss'] = null;

Yassine Doghri
committed
return $this;
}

Benjamin Bellamy
committed
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 .
'</item></channel></rss>',
),

Benjamin Bellamy
committed
)['elements'][0]['elements'][0];

Yassine Doghri
committed

Benjamin Bellamy
committed
if (array_key_exists('elements', $customRssArray)) {

Yassine Doghri
committed
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);

Benjamin Bellamy
committed
} else {
$this->attributes['custom_rss'] = null;
}

Yassine Doghri
committed
return $this;

Benjamin Bellamy
committed
}
public function getPartnerLink(?string $serviceSlug = null): string
$partnerLink =
rtrim($this->getPodcast()->partner_link_url, '/') .
$this->getPodcast()
->partner_id .
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, '/') .

Yassine Doghri
committed
'?pid=' .
$this->getPodcast()
->partner_id .

Yassine Doghri
committed
'&guid=' .
urlencode($this->attributes['guid']) .
($serviceSlug !== null ? '&_from=' . $serviceSlug : '');