Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • adaures/castopod
  • mkljczk/castopod-host
  • spaetz/castopod-host
  • PatrykMis/castopod
  • jonas/castopod
  • ajeremias/castopod
  • misuzu/castopod
  • KrzysztofDomanczyk/castopod
  • Behel/castopod
  • nebulon/castopod
  • ewen/castopod
  • NeoluxConsulting/castopod
  • nateritter/castopod-og
  • prcutler/castopod
14 results
Show changes
Showing
with 870 additions and 1079 deletions
...@@ -3,19 +3,20 @@ ...@@ -3,19 +3,20 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Entities\Clip; namespace App\Entities\Clip;
use App\Entities\Media\Video;
use App\Models\MediaModel;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use Modules\Media\Entities\Video;
use Modules\Media\Models\MediaModel;
use Override;
/** /**
* @property array $theme * @property array{name:string,preview:string} $theme
* @property string $format * @property string $format
*/ */
class VideoClip extends BaseClip class VideoClip extends BaseClip
...@@ -25,7 +26,7 @@ class VideoClip extends BaseClip ...@@ -25,7 +26,7 @@ class VideoClip extends BaseClip
/** /**
* @param array<string, mixed>|null $data * @param array<string, mixed>|null $data
*/ */
public function __construct(array $data = null) public function __construct(?array $data = null)
{ {
parent::__construct($data); parent::__construct($data);
...@@ -36,7 +37,7 @@ class VideoClip extends BaseClip ...@@ -36,7 +37,7 @@ class VideoClip extends BaseClip
} }
/** /**
* @param array<string, string> $theme * @param array{name:string,preview:string} $theme
*/ */
public function setTheme(array $theme): self public function setTheme(array $theme): self
{ {
...@@ -53,7 +54,7 @@ class VideoClip extends BaseClip ...@@ -53,7 +54,7 @@ class VideoClip extends BaseClip
public function setFormat(string $format): self public function setFormat(string $format): self
{ {
$this->attributes['metadata'] = json_decode($this->attributes['metadata'], true); $this->attributes['metadata'] = json_decode((string) $this->attributes['metadata'], true);
$this->attributes['format'] = $format; $this->attributes['format'] = $format;
$this->attributes['metadata']['format'] = $format; $this->attributes['metadata']['format'] = $format;
...@@ -63,25 +64,24 @@ class VideoClip extends BaseClip ...@@ -63,25 +64,24 @@ class VideoClip extends BaseClip
return $this; return $this;
} }
public function setMedia(string $filePath = null): static #[Override]
public function setMedia(File $file, string $fileKey): static
{ {
if ($filePath === null) { if ($this->attributes['media_id'] !== null) {
// media is already set, do nothing
return $this; return $this;
} }
helper('media');
$file = new File(media_path($filePath));
$video = new Video([ $video = new Video([
'file_path' => $filePath, 'file_key' => $fileKey,
'language_code' => $this->getPodcast() 'language_code' => $this->getPodcast()
->language_code, ->language_code,
'uploaded_by' => $this->attributes['created_by'], 'uploaded_by' => $this->attributes['created_by'],
'updated_by' => $this->attributes['created_by'], 'updated_by' => $this->attributes['created_by'],
]); ]);
$video->setFile($file); $video->setFile($file);
$this->attributes['media_id'] = (new MediaModel())->saveMedia($video); $this->attributes['media_id'] = (new MediaModel('video'))->saveMedia($video);
return $this; return $this;
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -45,12 +45,12 @@ class Credit extends Entity ...@@ -45,12 +45,12 @@ class Credit extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'podcast_id' => 'integer', 'podcast_id' => 'integer',
'episode_id' => '?integer', 'episode_id' => '?integer',
'person_id' => 'integer', 'person_id' => 'integer',
'full_name' => 'string', 'full_name' => 'string',
'person_group' => 'string', 'person_group' => 'string',
'person_role' => 'string', 'person_role' => 'string',
]; ];
public function getPerson(): ?Person public function getPerson(): ?Person
...@@ -98,6 +98,7 @@ class Credit extends Entity ...@@ -98,6 +98,7 @@ class Credit extends Entity
return ''; return '';
} }
/** @var string */
return lang("PersonsTaxonomy.persons.{$this->person_group}.label"); return lang("PersonsTaxonomy.persons.{$this->person_group}.label");
} }
...@@ -111,6 +112,7 @@ class Credit extends Entity ...@@ -111,6 +112,7 @@ class Credit extends Entity
return ''; return '';
} }
/** @var string */
return lang("PersonsTaxonomy.persons.{$this->person_group}.roles.{$this->person_role}.label"); return lang("PersonsTaxonomy.persons.{$this->person_group}.roles.{$this->person_role}.label");
} }
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -11,14 +11,9 @@ declare(strict_types=1); ...@@ -11,14 +11,9 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
use App\Entities\Clip\Soundbite; use App\Entities\Clip\Soundbite;
use App\Entities\Media\Audio;
use App\Entities\Media\Chapters;
use App\Entities\Media\Image;
use App\Entities\Media\Transcript;
use App\Libraries\SimpleRSSElement;
use App\Models\ClipModel; use App\Models\ClipModel;
use App\Models\EpisodeCommentModel; use App\Models\EpisodeCommentModel;
use App\Models\MediaModel; use App\Models\EpisodeModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel; use App\Models\PostModel;
...@@ -26,27 +21,41 @@ use CodeIgniter\Entity\Entity; ...@@ -26,27 +21,41 @@ use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter; use Exception;
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;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Image;
use Modules\Media\Entities\Transcript;
use Modules\Media\Models\MediaModel;
use Override;
use RuntimeException; use RuntimeException;
/** /**
* @property int $id * @property int $id
* @property int $podcast_id * @property int $podcast_id
* @property Podcast $podcast * @property Podcast $podcast
* @property ?string $preview_id
* @property string $preview_link
* @property string $link * @property string $link
* @property string $guid * @property string $guid
* @property string $slug * @property string $slug
* @property string $title * @property string $title
* @property int $audio_id * @property int $audio_id
* @property Audio $audio * @property ?Audio $audio
* @property string $audio_file_analytics_url * @property string $audio_url
* @property string $audio_file_web_url * @property string $audio_web_url
* @property string $audio_file_opengraph_url * @property string $audio_opengraph_url
* @property string|null $description Holds text only description, striped of any markdown or html special characters * @property string|null $description Holds text only description, striped of any markdown or html special characters
* @property string $description_markdown * @property string $description_markdown
* @property string $description_html * @property string $description_html
* @property int $cover_id * @property ?int $cover_id
* @property Image $cover * @property ?Image $cover
* @property int|null $transcript_id * @property int|null $transcript_id
* @property Transcript|null $transcript * @property Transcript|null $transcript
* @property string|null $transcript_remote_url * @property string|null $transcript_remote_url
...@@ -62,39 +71,38 @@ use RuntimeException; ...@@ -62,39 +71,38 @@ use RuntimeException;
* @property string|null $location_name * @property string|null $location_name
* @property string|null $location_geo * @property string|null $location_geo
* @property string|null $location_osm * @property string|null $location_osm
* @property array|null $custom_rss * @property bool $is_published_on_hubs
* @property string $custom_rss_string * @property int $downloads_count
* @property int $posts_count * @property int $posts_count
* @property int $comments_count * @property int $comments_count
* @property EpisodeComment[]|null $comments
* @property bool $is_premium
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property string $publication_status; * @property string $publication_status
* @property Time|null $published_at; * @property Time|null $published_at
* @property Time $created_at; * @property Time $created_at
* @property Time $updated_at; * @property Time $updated_at
* @property Time|null $deleted_at;
* *
* @property Person[] $persons; * @property Person[] $persons
* @property Soundbite[] $soundbites; * @property Soundbite[] $soundbites
* @property string $embed_url; * @property string $embed_url
*/ */
class Episode extends Entity class Episode extends Entity
{ {
protected Podcast $podcast; public string $link = '';
protected string $link;
protected ?Audio $audio = null; public string $audio_url = '';
protected string $audio_url; public string $audio_web_url = '';
protected string $audio_analytics_url; public string $audio_opengraph_url = '';
protected string $audio_web_url; protected Podcast $podcast;
protected string $audio_opengraph_url; protected ?Audio $audio = null;
protected string $embed_url; protected string $embed_url = '';
protected ?Image $cover = null; protected ?Image $cover = null;
...@@ -126,50 +134,77 @@ class Episode extends Entity ...@@ -126,50 +134,77 @@ class Episode extends Entity
protected ?Location $location = null; protected ?Location $location = null;
protected string $custom_rss_string;
protected ?string $publication_status = null; protected ?string $publication_status = null;
/** /**
* @var string[] * @var array<int, string>
* @phpstan-var list<string>
*/ */
protected $dates = ['published_at', 'created_at', 'updated_at', 'deleted_at']; protected $dates = ['published_at', 'created_at', 'updated_at'];
/** /**
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'podcast_id' => 'integer', 'podcast_id' => 'integer',
'guid' => 'string', 'preview_id' => '?string',
'slug' => 'string', 'guid' => 'string',
'title' => 'string', 'slug' => 'string',
'audio_id' => 'integer', 'title' => 'string',
'description_markdown' => 'string', 'audio_id' => 'integer',
'description_html' => 'string', 'description_markdown' => 'string',
'cover_id' => '?integer', 'description_html' => 'string',
'transcript_id' => '?integer', 'cover_id' => '?integer',
'transcript_id' => '?integer',
'transcript_remote_url' => '?string', 'transcript_remote_url' => '?string',
'chapters_id' => '?integer', 'chapters_id' => '?integer',
'chapters_remote_url' => '?string', 'chapters_remote_url' => '?string',
'parental_advisory' => '?string', 'parental_advisory' => '?string',
'number' => '?integer', 'number' => '?integer',
'season_number' => '?integer', 'season_number' => '?integer',
'type' => 'string', 'type' => 'string',
'is_blocked' => 'boolean', 'is_blocked' => 'boolean',
'location_name' => '?string', 'location_name' => '?string',
'location_geo' => '?string', 'location_geo' => '?string',
'location_osm' => '?string', 'location_osm' => '?string',
'custom_rss' => '?json-array', 'is_published_on_hubs' => 'boolean',
'posts_count' => 'integer', 'downloads_count' => 'integer',
'comments_count' => 'integer', 'posts_count' => 'integer',
'created_by' => 'integer', 'comments_count' => 'integer',
'updated_by' => 'integer', 'is_premium' => 'boolean',
'created_by' => 'integer',
'updated_by' => 'integer',
]; ];
public function setCover(UploadedFile | File $file = null): self /**
* @param array<string, mixed> $data
*/
#[Override]
public function injectRawData(array $data): static
{
parent::injectRawData($data);
$this->link = url_to('episode', esc($this->getPodcast()->handle, 'url'), esc($this->attributes['slug'], 'url'));
$this->audio_url = url_to(
'episode-audio',
$this->getPodcast()
->handle,
$this->slug,
$this->getAudio()
->file_extension,
);
$this->audio_opengraph_url = $this->audio_url . '?_from=-+Open+Graph+-';
$this->audio_web_url = $this->audio_url . '?_from=-+Website+-';
return $this;
}
public function setCover(UploadedFile | File|null $file = null): self
{ {
if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
} }
...@@ -177,16 +212,15 @@ class Episode extends Entity ...@@ -177,16 +212,15 @@ class Episode extends Entity
$this->getCover() $this->getCover()
->setFile($file); ->setFile($file);
$this->getCover() $this->getCover()
->updated_by = (int) user_id(); ->updated_by = $this->attributes['updated_by'];
(new MediaModel('image'))->updateMedia($this->getCover()); (new MediaModel('image'))->updateMedia($this->getCover());
} else { } else {
$cover = new Image([ $cover = new Image([
'file_name' => $this->attributes['slug'], 'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
'file_directory' => 'podcasts/' . $this->getPodcast()->handle, 'sizes' => config('Images')
'sizes' => config('Images')
->podcastCoverSizes, ->podcastCoverSizes,
'uploaded_by' => user_id(), 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => user_id(), 'updated_by' => $this->attributes['updated_by'],
]); ]);
$cover->setFile($file); $cover->setFile($file);
...@@ -214,9 +248,9 @@ class Episode extends Entity ...@@ -214,9 +248,9 @@ class Episode extends Entity
return $this->cover; return $this->cover;
} }
public function setAudio(UploadedFile | File $file = null): self public function setAudio(UploadedFile | File|null $file = null): self
{ {
if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
} }
...@@ -224,16 +258,15 @@ class Episode extends Entity ...@@ -224,16 +258,15 @@ class Episode extends Entity
$this->getAudio() $this->getAudio()
->setFile($file); ->setFile($file);
$this->getAudio() $this->getAudio()
->updated_by = (int) user_id(); ->updated_by = $this->attributes['updated_by'];
(new MediaModel('audio'))->updateMedia($this->getAudio()); (new MediaModel('audio'))->updateMedia($this->getAudio());
} else { } else {
$audio = new Audio([ $audio = new Audio([
'file_name' => $this->attributes['slug'], 'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $file->getRandomName(),
'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
'language_code' => $this->getPodcast() 'language_code' => $this->getPodcast()
->language_code, ->language_code,
'uploaded_by' => user_id(), 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => user_id(), 'updated_by' => $this->attributes['updated_by'],
]); ]);
$audio->setFile($file); $audio->setFile($file);
...@@ -252,26 +285,25 @@ class Episode extends Entity ...@@ -252,26 +285,25 @@ class Episode extends Entity
return $this->audio; return $this->audio;
} }
public function setTranscript(UploadedFile | File $file = null): self public function setTranscript(UploadedFile | File|null $file = null): self
{ {
if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
} }
if ($this->getTranscript() !== null) { if ($this->getTranscript() instanceof Transcript) {
$this->getTranscript() $this->getTranscript()
->setFile($file); ->setFile($file);
$this->getTranscript() $this->getTranscript()
->updated_by = (int) user_id(); ->updated_by = $this->attributes['updated_by'];
(new MediaModel('transcript'))->updateMedia($this->getTranscript()); (new MediaModel('transcript'))->updateMedia($this->getTranscript());
} else { } else {
$transcript = new Transcript([ $transcript = new Transcript([
'file_name' => $this->attributes['slug'] . '-transcript', 'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
'language_code' => $this->getPodcast() 'language_code' => $this->getPodcast()
->language_code, ->language_code,
'uploaded_by' => user_id(), 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => user_id(), 'updated_by' => $this->attributes['updated_by'],
]); ]);
$transcript->setFile($file); $transcript->setFile($file);
...@@ -283,33 +315,32 @@ class Episode extends Entity ...@@ -283,33 +315,32 @@ class Episode extends Entity
public function getTranscript(): ?Transcript public function getTranscript(): ?Transcript
{ {
if ($this->transcript_id !== null && $this->transcript === null) { if ($this->transcript_id !== null && ! $this->transcript instanceof Transcript) {
$this->transcript = (new MediaModel('transcript'))->getMediaById($this->transcript_id); $this->transcript = (new MediaModel('transcript'))->getMediaById($this->transcript_id);
} }
return $this->transcript; return $this->transcript;
} }
public function setChapters(UploadedFile | File $file = null): self public function setChapters(UploadedFile | File|null $file = null): self
{ {
if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
} }
if ($this->getChapters() !== null) { if ($this->getChapters() instanceof Chapters) {
$this->getChapters() $this->getChapters()
->setFile($file); ->setFile($file);
$this->getChapters() $this->getChapters()
->updated_by = (int) user_id(); ->updated_by = $this->attributes['updated_by'];
(new MediaModel('chapters'))->updateMedia($this->getChapters()); (new MediaModel('chapters'))->updateMedia($this->getChapters());
} else { } else {
$chapters = new Chapters([ $chapters = new Chapters([
'file_name' => $this->attributes['slug'] . '-chapters', 'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
'language_code' => $this->getPodcast() 'language_code' => $this->getPodcast()
->language_code, ->language_code,
'uploaded_by' => user_id(), 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => user_id(), 'updated_by' => $this->attributes['updated_by'],
]); ]);
$chapters->setFile($file); $chapters->setFile($file);
...@@ -321,56 +352,22 @@ class Episode extends Entity ...@@ -321,56 +352,22 @@ class Episode extends Entity
public function getChapters(): ?Chapters public function getChapters(): ?Chapters
{ {
if ($this->chapters_id !== null && $this->chapters === null) { if ($this->chapters_id !== null && ! $this->chapters instanceof Chapters) {
$this->chapters = (new MediaModel('chapters'))->getMediaById($this->chapters_id); $this->chapters = (new MediaModel('chapters'))->getMediaById($this->chapters_id);
} }
return $this->chapters; return $this->chapters;
} }
public function getAudioFileUrl(): string
{
helper('media');
return media_base_url($this->audio->file_path);
}
public function getAudioFileAnalyticsUrl(): string
{
helper('analytics');
// remove 'podcasts/' from audio file path
$strippedAudioFilePath = substr($this->getAudio()->file_path, 9);
return generate_episode_analytics_url(
$this->podcast_id,
$this->id,
$strippedAudioFilePath,
$this->audio->duration,
$this->audio->file_size,
$this->audio->header_size,
$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. * Gets transcript url from transcript file uri if it exists or returns the transcript_remote_url which can be null.
*/ */
public function getTranscriptUrl(): ?string public function getTranscriptUrl(): ?string
{ {
if ($this->transcript !== null) { if ($this->transcript instanceof Transcript) {
return $this->transcript->file_url; return $this->transcript->file_url;
} }
return $this->transcript_remote_url; return $this->transcript_remote_url;
} }
...@@ -379,7 +376,7 @@ class Episode extends Entity ...@@ -379,7 +376,7 @@ class Episode extends Entity
*/ */
public function getChaptersFileUrl(): ?string public function getChaptersFileUrl(): ?string
{ {
if ($this->chapters !== null) { if ($this->chapters instanceof Chapters) {
return $this->chapters->file_url; return $this->chapters->file_url;
} }
...@@ -454,23 +451,16 @@ class Episode extends Entity ...@@ -454,23 +451,16 @@ class Episode extends Entity
return $this->comments; return $this->comments;
} }
public function getLink(): string public function getEmbedUrl(?string $theme = null): string
{ {
return url_to('episode', $this->getPodcast()->handle, $this->attributes['slug']); return $theme
} ? url_to('embed-theme', esc($this->getPodcast()->handle), esc($this->attributes['slug']), $theme)
: url_to('embed', esc($this->getPodcast()->handle), esc($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 public function setGuid(?string $guid = null): static
{ {
$this->attributes['guid'] = $guid === null ? $this->getLink() : $guid; $this->attributes['guid'] = $guid ?? $this->link;
return $this; return $this;
} }
...@@ -482,50 +472,30 @@ class Episode extends Entity ...@@ -482,50 +472,30 @@ class Episode extends Entity
public function setDescriptionMarkdown(string $descriptionMarkdown): static public function setDescriptionMarkdown(string $descriptionMarkdown): static
{ {
$converter = new CommonMarkConverter([ $config = [
'html_input' => 'strip', 'html_input' => 'escape',
'allow_unsafe_links' => false, 'allow_unsafe_links' => false,
]); ];
$this->attributes['description_markdown'] = $descriptionMarkdown; $environment = new Environment($config);
$this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown); $environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
return $this; $converter = new MarkdownConverter($environment);
}
public function getDescriptionHtml(?string $serviceSlug = null): string $this->attributes['description_markdown'] = $descriptionMarkdown;
{ $this->attributes['description_html'] = $converter->convert($descriptionMarkdown);
$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; return $this;
} }
public function getDescription(): string public function getDescription(): string
{ {
if ($this->description === null) { if ($this->description === null) {
$this->description = trim( $this->description = trim(
preg_replace('~\s+~', ' ', strip_tags($this->attributes['description_html'])), (string) preg_replace('~\s+~', ' ', strip_tags((string) $this->attributes['description_html'])),
); );
} }
...@@ -535,8 +505,10 @@ class Episode extends Entity ...@@ -535,8 +505,10 @@ class Episode extends Entity
public function getPublicationStatus(): string public function getPublicationStatus(): string
{ {
if ($this->publication_status === null) { if ($this->publication_status === null) {
if ($this->published_at === null) { if (! $this->published_at instanceof Time) {
$this->publication_status = 'not_published'; $this->publication_status = 'not_published';
} elseif ($this->getPodcast()->publication_status !== 'published') {
$this->publication_status = 'with_podcast';
} elseif ($this->published_at->isBefore(Time::now())) { } elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published'; $this->publication_status = 'published';
} else { } else {
...@@ -552,7 +524,7 @@ class Episode extends Entity ...@@ -552,7 +524,7 @@ class Episode extends Entity
*/ */
public function setLocation(?Location $location = null): static public function setLocation(?Location $location = null): static
{ {
if ($location === null) { if (! $location instanceof Location) {
$this->attributes['location_name'] = null; $this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null; $this->attributes['location_geo'] = null;
$this->attributes['location_osm'] = null; $this->attributes['location_osm'] = null;
...@@ -580,89 +552,36 @@ class Episode extends Entity ...@@ -580,89 +552,36 @@ class Episode extends Entity
return null; return null;
} }
if ($this->location === null) { if (! $this->location instanceof Location) {
$this->location = new Location($this->location_name, $this->location_geo, $this->location_osm); $this->location = new Location($this->location_name, $this->location_geo, $this->location_osm);
} }
return $this->location; return $this->location;
} }
/** public function getPreviewLink(): string
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string
{ {
if ($this->custom_rss === null) { if ($this->preview_id === null) {
return ''; // generate preview id
} if (! $previewUUID = (new EpisodeModel())->setEpisodePreviewId($this->id)) {
throw new Exception('Could not set episode preview id');
helper('rss'); }
$xmlNode = (new SimpleRSSElement( $this->preview_id = $previewUUID;
'<?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,
], $xmlNode);
return str_replace(['<item>', '</item>'], '', $xmlNode->asXML()); return url_to('episode-preview', (string) $this->preview_id);
} }
/** /**
* Saves custom rss tag into json * Returns the episode's clip count
*/ */
public function setCustomRssString(?string $customRssString = null): static public function getClipCount(): int|string
{ {
if ($customRssString === '') { if ($this->id === null) {
$this->attributes['custom_rss'] = null; throw new RuntimeException('Episode must be created before getting number of video clips.');
return $this;
}
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>',
),
)['elements'][0]['elements'][0];
if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
} else {
$this->attributes['custom_rss'] = null;
}
return $this;
}
public function getPartnerLink(?string $serviceSlug = null): string
{
$partnerLink =
rtrim($this->getPodcast()->partner_link_url, '/') .
'?pid=' .
$this->getPodcast()
->partner_id .
'&guid=' .
urlencode($this->attributes['guid']);
if ($serviceSlug !== null) {
$partnerLink .= '&_from=' . $serviceSlug;
} }
return $partnerLink; return (new ClipModel())->getClipCount($this->podcast_id, $this->id);
}
public function getPartnerImageUrl(string $serviceSlug = null): string
{
return rtrim($this->getPodcast()->partner_image_url, '/') .
'?pid=' .
$this->getPodcast()
->partner_id .
'&guid=' .
urlencode($this->attributes['guid']) .
($serviceSlug !== null ? '&_from=' . $serviceSlug : '');
} }
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -51,7 +51,8 @@ class EpisodeComment extends UuidEntity ...@@ -51,7 +51,8 @@ class EpisodeComment extends UuidEntity
protected bool $has_replies = false; protected bool $has_replies = false;
/** /**
* @var string[] * @var array<int, string>
* @phpstan-var list<string>
*/ */
protected $dates = ['created_at']; protected $dates = ['created_at'];
...@@ -59,17 +60,17 @@ class EpisodeComment extends UuidEntity ...@@ -59,17 +60,17 @@ class EpisodeComment extends UuidEntity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'string', 'id' => 'string',
'uri' => 'string', 'uri' => 'string',
'episode_id' => 'integer', 'episode_id' => 'integer',
'actor_id' => 'integer', 'actor_id' => 'integer',
'in_reply_to_id' => '?string', 'in_reply_to_id' => '?string',
'message' => 'string', 'message' => 'string',
'message_html' => 'string', 'message_html' => 'string',
'likes_count' => 'integer', 'likes_count' => 'integer',
'replies_count' => 'integer', 'replies_count' => 'integer',
'created_by' => 'integer', 'created_by' => 'integer',
'is_from_post' => 'boolean', 'is_from_post' => 'boolean',
]; ];
public function getEpisode(): ?Episode public function getEpisode(): ?Episode
...@@ -87,8 +88,6 @@ class EpisodeComment extends UuidEntity ...@@ -87,8 +88,6 @@ class EpisodeComment extends UuidEntity
/** /**
* Returns the comment's actor * Returns the comment's actor
*
* @noRector ReturnTypeDeclarationRector
*/ */
public function getActor(): ?Actor public function getActor(): ?Actor
{ {
...@@ -96,13 +95,11 @@ class EpisodeComment extends UuidEntity ...@@ -96,13 +95,11 @@ class EpisodeComment extends UuidEntity
throw new RuntimeException('Comment must have an actor_id before getting actor.'); throw new RuntimeException('Comment must have an actor_id before getting actor.');
} }
if ($this->actor === null) { if (! $this->actor instanceof Actor) {
// @phpstan-ignore-next-line
$this->actor = model(ActorModel::class, false) $this->actor = model(ActorModel::class, false)
->getActorById($this->actor_id); ->getActorById($this->actor_id);
} }
// @phpstan-ignore-next-line
return $this->actor; return $this->actor;
} }
...@@ -127,16 +124,13 @@ class EpisodeComment extends UuidEntity ...@@ -127,16 +124,13 @@ class EpisodeComment extends UuidEntity
return $this->getReplies() !== []; return $this->getReplies() !== [];
} }
/**
* @noRector ReturnTypeDeclarationRector
*/
public function getReplyToComment(): ?self public function getReplyToComment(): ?self
{ {
if ($this->in_reply_to_id === null) { if ($this->in_reply_to_id === null) {
throw new RuntimeException('Comment is not a reply.'); throw new RuntimeException('Comment is not a reply.');
} }
if ($this->reply_to_comment === null) { if (! $this->reply_to_comment instanceof self) {
$this->reply_to_comment = model(EpisodeCommentModel::class, false) $this->reply_to_comment = model(EpisodeCommentModel::class, false)
->getCommentById($this->in_reply_to_id); ->getCommentById($this->in_reply_to_id);
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -22,7 +22,7 @@ class Language extends Entity ...@@ -22,7 +22,7 @@ class Language extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'code' => 'string', 'code' => 'string',
'native_name' => 'string', 'native_name' => 'string',
]; ];
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2021 Podlibre * @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -27,7 +27,7 @@ class Like extends UuidEntity ...@@ -27,7 +27,7 @@ class Like extends UuidEntity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'actor_id' => 'integer', 'actor_id' => 'integer',
'comment_id' => 'string', 'comment_id' => 'string',
]; ];
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2021 Podlibre * @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -11,7 +11,6 @@ declare(strict_types=1); ...@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use Config\Services;
/** /**
* @property string $url * @property string $url
...@@ -23,15 +22,9 @@ use Config\Services; ...@@ -23,15 +22,9 @@ use Config\Services;
*/ */
class Location extends Entity class Location extends Entity
{ {
/** private const string OSM_URL = 'https://www.openstreetmap.org/';
* @var string
*/
private const OSM_URL = 'https://www.openstreetmap.org/';
/** private const string NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
* @var string
*/
private const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
public function __construct( public function __construct(
protected string $name, protected string $name,
...@@ -42,14 +35,18 @@ class Location extends Entity ...@@ -42,14 +35,18 @@ class Location extends Entity
$longitude = null; $longitude = null;
if ($geo !== null) { if ($geo !== null) {
$geoArray = explode(',', substr($geo, 4)); $geoArray = explode(',', substr($geo, 4));
$latitude = floatval($geoArray[0]);
$longitude = floatval($geoArray[1]); if (count($geoArray) === 2) {
$latitude = (float) $geoArray[0];
$longitude = (float) $geoArray[1];
}
} }
parent::__construct([ parent::__construct([
'name' => $name, 'name' => $name,
'geo' => $geo, 'geo' => $geo,
'osm' => $osm, 'osm' => $osm,
'latitude' => $latitude, 'latitude' => $latitude,
'longitude' => $longitude, 'longitude' => $longitude,
]); ]);
} }
...@@ -81,7 +78,7 @@ class Location extends Entity ...@@ -81,7 +78,7 @@ class Location extends Entity
*/ */
public function fetchOsmLocation(): static public function fetchOsmLocation(): static
{ {
$client = Services::curlrequest(); $client = service('curlrequest');
$response = $client->request( $response = $client->request(
'GET', 'GET',
...@@ -92,12 +89,12 @@ class Location extends Entity ...@@ -92,12 +89,12 @@ class Location extends Entity
[ [
'headers' => [ 'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION, 'User-Agent' => 'Castopod/' . CP_VERSION,
'Accept' => 'application/json', 'Accept' => 'application/json',
], ],
], ],
); );
$places = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR); $places = json_decode((string) $response->getBody(), false, 512, JSON_THROW_ON_ERROR);
if ($places === []) { if ($places === []) {
return $this; return $this;
...@@ -105,16 +102,16 @@ class Location extends Entity ...@@ -105,16 +102,16 @@ class Location extends Entity
if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists( if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists(
$places[0], $places[0],
'lon' 'lon',
) && $places[0]->lon !== null)) { ) && $places[0]->lon !== null)) {
$this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}"; $this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}";
} }
if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists( if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists(
$places[0], $places[0],
'osm_id' 'osm_id',
) && $places[0]->osm_id !== null)) { ) && $places[0]->osm_id !== null)) {
$this->attributes['osm'] = strtoupper(substr($places[0]->osm_type, 0, 1)) . $places[0]->osm_id; $this->attributes['osm'] = strtoupper(substr((string) $places[0]->osm_type, 0, 1)) . $places[0]->osm_id;
} }
return $this; return $this;
......
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Media;
use CodeIgniter\Files\File;
/**
* @property array $sizes
*/
class Image extends BaseMedia
{
protected string $type = 'image';
public function initFileProperties(): void
{
parent::initFileProperties();
if ($this->file_path && $this->file_metadata) {
$this->sizes = $this->file_metadata['sizes'];
$this->initSizeProperties();
}
}
public function initSizeProperties(): bool
{
helper('media');
$extension = $this->file_extension;
$mimetype = $this->file_mimetype;
foreach ($this->sizes as $name => $size) {
if (array_key_exists('extension', $size)) {
$extension = $size['extension'];
}
if (array_key_exists('mimetype', $size)) {
$mimetype = $size['mimetype'];
}
$this->{$name . '_path'} = $this->file_directory . '/' . $this->file_name . '_' . $name . '.' . $extension;
$this->{$name . '_url'} = media_base_url($this->{$name . '_path'});
$this->{$name . '_mimetype'} = $mimetype;
}
return true;
}
public function setFile(File $file): self
{
parent::setFile($file);
if ($this->file_mimetype === 'image/jpeg' && $metadata = exif_read_data(
media_path($this->file_path),
null,
true
)) {
$metadata['sizes'] = $this->sizes;
$this->attributes['file_size'] = $metadata['FILE']['FileSize'];
} else {
$metadata = [
'sizes' => $this->sizes,
];
}
$this->attributes['file_metadata'] = json_encode($metadata);
$this->initFileProperties();
$this->saveSizes();
return $this;
}
public function deleteFile(): void
{
parent::deleteFile();
$this->deleteSizes();
}
public function saveSizes(): void
{
// save derived sizes
$imageService = service('image');
foreach ($this->sizes as $name => $size) {
$pathProperty = $name . '_path';
$imageService
->withFile(media_path($this->file_path))
->resize($size['width'], $size['height']);
$imageService->save(media_path($this->{$pathProperty}));
}
}
private function deleteSizes(): void
{
// delete all derived sizes
foreach (array_keys($this->sizes) as $name) {
$pathProperty = $name . '_path';
unlink(media_path($this->{$pathProperty}));
}
}
}
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Media;
use App\Libraries\TranscriptParser;
use CodeIgniter\Files\File;
class Transcript extends BaseMedia
{
public ?string $json_path = null;
public ?string $json_url = null;
protected string $type = 'transcript';
public function initFileProperties(): void
{
parent::initFileProperties();
if ($this->file_path && $this->file_metadata && array_key_exists('json_path', $this->file_metadata)) {
helper('media');
$this->json_path = media_path($this->file_metadata['json_path']);
$this->json_url = media_base_url($this->file_metadata['json_path']);
}
}
public function setFile(File $file): self
{
parent::setFile($file);
$content = file_get_contents(media_path($this->attributes['file_path']));
if ($content === false) {
return $this;
}
$metadata = [];
if ($fileMetadata = lstat((string) $file)) {
$metadata = $fileMetadata;
}
$transcriptParser = new TranscriptParser();
$jsonFilePath = $this->attributes['file_directory'] . '/' . $this->attributes['file_name'] . '.json';
if (($transcriptJson = $transcriptParser->loadString($content)->parseSrt()) && file_put_contents(
media_path($jsonFilePath),
$transcriptJson
)) {
// set metadata (generated json file path)
$metadata['json_path'] = $jsonFilePath;
}
$this->attributes['file_metadata'] = json_encode($metadata);
return $this;
}
public function deleteFile(): void
{
parent::deleteFile();
if ($this->json_path) {
unlink($this->json_path);
}
}
}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -12,7 +12,12 @@ namespace App\Entities; ...@@ -12,7 +12,12 @@ namespace App\Entities;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter; 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 $id
...@@ -35,11 +40,11 @@ class Page extends Entity ...@@ -35,11 +40,11 @@ class Page extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'title' => 'string', 'title' => 'string',
'slug' => 'string', 'slug' => 'string',
'content_markdown' => 'string', 'content_markdown' => 'string',
'content_html' => 'string', 'content_html' => 'string',
]; ];
public function getLink(): string public function getLink(): string
...@@ -49,13 +54,20 @@ class Page extends Entity ...@@ -49,13 +54,20 @@ class Page extends Entity
public function setContentMarkdown(string $contentMarkdown): static public function setContentMarkdown(string $contentMarkdown): static
{ {
$converter = new CommonMarkConverter([ $config = [
'html_input' => 'strip',
'allow_unsafe_links' => false, 'allow_unsafe_links' => false,
]); ];
$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['content_markdown'] = $contentMarkdown; $this->attributes['content_markdown'] = $contentMarkdown;
$this->attributes['content_html'] = $converter->convertToHtml($contentMarkdown); $this->attributes['content_html'] = $converter->convert($contentMarkdown);
return $this; return $this;
} }
......
...@@ -3,19 +3,19 @@ ...@@ -3,19 +3,19 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2021 Podlibre * @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Entities; namespace App\Entities;
use App\Entities\Media\Image;
use App\Models\MediaModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use RuntimeException; use RuntimeException;
/** /**
...@@ -24,7 +24,7 @@ use RuntimeException; ...@@ -24,7 +24,7 @@ use RuntimeException;
* @property string $unique_name * @property string $unique_name
* @property string|null $information_url * @property string|null $information_url
* @property int $avatar_id * @property int $avatar_id
* @property Image $avatar * @property ?Image $avatar
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property object[]|null $roles * @property object[]|null $roles
...@@ -42,23 +42,23 @@ class Person extends Entity ...@@ -42,23 +42,23 @@ class Person extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'full_name' => 'string', 'full_name' => 'string',
'unique_name' => 'string', 'unique_name' => 'string',
'information_url' => '?string', 'information_url' => '?string',
'avatar_id' => '?int', 'avatar_id' => '?int',
'podcast_id' => '?integer', 'podcast_id' => '?integer',
'episode_id' => '?integer', 'episode_id' => '?integer',
'created_by' => 'integer', 'created_by' => 'integer',
'updated_by' => 'integer', 'updated_by' => 'integer',
]; ];
/** /**
* Saves the person avatar in `public/media/persons/` * Saves the person avatar in `public/media/persons/`
*/ */
public function setAvatar(UploadedFile | File $file = null): static public function setAvatar(UploadedFile | File|null $file = null): static
{ {
if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
} }
...@@ -66,16 +66,15 @@ class Person extends Entity ...@@ -66,16 +66,15 @@ class Person extends Entity
$this->getAvatar() $this->getAvatar()
->setFile($file); ->setFile($file);
$this->getAvatar() $this->getAvatar()
->updated_by = (int) user_id(); ->updated_by = $this->attributes['updated_by'];
(new MediaModel('image'))->updateMedia($this->getAvatar()); (new MediaModel('image'))->updateMedia($this->getAvatar());
} else { } else {
$avatar = new Image([ $avatar = new Image([
'file_name' => $this->attributes['unique_name'], 'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
'file_directory' => 'persons', 'sizes' => config('Images')
'sizes' => config('Images')
->personAvatarSizes, ->personAvatarSizes,
'uploaded_by' => user_id(), 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => user_id(), 'updated_by' => $this->attributes['updated_by'],
]); ]);
$avatar->setFile($file); $avatar->setFile($file);
...@@ -85,24 +84,13 @@ class Person extends Entity ...@@ -85,24 +84,13 @@ class Person extends Entity
return $this; return $this;
} }
public function getAvatar(): Image public function getAvatar(): ?Image
{ {
if ($this->attributes['avatar_id'] === null) { if ($this->avatar_id === null) {
helper('media'); return null;
return new Image([
'file_path' => config('Images')
->avatarDefaultPath,
'file_mimetype' => config('Images')
->avatarDefaultMimeType,
'file_size' => 0,
'file_metadata' => [
'sizes' => config('Images')
->personAvatarSizes,
],
]);
} }
if ($this->avatar === null) { if (! $this->avatar instanceof Image) {
$this->avatar = (new MediaModel('image'))->getMediaById($this->avatar_id); $this->avatar = (new MediaModel('image'))->getMediaById($this->avatar_id);
} }
...@@ -122,7 +110,7 @@ class Person extends Entity ...@@ -122,7 +110,7 @@ class Person extends Entity
$this->roles = (new PersonModel())->getPersonRoles( $this->roles = (new PersonModel())->getPersonRoles(
$this->id, $this->id,
(int) $this->attributes['podcast_id'], (int) $this->attributes['podcast_id'],
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null,
); );
} }
......
...@@ -3,28 +3,36 @@ ...@@ -3,28 +3,36 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Entities; namespace App\Entities;
use App\Entities\Media\Image;
use App\Libraries\SimpleRSSElement;
use App\Models\ActorModel; use App\Models\ActorModel;
use App\Models\CategoryModel; use App\Models\CategoryModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PlatformModel;
use App\Models\UserModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter; use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Entities\User; use Exception;
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;
use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Models\PlatformModel;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException; use RuntimeException;
/** /**
...@@ -33,6 +41,7 @@ use RuntimeException; ...@@ -33,6 +41,7 @@ use RuntimeException;
* @property int $actor_id * @property int $actor_id
* @property Actor|null $actor * @property Actor|null $actor
* @property string $handle * @property string $handle
* @property string $at_handle
* @property string $link * @property string $link
* @property string $feed_url * @property string $feed_url
* @property string $title * @property string $title
...@@ -40,9 +49,9 @@ use RuntimeException; ...@@ -40,9 +49,9 @@ use RuntimeException;
* @property string $description_markdown * @property string $description_markdown
* @property string $description_html * @property string $description_html
* @property int $cover_id * @property int $cover_id
* @property Image $cover * @property ?Image $cover
* @property int|null $banner_id * @property int|null $banner_id
* @property Image|null $banner * @property ?Image $banner
* @property string $language_code * @property string $language_code
* @property int $category_id * @property int $category_id
* @property Category|null $category * @property Category|null $category
...@@ -54,8 +63,6 @@ use RuntimeException; ...@@ -54,8 +63,6 @@ use RuntimeException;
* @property string $owner_email * @property string $owner_email
* @property string $type * @property string $type
* @property string|null $copyright * @property string|null $copyright
* @property string|null $episode_description_footer_markdown
* @property string|null $episode_description_footer_html
* @property bool $is_blocked * @property bool $is_blocked
* @property bool $is_completed * @property bool $is_completed
* @property bool $is_locked * @property bool $is_locked
...@@ -65,21 +72,20 @@ use RuntimeException; ...@@ -65,21 +72,20 @@ use RuntimeException;
* @property string|null $location_name * @property string|null $location_name
* @property string|null $location_geo * @property string|null $location_geo
* @property string|null $location_osm * @property string|null $location_osm
* @property string|null $payment_pointer * @property bool $is_published_on_hubs
* @property array|null $custom_rss
* @property string $custom_rss_string
* @property string|null $partner_id
* @property string|null $partner_link_url
* @property string|null $partner_image_url
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property Time $created_at; * @property string $publication_status
* @property Time $updated_at; * @property bool $is_premium_by_default
* @property Time|null $deleted_at; * @property bool $is_premium
* @property Time|null $published_at
* @property Time $created_at
* @property Time $updated_at
* *
* @property Episode[] $episodes * @property Episode[] $episodes
* @property Person[] $persons * @property Person[] $persons
* @property User[] $contributors * @property User[] $contributors
* @property Subscription[] $subscriptions
* @property Platform[] $podcasting_platforms * @property Platform[] $podcasting_platforms
* @property Platform[] $social_platforms * @property Platform[] $social_platforms
* @property Platform[] $funding_platforms * @property Platform[] $funding_platforms
...@@ -88,6 +94,8 @@ class Podcast extends Entity ...@@ -88,6 +94,8 @@ class Podcast extends Entity
{ {
protected string $link; protected string $link;
protected string $at_handle;
protected ?Actor $actor = null; protected ?Actor $actor = null;
protected ?Image $cover = null; protected ?Image $cover = null;
...@@ -104,9 +112,9 @@ class Podcast extends Entity ...@@ -104,9 +112,9 @@ class Podcast extends Entity
protected ?array $other_categories = null; protected ?array $other_categories = null;
/** /**
* @var string[]|null * @var int[]
*/ */
protected ?array $other_categories_ids = null; protected array $other_categories_ids = [];
/** /**
* @var Episode[]|null * @var Episode[]|null
...@@ -123,6 +131,11 @@ class Podcast extends Entity ...@@ -123,6 +131,11 @@ class Podcast extends Entity
*/ */
protected ?array $contributors = null; protected ?array $contributors = null;
/**
* @var Subscription[]|null
*/
protected ?array $subscriptions = null;
/** /**
* @var Platform[]|null * @var Platform[]|null
*/ */
...@@ -140,70 +153,71 @@ class Podcast extends Entity ...@@ -140,70 +153,71 @@ class Podcast extends Entity
protected ?Location $location = null; protected ?Location $location = null;
protected string $custom_rss_string; protected ?string $publication_status = null;
/**
* @var array<int, string>
* @phpstan-var list<string>
*/
protected $dates = ['published_at', 'created_at', 'updated_at'];
/** /**
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'guid' => 'string', 'guid' => 'string',
'actor_id' => 'integer', 'actor_id' => 'integer',
'handle' => 'string', 'handle' => 'string',
'title' => 'string', 'title' => 'string',
'description_markdown' => 'string', 'description_markdown' => 'string',
'description_html' => 'string', 'description_html' => 'string',
'cover_id' => 'int', 'cover_id' => 'int',
'banner_id' => '?int', 'banner_id' => '?int',
'language_code' => 'string', 'language_code' => 'string',
'category_id' => 'integer', 'category_id' => 'integer',
'parental_advisory' => '?string', 'parental_advisory' => '?string',
'publisher' => '?string', 'publisher' => '?string',
'owner_name' => 'string', 'owner_name' => 'string',
'owner_email' => 'string', 'owner_email' => 'string',
'type' => 'string', 'type' => 'string',
'copyright' => '?string', 'copyright' => '?string',
'episode_description_footer_markdown' => '?string', 'is_blocked' => 'boolean',
'episode_description_footer_html' => '?string', 'is_completed' => 'boolean',
'is_blocked' => 'boolean', 'is_locked' => 'boolean',
'is_completed' => 'boolean', 'is_premium_by_default' => 'boolean',
'is_locked' => 'boolean', 'imported_feed_url' => '?string',
'imported_feed_url' => '?string', 'new_feed_url' => '?string',
'new_feed_url' => '?string', 'location_name' => '?string',
'location_name' => '?string', 'location_geo' => '?string',
'location_geo' => '?string', 'location_osm' => '?string',
'location_osm' => '?string', 'is_published_on_hubs' => 'boolean',
'payment_pointer' => '?string', 'created_by' => 'integer',
'custom_rss' => '?json-array', 'updated_by' => 'integer',
'partner_id' => '?string',
'partner_link_url' => '?string',
'partner_image_url' => '?string',
'created_by' => 'integer',
'updated_by' => 'integer',
]; ];
/** public function getAtHandle(): string
* @noRector ReturnTypeDeclarationRector {
*/ return '@' . $this->handle;
}
public function getActor(): ?Actor public function getActor(): ?Actor
{ {
if ($this->actor_id === 0) { if ($this->actor_id === 0) {
throw new RuntimeException('Podcast must have an actor_id before getting actor.'); throw new RuntimeException('Podcast must have an actor_id before getting actor.');
} }
if ($this->actor === null) { if (! $this->actor instanceof Actor) {
// @phpstan-ignore-next-line $this->actor = model(ActorModel::class, false)
$this->actor = model(ActorModel::class)
->getActorById($this->actor_id); ->getActorById($this->actor_id);
} }
// @phpstan-ignore-next-line
return $this->actor; return $this->actor;
} }
public function setCover(UploadedFile | File $file = null): self public function setCover(UploadedFile | File|null $file = null): self
{ {
if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
} }
...@@ -211,16 +225,15 @@ class Podcast extends Entity ...@@ -211,16 +225,15 @@ class Podcast extends Entity
$this->getCover() $this->getCover()
->setFile($file); ->setFile($file);
$this->getCover() $this->getCover()
->updated_by = (int) user_id(); ->updated_by = $this->attributes['updated_by'];
(new MediaModel('image'))->updateMedia($this->getCover()); (new MediaModel('image'))->updateMedia($this->getCover());
} else { } else {
$cover = new Image([ $cover = new Image([
'file_name' => 'cover', 'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
'file_directory' => 'podcasts/' . $this->attributes['handle'], 'sizes' => config('Images')
'sizes' => config('Images')
->podcastCoverSizes, ->podcastCoverSizes,
'uploaded_by' => user_id(), 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => user_id(), 'updated_by' => $this->attributes['updated_by'],
]); ]);
$cover->setFile($file); $cover->setFile($file);
...@@ -233,15 +246,21 @@ class Podcast extends Entity ...@@ -233,15 +246,21 @@ class Podcast extends Entity
public function getCover(): Image public function getCover(): Image
{ {
if (! $this->cover instanceof Image) { if (! $this->cover instanceof Image) {
$this->cover = (new MediaModel('image'))->getMediaById($this->cover_id); $cover = (new MediaModel('image'))->getMediaById($this->cover_id);
if (! $cover instanceof Image) {
throw new Exception('Could not retrieve podcast cover.');
}
$this->cover = $cover;
} }
return $this->cover; return $this->cover;
} }
public function setBanner(UploadedFile | File $file = null): self public function setBanner(UploadedFile | File|null $file = null): self
{ {
if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
} }
...@@ -249,16 +268,15 @@ class Podcast extends Entity ...@@ -249,16 +268,15 @@ class Podcast extends Entity
$this->getBanner() $this->getBanner()
->setFile($file); ->setFile($file);
$this->getBanner() $this->getBanner()
->updated_by = (int) user_id(); ->updated_by = $this->attributes['updated_by'];
(new MediaModel('image'))->updateMedia($this->getBanner()); (new MediaModel('image'))->updateMedia($this->getBanner());
} else { } else {
$banner = new Image([ $banner = new Image([
'file_name' => 'banner', 'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
'file_directory' => 'podcasts/' . $this->attributes['handle'], 'sizes' => config('Images')
'sizes' => config('Images')
->podcastBannerSizes, ->podcastBannerSizes,
'uploaded_by' => user_id(), 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => user_id(), 'updated_by' => $this->attributes['updated_by'],
]); ]);
$banner->setFile($file); $banner->setFile($file);
...@@ -268,20 +286,10 @@ class Podcast extends Entity ...@@ -268,20 +286,10 @@ class Podcast extends Entity
return $this; return $this;
} }
public function getBanner(): Image public function getBanner(): ?Image
{ {
if ($this->banner_id === null) { if ($this->banner_id === null) {
return new Image([ return null;
'file_path' => config('Images')
->podcastBannerDefaultPath,
'file_mimetype' => config('Images')
->podcastBannerDefaultMimeType,
'file_size' => 0,
'file_metadata' => [
'sizes' => config('Images')
->podcastBannerSizes,
],
]);
} }
if (! $this->banner instanceof Image) { if (! $this->banner instanceof Image) {
...@@ -298,7 +306,7 @@ class Podcast extends Entity ...@@ -298,7 +306,7 @@ class Podcast extends Entity
public function getFeedUrl(): string public function getFeedUrl(): string
{ {
return url_to('podcast_feed', $this->attributes['handle']); return url_to('podcast-rss-feed', $this->attributes['handle']);
} }
/** /**
...@@ -319,6 +327,18 @@ class Podcast extends Entity ...@@ -319,6 +327,18 @@ class Podcast extends Entity
return $this->episodes; return $this->episodes;
} }
/**
* Returns the podcast's episodes count
*/
public function getEpisodesCount(): int|string
{
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting number of episodes.');
}
return (new EpisodeModel())->getPodcastEpisodesCount($this->id);
}
/** /**
* Returns the podcast's persons * Returns the podcast's persons
* *
...@@ -353,6 +373,24 @@ class Podcast extends Entity ...@@ -353,6 +373,24 @@ class Podcast extends Entity
return $this->category; return $this->category;
} }
/**
* Returns all podcast subscriptions
*
* @return Subscription[]
*/
public function getSubscriptions(): array
{
if ($this->id === null) {
throw new RuntimeException('Podcasts must be created before getting subscriptions.');
}
if ($this->subscriptions === null) {
$this->subscriptions = (new SubscriptionModel())->getPodcastSubscriptions($this->id);
}
return $this->subscriptions;
}
/** /**
* Returns all podcast contributors * Returns all podcast contributors
* *
...@@ -373,41 +411,21 @@ class Podcast extends Entity ...@@ -373,41 +411,21 @@ class Podcast extends Entity
public function setDescriptionMarkdown(string $descriptionMarkdown): static public function setDescriptionMarkdown(string $descriptionMarkdown): static
{ {
$converter = new CommonMarkConverter([ $config = [
'html_input' => 'strip', 'html_input' => 'escape',
'allow_unsafe_links' => false, 'allow_unsafe_links' => false,
]); ];
$this->attributes['description_markdown'] = $descriptionMarkdown; $environment = new Environment($config);
$this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown); $environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
return $this; $environment->addExtension(new SmartPunctExtension());
} $environment->addExtension(new DisallowedRawHtmlExtension());
public function setEpisodeDescriptionFooterMarkdown(?string $episodeDescriptionFooterMarkdown = null): static
{
if ($episodeDescriptionFooterMarkdown === null || $episodeDescriptionFooterMarkdown === '') {
$this->attributes[
'episode_description_footer_markdown'
] = null;
$this->attributes[
'episode_description_footer_html'
] = null;
return $this;
}
$converter = new CommonMarkConverter([ $converter = new MarkdownConverter($environment);
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
$this->attributes[ $this->attributes['description_markdown'] = $descriptionMarkdown;
'episode_description_footer_markdown' $this->attributes['description_html'] = $converter->convert($descriptionMarkdown);
] = $episodeDescriptionFooterMarkdown;
$this->attributes[
'episode_description_footer_html'
] = $converter->convertToHtml($episodeDescriptionFooterMarkdown);
return $this; return $this;
} }
...@@ -416,13 +434,28 @@ class Podcast extends Entity ...@@ -416,13 +434,28 @@ class Podcast extends Entity
{ {
if ($this->description === null) { if ($this->description === null) {
$this->description = trim( $this->description = trim(
(string) preg_replace('~\s+~', ' ', strip_tags($this->attributes['description_html'])), (string) preg_replace('~\s+~', ' ', strip_tags((string) $this->attributes['description_html'])),
); );
} }
return $this->description; return $this->description;
} }
public function getPublicationStatus(): string
{
if ($this->publication_status === null) {
if (! $this->published_at instanceof Time) {
$this->publication_status = 'not_published';
} elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published';
} else {
$this->publication_status = 'scheduled';
}
}
return $this->publication_status;
}
/** /**
* Returns the podcast's podcasting platform links * Returns the podcast's podcasting platform links
* *
...@@ -435,7 +468,7 @@ class Podcast extends Entity ...@@ -435,7 +468,7 @@ class Podcast extends Entity
} }
if ($this->podcasting_platforms === null) { if ($this->podcasting_platforms === null) {
$this->podcasting_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'podcasting'); $this->podcasting_platforms = (new PlatformModel())->getPlatforms($this->id, 'podcasting');
} }
return $this->podcasting_platforms; return $this->podcasting_platforms;
...@@ -453,7 +486,7 @@ class Podcast extends Entity ...@@ -453,7 +486,7 @@ class Podcast extends Entity
} }
if ($this->social_platforms === null) { if ($this->social_platforms === null) {
$this->social_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'social'); $this->social_platforms = (new PlatformModel())->getPlatforms($this->id, 'social');
} }
return $this->social_platforms; return $this->social_platforms;
...@@ -471,7 +504,7 @@ class Podcast extends Entity ...@@ -471,7 +504,7 @@ class Podcast extends Entity
} }
if ($this->funding_platforms === null) { if ($this->funding_platforms === null) {
$this->funding_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'funding'); $this->funding_platforms = (new PlatformModel())->getPlatforms($this->id, 'funding');
} }
return $this->funding_platforms; return $this->funding_platforms;
...@@ -498,7 +531,7 @@ class Podcast extends Entity ...@@ -498,7 +531,7 @@ class Podcast extends Entity
*/ */
public function getOtherCategoriesIds(): array public function getOtherCategoriesIds(): array
{ {
if ($this->other_categories_ids === null) { if ($this->other_categories_ids === []) {
$this->other_categories_ids = array_column($this->getOtherCategories(), 'id'); $this->other_categories_ids = array_column($this->getOtherCategories(), 'id');
} }
...@@ -510,7 +543,7 @@ class Podcast extends Entity ...@@ -510,7 +543,7 @@ class Podcast extends Entity
*/ */
public function setLocation(?Location $location = null): static public function setLocation(?Location $location = null): static
{ {
if ($location === null) { if (! $location instanceof Location) {
$this->attributes['location_name'] = null; $this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null; $this->attributes['location_geo'] = null;
$this->attributes['location_osm'] = null; $this->attributes['location_osm'] = null;
...@@ -538,59 +571,16 @@ class Podcast extends Entity ...@@ -538,59 +571,16 @@ class Podcast extends Entity
return null; return null;
} }
if ($this->location === null) { if (! $this->location instanceof Location) {
$this->location = new Location($this->location_name, $this->location_geo, $this->location_osm); $this->location = new Location($this->location_name, $this->location_geo, $this->location_osm);
} }
return $this->location; return $this->location;
} }
/** public function getIsPremium(): bool
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string
{ {
if ($this->attributes['custom_rss'] === null) { // podcast is premium if at least one of its episodes is set as premium
return ''; return (new EpisodeModel())->doesPodcastHavePremiumEpisodes($this->id);
}
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');
array_to_rss([
'elements' => $this->custom_rss,
], $xmlNode);
return str_replace(['<channel>', '</channel>'], '', $xmlNode->asXML());
}
/**
* Saves custom rss tag into json
*/
public function setCustomRssString(string $customRssString): static
{
if ($customRssString === '') {
$this->attributes['custom_rss'] = null;
return $this;
}
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>' .
$customRssString .
'</channel></rss>',
),
)['elements'][0];
if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
} else {
$this->attributes['custom_rss'] = null;
}
return $this;
} }
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -26,18 +26,18 @@ class Post extends FediversePost ...@@ -26,18 +26,18 @@ class Post extends FediversePost
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'string', 'id' => 'string',
'uri' => 'string', 'uri' => 'string',
'actor_id' => 'integer', 'actor_id' => 'integer',
'in_reply_to_id' => '?string', 'in_reply_to_id' => '?string',
'reblog_of_id' => '?string', 'reblog_of_id' => '?string',
'episode_id' => '?integer', 'episode_id' => '?integer',
'message' => 'string', 'message' => 'string',
'message_html' => 'string', 'message_html' => 'string',
'favourites_count' => 'integer', 'favourites_count' => 'integer',
'reblogs_count' => 'integer', 'reblogs_count' => 'integer',
'replies_count' => 'integer', 'replies_count' => 'integer',
'created_by' => 'integer', 'created_by' => 'integer',
]; ];
/** /**
......
<?php
declare(strict_types=1);
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Override;
class AllowCorsFilter implements FilterInterface
{
/**
* @param list<string>|null $arguments
*
* @return RequestInterface|ResponseInterface|string|null
*/
#[Override]
public function before(RequestInterface $request, $arguments = null)
{
return null;
}
/**
* @param list<string>|null $arguments
*
* @return ResponseInterface|null
*/
#[Override]
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
if (! $response->hasHeader('Cache-Control')) {
$response->setHeader('Cache-Control', 'public, max-age=86400');
}
$response->setHeader('Access-Control-Allow-Origin', '*') // for allowing any domain, insecure
->setHeader('Access-Control-Allow-Headers', '*') // for allowing any headers, insecure
->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') // allows GET and OPTIONS methods only
->setHeader('Access-Control-Max-Age', '86400');
return $response;
}
}
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use App\Models\ActorModel;
use CodeIgniter\Database\Exceptions\DataException;
use Modules\Auth\Entities\User;
use Modules\Fediverse\Entities\Actor;
if (! function_exists('user')) {
/**
* Returns the User instance for the current logged in user.
*/
function user(): ?User
{
$authenticate = service('authentication');
$authenticate->check();
return $authenticate->user();
}
}
if (! function_exists('set_interact_as_actor')) {
/**
* Sets the actor id of which the user is acting as
*/
function set_interact_as_actor(int $actorId): void
{
$authenticate = service('authentication');
$authenticate->check();
$session = session();
$session->set('interact_as_actor_id', $actorId);
}
}
if (! function_exists('remove_interact_as_actor')) {
/**
* Removes the actor id of which the user is acting as
*/
function remove_interact_as_actor(): void
{
$session = session();
$session->remove('interact_as_actor_id');
}
}
if (! function_exists('interact_as_actor_id')) {
/**
* Sets the podcast id of which the user is acting as
*/
function interact_as_actor_id(): int
{
$authenticate = service('authentication');
$authenticate->check();
$session = session();
return $session->get('interact_as_actor_id');
}
}
if (! function_exists('interact_as_actor')) {
/**
* Get the actor the user is currently interacting as
*/
function interact_as_actor(): Actor | false
{
$authenticate = service('authentication');
$authenticate->check();
$session = session();
if ($session->has('interact_as_actor_id')) {
return model(ActorModel::class)->getActorById($session->get('interact_as_actor_id'));
}
return false;
}
}
if (! function_exists('can_user_interact')) {
/**
* @throws DataException
*/
function can_user_interact(): bool
{
return (bool) interact_as_actor();
}
}
...@@ -2,35 +2,25 @@ ...@@ -2,35 +2,25 @@
declare(strict_types=1); declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use Config\Services;
if (! function_exists('render_breadcrumb')) { if (! function_exists('render_breadcrumb')) {
/** /**
* Renders the breadcrumb navigation through the Breadcrumb service * Renders the breadcrumb navigation through the Breadcrumb service
* *
* @param string $class to be added to the breadcrumb nav * @param string|null $class to be added to the breadcrumb nav
* @return string html breadcrumb * @return string html breadcrumb
*/ */
function render_breadcrumb(string $class = null): string function render_breadcrumb(?string $class = null): string
{ {
$breadcrumb = Services::breadcrumb(); return service('breadcrumb')->render($class);
return $breadcrumb->render($class);
} }
} }
if (! function_exists('replace_breadcrumb_params')) { if (! function_exists('replace_breadcrumb_params')) {
/** /**
* @param string[] $newParams * @param array<string|int,string> $newParams
*/ */
function replace_breadcrumb_params(array $newParams): void function replace_breadcrumb_params(array $newParams): void
{ {
$breadcrumb = Services::breadcrumb(); service('breadcrumb')->replaceParams($newParams);
$breadcrumb->replaceParams($newParams);
} }
} }
...@@ -3,41 +3,19 @@ ...@@ -3,41 +3,19 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
use App\Entities\Category;
use App\Entities\Episode;
use App\Entities\Location; use App\Entities\Location;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use CodeIgniter\View\Table; use CodeIgniter\View\Table;
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('hint_tooltip')) {
/**
* Hint component
*
* Used to produce tooltip with a question mark icon for hint texts
*
* @param string $hintText The hint text
*/
function hint_tooltip(string $hintText = '', string $class = ''): string
{
$tooltip =
'<span data-tooltip="bottom" tabindex="0" title="' .
$hintText .
'" class="inline-block align-middle opacity-75 focus:ring-accent';
if ($class !== '') {
$tooltip .= ' ' . $class;
}
return $tooltip . '">' . icon('question') . '</span>';
}
}
// ------------------------------------------------------------------------
if (! function_exists('data_table')) { if (! function_exists('data_table')) {
/** /**
* Data table component * Data table component
...@@ -48,21 +26,20 @@ if (! function_exists('data_table')) { ...@@ -48,21 +26,20 @@ if (! function_exists('data_table')) {
* @param mixed[] $data data to loop through and display in rows * @param mixed[] $data data to loop through and display in rows
* @param mixed ...$rest Any other argument to pass to the `cell` function * @param mixed ...$rest Any other argument to pass to the `cell` function
*/ */
function data_table(array $columns, array $data = [], string $class = '', ...$rest): string function data_table(array $columns, array $data = [], string $class = '', mixed ...$rest): string
{ {
$table = new Table(); $table = new Table();
$template = [ $template = [
'table_open' => '<table class="w-full whitespace-no-wrap">', 'table_open' => '<table class="w-full whitespace-nowrap">',
'thead_open' => 'thead_open' => '<thead class="text-xs font-semibold text-left uppercase text-skin-muted">',
'<thead class="text-xs font-semibold text-left uppercase text-skin-muted">',
'heading_cell_start' => '<th class="px-4 py-2">', 'heading_cell_start' => '<th class="px-4 py-2">',
'cell_start' => '<td class="px-4 py-2">', 'cell_start' => '<td class="px-4 py-2">',
'cell_alt_start' => '<td class="px-4 py-2">', 'cell_alt_start' => '<td class="px-4 py-2">',
'row_start' => '<tr class="border-t border-subtle hover:bg-base">', 'row_start' => '<tr class="border-t border-subtle hover:bg-base">',
'row_alt_start' => '<tr class="border-t border-subtle hover:bg-base">', 'row_alt_start' => '<tr class="border-t border-subtle hover:bg-base">',
]; ];
...@@ -82,14 +59,15 @@ if (! function_exists('data_table')) { ...@@ -82,14 +59,15 @@ if (! function_exists('data_table')) {
foreach ($columns as $column) { foreach ($columns as $column) {
$rowData[] = $column['cell']($row, ...$rest); $rowData[] = $column['cell']($row, ...$rest);
} }
$table->addRow($rowData); $table->addRow($rowData);
} }
} else { } else {
$table->addRow([ $table->addRow([
[ [
'colspan' => count($tableHeaders), 'colspan' => count($tableHeaders),
'class' => 'px-4 py-2 italic font-semibold text-center', 'class' => 'px-4 py-2 italic font-semibold text-center',
'data' => lang('Common.no_data'), 'data' => lang('Common.no_data'),
], ],
]); ]);
} }
...@@ -110,22 +88,29 @@ if (! function_exists('publication_pill')) { ...@@ -110,22 +88,29 @@ if (! function_exists('publication_pill')) {
*/ */
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
{ {
$class = match ($publicationStatus) { $variant = match ($publicationStatus) {
'published' => 'text-pine-500 border-pine-500 bg-pine-50', 'published' => 'success',
'scheduled' => 'text-red-600 border-red-600 bg-red-50', 'scheduled' => 'warning',
'not_published' => 'text-gray-600 border-gray-600 bg-gray-50', 'with_podcast' => 'info',
default => 'text-gray-600 border-gray-600 bg-gray-50', 'not_published' => 'default',
default => 'default',
};
$title = match ($publicationStatus) {
'published', 'scheduled' => (string) $publicationDate,
'with_podcast' => lang('Episode.with_podcast_hint'),
'not_published' => '',
default => '',
}; };
$label = lang('Episode.publication_status.' . $publicationStatus); $label = lang('Episode.publication_status.' . $publicationStatus);
return '<span ' . ($publicationDate === null ? '' : 'title="' . $publicationDate . '"') . ' class="px-1 font-semibold border rounded ' . // @icon("error-warning-fill")
$class . return '<x-Pill ' . ($title === '' ? '' : 'title="' . $title . '"') . ' variant="' . $variant . '" class="' . $customClass .
' ' . '">' . $label . ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
$customClass . 'class' => 'flex-shrink-0 ml-1 text-lg',
'">' . ]) : '') .
$label . '</x-Pill>';
'</span>';
} }
} }
...@@ -133,31 +118,31 @@ if (! function_exists('publication_pill')) { ...@@ -133,31 +118,31 @@ if (! function_exists('publication_pill')) {
if (! function_exists('publication_button')) { if (! function_exists('publication_button')) {
/** /**
* Publication button component * Publication button component for episodes
* *
* Displays the appropriate publication button depending on the publication post. * Displays the appropriate publication button depending on the publication post.
*/ */
function publication_button(int $podcastId, int $episodeId, string $publicationStatus): string function publication_button(int $podcastId, int $episodeId, string $publicationStatus): string
{ {
/** @phpstan-ignore-next-line */
switch ($publicationStatus) { switch ($publicationStatus) {
case 'not_published': case 'not_published':
$label = lang('Episode.publish'); $label = lang('Episode.publish');
$route = route_to('episode-publish', $podcastId, $episodeId); $route = route_to('episode-publish', $podcastId, $episodeId);
$variant = 'primary'; $variant = 'primary';
$iconLeft = 'upload-cloud'; $iconLeft = 'upload-cloud-fill'; // @icon("upload-cloud-fill")
break; break;
case 'with_podcast':
case 'scheduled': case 'scheduled':
$label = lang('Episode.publish_edit'); $label = lang('Episode.publish_edit');
$route = route_to('episode-publish_edit', $podcastId, $episodeId); $route = route_to('episode-publish_edit', $podcastId, $episodeId);
$variant = 'warning'; $variant = 'warning';
$iconLeft = 'upload-cloud'; $iconLeft = 'upload-cloud-fill'; // @icon("upload-cloud-fill")
break; break;
case 'published': case 'published':
$label = lang('Episode.unpublish'); $label = lang('Episode.unpublish');
$route = route_to('episode-unpublish', $podcastId, $episodeId); $route = route_to('episode-unpublish', $podcastId, $episodeId);
$variant = 'danger'; $variant = 'danger';
$iconLeft = 'cloud-off'; $iconLeft = 'cloud-off-fill'; // @icon("cloud-off-fill")
break; break;
default: default:
$label = ''; $label = '';
...@@ -167,9 +152,106 @@ if (! function_exists('publication_button')) { ...@@ -167,9 +152,106 @@ if (! function_exists('publication_button')) {
break; break;
} }
return <<<CODE_SAMPLE return <<<HTML
<Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</Button> <x-Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</x-Button>
CODE_SAMPLE; HTML;
}
}
// ------------------------------------------------------------------------
if (! function_exists('publication_status_banner')) {
/**
* Publication status banner component for podcasts
*
* Displays the appropriate banner depending on the podcast's publication status.
*/
function publication_status_banner(?Time $publicationDate, int $podcastId, string $publicationStatus): string
{
switch ($publicationStatus) {
case 'not_published':
$bannerDisclaimer = lang('Podcast.publication_status_banner.draft_mode');
$bannerText = lang('Podcast.publication_status_banner.not_published');
$linkRoute = route_to('podcast-publish', $podcastId);
$linkLabel = lang('Podcast.publish');
break;
case 'scheduled':
$bannerDisclaimer = lang('Podcast.publication_status_banner.draft_mode');
$bannerText = lang('Podcast.publication_status_banner.scheduled', [
'publication_date' => local_datetime($publicationDate),
]);
$linkRoute = route_to('podcast-publish_edit', $podcastId);
$linkLabel = lang('Podcast.publish_edit');
break;
default:
$bannerDisclaimer = '';
$bannerText = '';
$linkRoute = '';
$linkLabel = '';
break;
}
return <<<HTML
<div class="flex flex-wrap items-baseline px-4 py-2 border-b md:px-12 bg-stripes-default border-subtle" role="alert">
<p class="flex items-baseline text-gray-900">
<span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
<span class="ml-3 text-sm">{$bannerText}</span>
</p>
<a href="{$linkRoute}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$linkLabel}</a>
</div>
HTML;
}
}
// ------------------------------------------------------------------------
if (! function_exists('episode_publication_status_banner')) {
/**
* Publication status banner component for podcasts
*
* Displays the appropriate banner depending on the podcast's publication status.
*/
function episode_publication_status_banner(Episode $episode, string $class = ''): string
{
switch ($episode->publication_status) {
case 'not_published':
$linkRoute = route_to('episode-publish', $episode->podcast_id, $episode->id);
$publishLinkLabel = lang('Episode.publish');
break;
case 'scheduled':
case 'with_podcast':
$linkRoute = route_to('episode-publish_edit', $episode->podcast_id, $episode->id);
$publishLinkLabel = lang('Episode.publish_edit');
break;
default:
$bannerDisclaimer = '';
$linkRoute = '';
$publishLinkLabel = '';
break;
}
$bannerDisclaimer = lang('Episode.publication_status_banner.draft_mode');
$bannerText = lang('Episode.publication_status_banner.text', [
'publication_status' => $episode->publication_status,
'publication_date' => $episode->published_at instanceof Time ? local_datetime(
$episode->published_at,
) : null,
]);
$previewLinkLabel = lang('Episode.publication_status_banner.preview');
return <<<HTML
<div class="flex flex-wrap gap-4 items-baseline px-4 md:px-12 py-2 bg-stripes-default border-subtle {$class}" role="alert">
<p class="flex items-baseline text-gray-900">
<span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
<span class="ml-3 text-sm">{$bannerText}</span>
</p>
<div class="flex items-baseline">
<a href="{$episode->preview_link}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$previewLinkLabel}</a>
<span class="mx-1">•</span>
<a href="{$linkRoute}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$publishLinkLabel}</a>
</div>
</div>
HTML;
} }
} }
...@@ -185,7 +267,7 @@ if (! function_exists('episode_numbering')) { ...@@ -185,7 +267,7 @@ if (! function_exists('episode_numbering')) {
?int $episodeNumber = null, ?int $episodeNumber = null,
?int $seasonNumber = null, ?int $seasonNumber = null,
string $class = '', string $class = '',
bool $isAbbr = false bool $isAbbr = false,
): string { ): string {
if (! $episodeNumber && ! $seasonNumber) { if (! $episodeNumber && ! $seasonNumber) {
return ''; return '';
...@@ -235,19 +317,20 @@ if (! function_exists('location_link')) { ...@@ -235,19 +317,20 @@ if (! function_exists('location_link')) {
*/ */
function location_link(?Location $location, string $class = ''): string function location_link(?Location $location, string $class = ''): string
{ {
if ($location === null) { if (! $location instanceof Location) {
return ''; return '';
} }
return anchor( return anchor(
$location->url, $location->url,
icon('map-pin', 'mr-2') . $location->name, icon('map-pin-2-fill', [
'class' => 'mr-2 flex-shrink-0',
]) . '<span class="truncate">' . esc($location->name) . '</span>',
[ [
'class' => 'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline' .
'inline-flex items-baseline hover:underline focus:ring-accent' .
($class === '' ? '' : " {$class}"), ($class === '' ? '' : " {$class}"),
'target' => '_blank', 'target' => '_blank',
'rel' => 'noreferrer noopener', 'rel' => 'noreferrer noopener',
], ],
); );
} }
...@@ -264,12 +347,11 @@ if (! function_exists('audio_player')) { ...@@ -264,12 +347,11 @@ if (! function_exists('audio_player')) {
$language = service('request') $language = service('request')
->getLocale(); ->getLocale();
return <<<CODE_SAMPLE return <<<HTML
<vm-player <vm-player
id="castopod-vm-player" id="castopod-vm-player"
theme="light" theme="light"
language="{$language}" language="{$language}"
icons="castopod-icons"
class="{$class} relative z-0" class="{$class} relative z-0"
style="--vm-player-box-shadow:0; --vm-player-theme: hsl(var(--color-accent-base)); --vm-control-focus-color: hsl(var(--color-accent-contrast)); --vm-control-spacing: 4px; --vm-menu-item-focus-bg: hsl(var(--color-background-highlight));" style="--vm-player-box-shadow:0; --vm-player-theme: hsl(var(--color-accent-base)); --vm-control-focus-color: hsl(var(--color-accent-contrast)); --vm-control-spacing: 4px; --vm-menu-item-focus-bg: hsl(var(--color-background-highlight));"
> >
...@@ -277,7 +359,7 @@ if (! function_exists('audio_player')) { ...@@ -277,7 +359,7 @@ if (! function_exists('audio_player')) {
<source src="{$source}" type="{$mediaType}" /> <source src="{$source}" type="{$mediaType}" />
</vm-audio> </vm-audio>
<vm-ui> <vm-ui>
<vm-icon-library name="castopod-icons"></vm-icon-library> <vm-icon-library></vm-icon-library>
<vm-controls full-width> <vm-controls full-width>
<vm-playback-control></vm-playback-control> <vm-playback-control></vm-playback-control>
<vm-volume-control></vm-volume-control> <vm-volume-control></vm-volume-control>
...@@ -289,7 +371,7 @@ if (! function_exists('audio_player')) { ...@@ -289,7 +371,7 @@ if (! function_exists('audio_player')) {
</vm-controls> </vm-controls>
</vm-ui> </vm-ui>
</vm-player> </vm-player>
CODE_SAMPLE; HTML;
} }
} }
...@@ -299,20 +381,127 @@ if (! function_exists('relative_time')) { ...@@ -299,20 +381,127 @@ if (! function_exists('relative_time')) {
function relative_time(Time $time, string $class = ''): string function relative_time(Time $time, string $class = ''): string
{ {
$formatter = new IntlDateFormatter(service( $formatter = new IntlDateFormatter(service(
'request' 'request',
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE); )->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
$translatedDate = $time->toLocalizedString($formatter->getPattern()); $translatedDate = $time->toLocalizedString($formatter->getPattern());
$datetime = $time->format(DateTime::ISO8601); $datetime = $time->format(DateTime::ATOM);
return <<<HTML
<relative-time tense="auto" class="{$class}" datetime="{$datetime}">
<time
datetime="{$datetime}"
title="{$time}">{$translatedDate}</time>
</relative-time>
HTML;
}
}
// ------------------------------------------------------------------------
return <<<CODE_SAMPLE if (! function_exists('local_datetime')) {
<time-ago class="{$class}" datetime="{$datetime}"> function local_datetime(Time $time): string
{
$formatter = new IntlDateFormatter(service(
'request',
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
$translatedDate = $time->toLocalizedString($formatter->getPattern());
$datetime = $time->format(DateTime::ATOM);
return <<<HTML
<relative-time datetime="{$datetime}"
prefix=""
threshold="PT0S"
weekday="long"
day="numeric"
month="long"
year="numeric"
hour="numeric"
minute="numeric">
<time <time
itemprop="published"
datetime="{$datetime}" datetime="{$datetime}"
title="{$time}">{$translatedDate}</time> title="{$time}">{$translatedDate}</time>
</time-ago> </relative-time>
CODE_SAMPLE; HTML;
} }
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('local_date')) {
function local_date(Time $time): string
{
$formatter = new IntlDateFormatter(service(
'request',
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
$translatedDate = $time->toLocalizedString($formatter->getPattern());
return <<<HTML
<time title="{$time}">{$translatedDate}</time>
HTML;
}
}
// ------------------------------------------------------------------------
if (! function_exists('explicit_badge')) {
function explicit_badge(bool $isExplicit, string $class = ''): string
{
if (! $isExplicit) {
return '';
}
$explicitLabel = lang('Common.explicit');
return <<<HTML
<span class="px-1 text-xs font-semibold leading-tight tracking-wider uppercase border md:border-white/50 {$class}">{$explicitLabel}</span>
HTML;
}
}
// ------------------------------------------------------------------------
if (! function_exists('category_label')) {
function category_label(Category $category): string
{
$categoryLabel = '';
if ($category->parent_id !== null) {
$categoryLabel .= lang('Podcast.category_options.' . $category->parent->code) . ' › ';
}
return $categoryLabel . lang('Podcast.category_options.' . $category->code);
}
}
// ------------------------------------------------------------------------
if (! function_exists('downloads_abbr')) {
function downloads_abbr(int $downloads): string
{
if ($downloads < 1000) {
return (string) $downloads;
}
$option = match (true) {
$downloads < 1_000_000 => [
'divider' => 1_000,
'suffix' => 'K',
],
$downloads < 1_000_000_000 => [
'divider' => 1_000_000,
'suffix' => 'M',
],
default => [
'divider' => 1_000_000_000,
'suffix' => 'B',
],
};
$formatter = new NumberFormatter(service('request')->getLocale(), NumberFormatter::DECIMAL);
$formatter->setPattern('#,##0.##');
$abbr = $formatter->format($downloads / $option['divider']) . $option['suffix'];
return <<<HTML
<abbr title="{$downloads}">{$abbr}</abbr>
HTML;
}
}
<?php
declare(strict_types=1);
if (! function_exists('form_textarea')) {
/**
* Adapted textarea field from CI4 core: without value escaping.
*/
function form_textarea(mixed $data = '', string $value = '', mixed $extra = ''): string
{
$defaults = [
'name' => is_array($data) ? '' : $data,
'cols' => '40',
'rows' => '10',
];
if (! is_array($data) || ! isset($data['value'])) {
$val = $value;
} else {
$val = $data['value'];
unset($data['value']); // textareas don't use the value attribute
}
// Unsets default rows and cols if defined in extra field as array or string.
if ((is_array($extra) && array_key_exists('rows', $extra)) || (is_string($extra) && stripos(
(string) preg_replace('~\s+~', '', $extra),
'rows=',
) !== false)) {
unset($defaults['rows']);
}
if ((is_array($extra) && array_key_exists('cols', $extra)) || (is_string($extra) && stripos(
(string) preg_replace('~\s+~', '', $extra),
'cols=',
) !== false)) {
unset($defaults['cols']);
}
return '<textarea ' . rtrim(parse_form_attributes($data, $defaults)) . stringify_attributes(
$extra,
) . '>' . $val . "</textarea>\n";
}
}
if (! function_exists('parse_form_attributes')) {
/**
* Parse the form attributes
*
* Helper function used by some of the form helpers
*
* @param array<string, string>|string $attributes List of attributes
* @param array<string, mixed> $default Default values
*/
function parse_form_attributes(array|string $attributes, array $default): string
{
if (is_array($attributes)) {
foreach (array_keys($default) as $key) {
if (isset($attributes[$key])) {
$default[$key] = $attributes[$key];
unset($attributes[$key]);
}
}
if ($attributes !== []) {
$default = array_merge($default, $attributes);
}
}
$att = '';
foreach ($default as $key => $val) {
if (! is_bool($val)) {
if ($key === 'name' && ! strlen((string) $default['name'])) {
continue;
}
$att .= $key . '="' . $val . '"' . ($key === array_key_last($default) ? '' : ' ');
} else {
$att .= $key . ' ';
}
}
return $att;
}
}
...@@ -3,14 +3,14 @@ ...@@ -3,14 +3,14 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
use App\Entities\Episode; use App\Entities\Episode;
use CodeIgniter\Files\File; use CodeIgniter\I18n\Time;
use JamesHeinrich\GetID3\WriteTags; use JamesHeinrich\GetID3\WriteTags;
use Modules\Media\FileManagers\FileManagerInterface;
if (! function_exists('write_audio_file_tags')) { if (! function_exists('write_audio_file_tags')) {
/** /**
...@@ -24,15 +24,16 @@ if (! function_exists('write_audio_file_tags')) { ...@@ -24,15 +24,16 @@ if (! function_exists('write_audio_file_tags')) {
// Initialize getID3 tag-writing module // Initialize getID3 tag-writing module
$tagwriter = new WriteTags(); $tagwriter = new WriteTags();
$tagwriter->filename = media_path($episode->audio->file_path); $tagwriter->filename = $episode->audio->file_name;
// set various options (optional) // set various options (optional)
$tagwriter->tagformats = ['id3v2.4']; $tagwriter->tagformats = ['id3v2.4'];
$tagwriter->tag_encoding = $TextEncoding; $tagwriter->tag_encoding = $TextEncoding;
$cover = new File(media_path($episode->cover->id3_path)); /** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$APICdata = file_get_contents($cover->getRealPath()); $APICdata = (string) $fileManager->getFileContents($episode->cover->id3_key);
// TODO: variables used for podcast specific tags // TODO: variables used for podcast specific tags
// $podcastUrl = $episode->podcast->link; // $podcastUrl = $episode->podcast->link;
...@@ -41,24 +42,16 @@ if (! function_exists('write_audio_file_tags')) { ...@@ -41,24 +42,16 @@ if (! function_exists('write_audio_file_tags')) {
// populate data array // populate data array
$TagData = [ $TagData = [
'title' => [$episode->title], 'title' => [esc($episode->title)],
'artist' => [ 'artist' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
$episode->podcast->publisher === null 'album' => [esc($episode->podcast->title)],
? $episode->podcast->owner_name 'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''],
: $episode->podcast->publisher, 'genre' => ['Podcast'],
], 'comment' => [$episode->description],
'album' => [$episode->podcast->title], 'track_number' => [(string) $episode->number],
'year' => [$episode->published_at !== null ? $episode->published_at->format('Y') : ''],
'genre' => ['Podcast'],
'comment' => [$episode->description],
'track_number' => [(string) $episode->number],
'copyright_message' => [$episode->podcast->copyright], 'copyright_message' => [$episode->podcast->copyright],
'publisher' => [ 'publisher' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
$episode->podcast->publisher === null 'encoded_by' => ['Castopod'],
? $episode->podcast->owner_name
: $episode->podcast->publisher,
],
'encoded_by' => ['Castopod'],
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it // TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
// 'website' => [$podcast_url], // 'website' => [$podcast_url],
...@@ -71,23 +64,21 @@ if (! function_exists('write_audio_file_tags')) { ...@@ -71,23 +64,21 @@ if (! function_exists('write_audio_file_tags')) {
$TagData['attached_picture'][] = [ $TagData['attached_picture'][] = [
// picturetypeid == Cover. More: module.tag.id3v2.php // picturetypeid == Cover. More: module.tag.id3v2.php
'picturetypeid' => 2, 'picturetypeid' => 2,
'data' => $APICdata, 'data' => $APICdata,
'description' => 'cover', 'description' => 'cover',
'mime' => $cover->getMimeType(), 'mime' => $episode->cover->file_mimetype,
]; ];
$tagwriter->tag_data = $TagData; $tagwriter->tag_data = $TagData;
// write tags // write tags
if ($tagwriter->WriteTags()) { if ($tagwriter->WriteTags()) {
echo 'Successfully wrote tags<br>'; // Successfully wrote tags
if ($tagwriter->warnings !== []) { if ($tagwriter->warnings !== []) {
echo 'There were some warnings:<br>' . log_message('warning', 'There were some warnings:' . PHP_EOL . implode(PHP_EOL, $tagwriter->warnings));
implode('<br><br>', $tagwriter->warnings);
} }
} else { } else {
echo 'Failed to write tags!<br>' . log_message('critical', 'Failed to write tags!' . PHP_EOL . implode(PHP_EOL, $tagwriter->errors));
implode('<br><br>', $tagwriter->errors);
} }
} }
} }
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Mimes;
use Config\Services;
if (! function_exists('save_media')) {
/**
* Saves a file to the corresponding podcast folder in `public/media`
*/
function save_media(File | UploadedFile $file, string $folder = '', string $filename = ''): string
{
if (($extension = $file->getExtension()) !== '') {
$filename = $filename . '.' . $extension;
}
$mediaRoot = config('App')
->mediaRoot . '/' . $folder;
if (! file_exists($mediaRoot)) {
mkdir($mediaRoot, 0777, true);
}
if (! file_exists($mediaRoot . '/index.html')) {
touch($mediaRoot . '/index.html');
}
// move to media folder, overwrite file if already existing
$file->move($mediaRoot . '/', $filename, true);
return $folder . '/' . $filename;
}
}
if (! function_exists('download_file')) {
function download_file(string $fileUrl, string $mimetype = ''): File
{
$client = Services::curlrequest();
$response = $client->get($fileUrl, [
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
],
]);
// redirect to new file location
$newFileUrl = $fileUrl;
while (
in_array(
$response->getStatusCode(),
[
ResponseInterface::HTTP_MOVED_PERMANENTLY,
ResponseInterface::HTTP_FOUND,
ResponseInterface::HTTP_SEE_OTHER,
ResponseInterface::HTTP_NOT_MODIFIED,
ResponseInterface::HTTP_TEMPORARY_REDIRECT,
ResponseInterface::HTTP_PERMANENT_REDIRECT,
],
true,
)
) {
$newFileUrl = trim($response->getHeader('location')->getValue());
$response = $client->get($newFileUrl, [
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
],
'http_errors' => false,
]);
}
$fileExtension = pathinfo(parse_url($newFileUrl, PHP_URL_PATH), PATHINFO_EXTENSION);
$extension = $fileExtension === '' ? Mimes::guessExtensionFromType($mimetype) : $fileExtension;
$tmpFilename =
time() .
'_' .
bin2hex(random_bytes(10)) .
'.' .
$extension;
$tmpFilePath = WRITEPATH . 'uploads/' . $tmpFilename;
file_put_contents($tmpFilePath, $response->getBody());
return new File($tmpFilePath);
}
}
if (! function_exists('media_path')) {
/**
* Prefixes the root media path to a given uri
*
* @param string|string[] $uri URI string or array of URI segments
*/
function media_path(string | array $uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
return config('App')->mediaRoot . '/' . $uri;
}
}
if (! function_exists('media_base_url')) {
/**
* Return the media base URL to use in views
*
* @param string|string[] $uri URI string or array of URI segments
*/
function media_base_url(string | array $uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
$appConfig = config('App');
$mediaBaseUrl = $appConfig->mediaBaseURL === '' ? $appConfig->baseURL : $appConfig->mediaBaseURL;
return rtrim($mediaBaseUrl, '/') .
'/' .
$appConfig->mediaRoot .
'/' .
$uri;
}
}