refactor: remove fields from podcast and episode entities to be replaced with plugins

This commit is contained in:
Yassine Doghri 2024-12-15 17:34:36 +00:00
parent 11ccd0ebe7
commit b869acb3a9
47 changed files with 465 additions and 930 deletions

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsMediumField adds medium field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class DropDeprecatedPodcastsFields extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->dropColumn(
'podcasts',
'episode_description_footer_markdown,episode_description_footer_html,is_owner_email_removed_from_feed,medium,payment_pointer,verify_txt,custom_rss,partner_id,partner_link_url,partner_image_url'
);
}
#[Override]
public function down(): void
{
$fields = [
'episode_description_footer_markdown' => [
'type' => 'TEXT',
'null' => true,
],
'episode_description_footer_html' => [
'type' => 'TEXT',
'null' => true,
],
'is_owner_email_removed_from_feed' => [
'type' => 'BOOLEAN',
'null' => false,
'default' => 0,
'after' => 'owner_email',
],
'medium' => [
'type' => "ENUM('podcast','music','audiobook')",
'null' => false,
'default' => 'podcast',
'after' => 'type',
],
'payment_pointer' => [
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'verify_txt' => [
'type' => 'TEXT',
'null' => true,
'after' => 'location_osm',
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
];
$this->forge->addColumn('podcasts', $fields);
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsMediumField adds medium field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class DropDeprecatedEpisodesFields extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->dropColumn('episodes', 'custom_rss');
}
#[Override]
public function down(): void
{
$fields = [
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
];
$this->forge->addColumn('episodes', $fields);
}
}

View File

@ -11,7 +11,6 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
use App\Entities\Clip\Soundbite; use App\Entities\Clip\Soundbite;
use App\Libraries\SimpleRSSElement;
use App\Models\ClipModel; use App\Models\ClipModel;
use App\Models\EpisodeCommentModel; use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
@ -29,14 +28,13 @@ use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension; use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension; use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter; use League\CommonMark\MarkdownConverter;
use Modules\Analytics\OP3;
use Modules\Media\Entities\Audio; use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Chapters; use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Image; use Modules\Media\Entities\Image;
use Modules\Media\Entities\Transcript; use Modules\Media\Entities\Transcript;
use Modules\Media\Models\MediaModel; use Modules\Media\Models\MediaModel;
use Override;
use RuntimeException; use RuntimeException;
use SimpleXMLElement;
/** /**
* @property int $id * @property int $id
@ -73,8 +71,6 @@ use SimpleXMLElement;
* @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 string $custom_rss_string
* @property bool $is_published_on_hubs * @property bool $is_published_on_hubs
* @property int $posts_count * @property int $posts_count
* @property int $comments_count * @property int $comments_count
@ -94,19 +90,19 @@ use SimpleXMLElement;
*/ */
class Episode extends Entity class Episode extends Entity
{ {
protected Podcast $podcast; public string $link = '';
protected string $link; public string $audio_url = '';
public string $audio_web_url = '';
public string $audio_opengraph_url = '';
protected Podcast $podcast;
protected ?Audio $audio = null; protected ?Audio $audio = null;
protected string $audio_url; protected string $embed_url = '';
protected string $audio_web_url;
protected string $audio_opengraph_url;
protected string $embed_url;
protected ?Image $cover = null; protected ?Image $cover = null;
@ -140,8 +136,6 @@ 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;
/** /**
@ -176,7 +170,6 @@ class Episode extends Entity
'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', 'is_published_on_hubs' => 'boolean',
'posts_count' => 'integer', 'posts_count' => 'integer',
'comments_count' => 'integer', 'comments_count' => 'integer',
@ -185,6 +178,31 @@ class Episode extends Entity
'updated_by' => 'integer', 'updated_by' => 'integer',
]; ];
/**
* @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 $file = null): self public function setCover(UploadedFile | File $file = null): self
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
@ -342,40 +360,6 @@ class Episode extends Entity
return $this->chapters; return $this->chapters;
} }
public function getAudioUrl(): string
{
$audioURL = url_to(
'episode-audio',
$this->getPodcast()
->handle,
$this->slug,
$this->getAudio()
->file_extension
);
// Wrap episode url with OP3 if episode is public and OP3 is enabled on this podcast
if (! $this->is_premium && service('settings')->get(
'Analytics.enableOP3',
'podcast:' . $this->podcast_id
)) {
$op3 = new OP3(config('Analytics')->OP3);
return $op3->wrap($audioURL, $this);
}
return $audioURL;
}
public function getAudioWebUrl(): string
{
return $this->getAudioUrl() . '?_from=-+Website+-';
}
public function getAudioOpengraphUrl(): string
{
return $this->getAudioUrl() . '?_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.
*/ */
@ -468,11 +452,6 @@ class Episode extends Entity
return $this->comments; return $this->comments;
} }
public function getLink(): string
{
return url_to('episode', esc($this->getPodcast()->handle), esc($this->attributes['slug']));
}
public function getEmbedUrl(string $theme = null): string public function getEmbedUrl(string $theme = null): string
{ {
return $theme return $theme
@ -482,7 +461,7 @@ class Episode extends Entity
public function setGuid(?string $guid = null): static public function setGuid(?string $guid = null): static
{ {
$this->attributes['guid'] = $guid ?? $this->getLink(); $this->attributes['guid'] = $guid ?? $this->link;
return $this; return $this;
} }
@ -513,34 +492,6 @@ class Episode extends Entity
return $this; return $this;
} }
public function getDescriptionHtml(?string $serviceSlug = null): string
{
$descriptionHtml = '';
if (
$this->getPodcast()
->partner_id !== null &&
$this->getPodcast()
->partner_link_url !== null &&
$this->getPodcast()
->partner_image_url !== null
) {
$descriptionHtml .= "<div><a href=\"{$this->getPartnerLink(
$serviceSlug,
)}\" rel=\"sponsored noopener noreferrer\" target=\"_blank\"><img src=\"{$this->getPartnerImageUrl(
$serviceSlug,
)}\" alt=\"Partner image\" /></a></div>";
}
$descriptionHtml .= $this->attributes['description_html'];
if ($this->getPodcast()->episode_description_footer_html) {
$descriptionHtml .= "<footer>{$this->getPodcast()
->episode_description_footer_html}</footer>";
}
return $descriptionHtml;
}
public function getDescription(): string public function getDescription(): string
{ {
if ($this->description === null) { if ($this->description === null) {
@ -609,91 +560,6 @@ class Episode extends Entity
return $this->location; return $this->location;
} }
/**
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string
{
if ($this->custom_rss === null) {
return '';
}
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://podcastindex.org/namespace/1.0" 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>'], '', (string) $xmlNode->asXML());
}
/**
* Saves custom rss tag into json
*/
public function setCustomRssString(?string $customRssString = null): static
{
if ($customRssString === '') {
$this->attributes['custom_rss'] = null;
return $this;
}
helper('rss');
$customXML = 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://podcastindex.org/namespace/1.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><item>' .
$customRssString .
'</item></channel></rss>',
);
if (! $customXML instanceof SimpleXMLElement) {
// TODO: Failed to parse custom xml, should return error?
return $this;
}
$customRssArray = rss_to_array($customXML)['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((string) $this->getPodcast()->partner_link_url, '/') .
'?pid=' .
$this->getPodcast()
->partner_id .
'&guid=' .
urlencode((string) $this->attributes['guid']);
if ($serviceSlug !== null) {
$partnerLink .= '&_from=' . $serviceSlug;
}
return $partnerLink;
}
public function getPartnerImageUrl(string $serviceSlug = null): string
{
return rtrim((string) $this->getPodcast()->partner_image_url, '/') .
'?pid=' .
$this->getPodcast()
->partner_id .
'&guid=' .
urlencode((string) $this->attributes['guid']) .
($serviceSlug !== null ? '&_from=' . $serviceSlug : '');
}
public function getPreviewLink(): string public function getPreviewLink(): string
{ {
if ($this->preview_id === null) { if ($this->preview_id === null) {

View File

@ -10,7 +10,6 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
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;
@ -62,12 +61,8 @@ use RuntimeException;
* @property string|null $publisher * @property string|null $publisher
* @property string $owner_name * @property string $owner_name
* @property string $owner_email * @property string $owner_email
* @property bool $is_owner_email_removed_from_feed
* @property string $type * @property string $type
* @property string $medium
* @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
@ -77,15 +72,7 @@ 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 array|null $custom_rss
* @property bool $is_op3_enabled
* @property string $op3_url
* @property string $custom_rss_string
* @property bool $is_published_on_hubs * @property bool $is_published_on_hubs
* @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 string $publication_status * @property string $publication_status
@ -166,8 +153,6 @@ class Podcast 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;
/** /**
@ -180,44 +165,35 @@ class Podcast extends Entity
* @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',
'is_owner_email_removed_from_feed' => 'boolean', 'type' => 'string',
'type' => 'string', 'copyright' => '?string',
'medium' => 'string', 'is_blocked' => 'boolean',
'copyright' => '?string', 'is_completed' => 'boolean',
'episode_description_footer_markdown' => '?string', 'is_locked' => 'boolean',
'episode_description_footer_html' => '?string', 'is_premium_by_default' => 'boolean',
'is_blocked' => 'boolean', 'imported_feed_url' => '?string',
'is_completed' => 'boolean', 'new_feed_url' => '?string',
'is_locked' => 'boolean', 'location_name' => '?string',
'is_premium_by_default' => 'boolean', 'location_geo' => '?string',
'imported_feed_url' => '?string', 'location_osm' => '?string',
'new_feed_url' => '?string', 'is_published_on_hubs' => 'boolean',
'location_name' => '?string', 'created_by' => 'integer',
'location_geo' => '?string', 'updated_by' => 'integer',
'location_osm' => '?string',
'payment_pointer' => '?string',
'custom_rss' => '?json-array',
'is_published_on_hubs' => 'boolean',
'partner_id' => '?string',
'partner_link_url' => '?string',
'partner_image_url' => '?string',
'created_by' => 'integer',
'updated_by' => 'integer',
]; ];
public function getAtHandle(): string public function getAtHandle(): string
@ -454,42 +430,6 @@ class Podcast extends Entity
return $this; return $this;
} }
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;
}
$config = [
'html_input' => 'escape',
'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[
'episode_description_footer_markdown'
] = $episodeDescriptionFooterMarkdown;
$this->attributes[
'episode_description_footer_html'
] = $converter->convert($episodeDescriptionFooterMarkdown);
return $this;
}
public function getDescription(): string public function getDescription(): string
{ {
if ($this->description === null) { if ($this->description === null) {
@ -638,68 +578,9 @@ class Podcast extends Entity
return $this->location; return $this->location;
} }
/**
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string
{
if ($this->attributes['custom_rss'] === null) {
return '';
}
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://podcastindex.org/namespace/1.0" 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>'], '', (string) $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://podcastindex.org/namespace/1.0" 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;
}
public function getIsPremium(): bool public function getIsPremium(): bool
{ {
// podcast is premium if at least one of its episodes is set as premium // podcast is premium if at least one of its episodes is set as premium
return (new EpisodeModel())->doesPodcastHavePremiumEpisodes($this->id); return (new EpisodeModel())->doesPodcastHavePremiumEpisodes($this->id);
} }
public function getIsOp3Enabled(): bool
{
return service('settings')->get('Analytics.enableOP3', 'podcast:' . $this->id);
}
public function getOp3Url(): string
{
return 'https://op3.dev/show/' . $this->guid;
}
} }

View File

@ -10,7 +10,7 @@ declare(strict_types=1);
use App\Entities\Category; use App\Entities\Category;
use App\Entities\Location; use App\Entities\Location;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement; use App\Libraries\RssFeed;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Config\Mimes; use Config\Mimes;
@ -37,21 +37,13 @@ if (! function_exists('get_rss_feed')) {
$episodes = $podcast->episodes; $episodes = $podcast->episodes;
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; $rss = new RssFeed();
$podcastNamespace = 'https://podcastindex.org/namespace/1.0';
$atomNamespace = 'http://www.w3.org/2005/Atom';
$rss = new SimpleRSSElement(
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='{$itunesNamespace}' xmlns:podcast='{$podcastNamespace}' xmlns:atom='{$atomNamespace}' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>"
);
$plugins->rssBeforeChannel($podcast); $plugins->rssBeforeChannel($podcast);
$channel = $rss->addChild('channel'); $channel = $rss->addChild('channel');
$atomLink = $channel->addChild('link', null, $atomNamespace); $atomLink = $channel->addChild('link', null, RssFeed::ATOM_NAMESPACE);
$atomLink->addAttribute('href', $podcast->feed_url); $atomLink->addAttribute('href', $podcast->feed_url);
$atomLink->addAttribute('rel', 'self'); $atomLink->addAttribute('rel', 'self');
$atomLink->addAttribute('type', 'application/rss+xml'); $atomLink->addAttribute('type', 'application/rss+xml');
@ -60,14 +52,14 @@ if (! function_exists('get_rss_feed')) {
$websubHubs = config('WebSub') $websubHubs = config('WebSub')
->hubs; ->hubs;
foreach ($websubHubs as $websubHub) { foreach ($websubHubs as $websubHub) {
$atomLinkHub = $channel->addChild('link', null, $atomNamespace); $atomLinkHub = $channel->addChild('link', null, RssFeed::ATOM_NAMESPACE);
$atomLinkHub->addAttribute('href', $websubHub); $atomLinkHub->addAttribute('href', $websubHub);
$atomLinkHub->addAttribute('rel', 'hub'); $atomLinkHub->addAttribute('rel', 'hub');
$atomLinkHub->addAttribute('type', 'application/rss+xml'); $atomLinkHub->addAttribute('type', 'application/rss+xml');
} }
if ($podcast->new_feed_url !== null) { if ($podcast->new_feed_url !== null) {
$channel->addChild('new-feed-url', $podcast->new_feed_url, $itunesNamespace); $channel->addChild('new-feed-url', $podcast->new_feed_url, RssFeed::ITUNES_NAMESPACE);
} }
// the last build date corresponds to the creation of the feed.xml cache // the last build date corresponds to the creation of the feed.xml cache
@ -85,19 +77,17 @@ if (! function_exists('get_rss_feed')) {
(new PodcastModel())->save($podcast); (new PodcastModel())->save($podcast);
} }
$channel->addChild('guid', $podcast->guid, $podcastNamespace); $channel->addChild('guid', $podcast->guid, RssFeed::PODCAST_NAMESPACE);
$channel->addChild('title', $podcast->title, null, false); $channel->addChild('title', $podcast->title, null, false);
$channel->addChildWithCDATA('description', $podcast->description_html); $channel->addChildWithCDATA('description', $podcast->description_html);
$channel->addChild('medium', $podcast->medium, $podcastNamespace); $itunesImage = $channel->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
$itunesImage = $channel->addChild('image', null, $itunesNamespace);
$itunesImage->addAttribute('href', $podcast->cover->feed_url); $itunesImage->addAttribute('href', $podcast->cover->feed_url);
$channel->addChild('language', $podcast->language_code); $channel->addChild('language', $podcast->language_code);
if ($podcast->location instanceof Location) { if ($podcast->location instanceof Location) {
$locationElement = $channel->addChild('location', $podcast->location->name, $podcastNamespace); $locationElement = $channel->addChild('location', $podcast->location->name, RssFeed::PODCAST_NAMESPACE);
if ($podcast->location->geo !== null) { if ($podcast->location->geo !== null) {
$locationElement->addAttribute('geo', $podcast->location->geo); $locationElement->addAttribute('geo', $podcast->location->geo);
} }
@ -107,38 +97,16 @@ if (! function_exists('get_rss_feed')) {
} }
} }
if ($podcast->payment_pointer !== null) { $channel
$valueElement = $channel->addChild('value', null, $podcastNamespace); ->addChild('locked', $podcast->is_locked ? 'yes' : 'no', RssFeed::PODCAST_NAMESPACE)
$valueElement->addAttribute('type', 'webmonetization'); ->addAttribute('owner', $podcast->owner_email);
$valueElement->addAttribute('method', 'ILP');
$recipientElement = $valueElement->addChild('valueRecipient', null, $podcastNamespace);
$recipientElement->addAttribute('name', $podcast->owner_name);
$recipientElement->addAttribute('type', 'paymentpointer');
$recipientElement->addAttribute('address', $podcast->payment_pointer);
$recipientElement->addAttribute('split', '100');
}
if ($podcast->is_owner_email_removed_from_feed) {
$channel
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', $podcastNamespace);
} else {
$channel
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', $podcastNamespace)
->addAttribute('owner', $podcast->owner_email);
}
if ($podcast->verify_txt !== null) {
$channel
->addChild('txt', $podcast->verify_txt, $podcastNamespace)
->addAttribute('purpose', 'verify');
}
if ($podcast->imported_feed_url !== null) { if ($podcast->imported_feed_url !== null) {
$channel->addChild('previousUrl', $podcast->imported_feed_url, $podcastNamespace); $channel->addChild('previousUrl', $podcast->imported_feed_url, RssFeed::PODCAST_NAMESPACE);
} }
foreach ($podcast->podcasting_platforms as $podcastingPlatform) { foreach ($podcast->podcasting_platforms as $podcastingPlatform) {
$podcastingPlatformElement = $channel->addChild('id', null, $podcastNamespace); $podcastingPlatformElement = $channel->addChild('id', null, RssFeed::PODCAST_NAMESPACE);
$podcastingPlatformElement->addAttribute('platform', $podcastingPlatform->slug); $podcastingPlatformElement->addAttribute('platform', $podcastingPlatform->slug);
if ($podcastingPlatform->account_id !== null) { if ($podcastingPlatform->account_id !== null) {
$podcastingPlatformElement->addAttribute('id', $podcastingPlatform->account_id); $podcastingPlatformElement->addAttribute('id', $podcastingPlatform->account_id);
@ -149,7 +117,7 @@ if (! function_exists('get_rss_feed')) {
} }
} }
$castopodSocialElement = $channel->addChild('social', null, $podcastNamespace); $castopodSocialElement = $channel->addChild('social', null, RssFeed::PODCAST_NAMESPACE);
$castopodSocialElement->addAttribute('priority', '1'); $castopodSocialElement->addAttribute('priority', '1');
$castopodSocialElement->addAttribute('platform', 'castopod'); $castopodSocialElement->addAttribute('platform', 'castopod');
$castopodSocialElement->addAttribute('protocol', 'activitypub'); $castopodSocialElement->addAttribute('protocol', 'activitypub');
@ -157,7 +125,7 @@ if (! function_exists('get_rss_feed')) {
$castopodSocialElement->addAttribute('accountUrl', $podcast->link); $castopodSocialElement->addAttribute('accountUrl', $podcast->link);
foreach ($podcast->social_platforms as $socialPlatform) { foreach ($podcast->social_platforms as $socialPlatform) {
$socialElement = $channel->addChild('social', null, $podcastNamespace); $socialElement = $channel->addChild('social', null, RssFeed::PODCAST_NAMESPACE);
$socialElement->addAttribute('priority', '2'); $socialElement->addAttribute('priority', '2');
$socialElement->addAttribute('platform', $socialPlatform->slug); $socialElement->addAttribute('platform', $socialPlatform->slug);
@ -181,7 +149,7 @@ if (! function_exists('get_rss_feed')) {
} }
if ($socialPlatform->slug === 'mastodon') { if ($socialPlatform->slug === 'mastodon') {
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, $podcastNamespace); $socialSignUpelement = $socialElement->addChild('socialSignUp', null, RssFeed::PODCAST_NAMESPACE);
$socialSignUpelement->addAttribute('priority', '1'); $socialSignUpelement->addAttribute('priority', '1');
$socialSignUpelement->addAttribute( $socialSignUpelement->addAttribute(
'homeUrl', 'homeUrl',
@ -200,7 +168,7 @@ if (! function_exists('get_rss_feed')) {
$castopodSocialSignUpelement = $castopodSocialElement->addChild( $castopodSocialSignUpelement = $castopodSocialElement->addChild(
'socialSignUp', 'socialSignUp',
null, null,
$podcastNamespace RssFeed::PODCAST_NAMESPACE
); );
$castopodSocialSignUpelement->addAttribute('priority', '1'); $castopodSocialSignUpelement->addAttribute('priority', '1');
$castopodSocialSignUpelement->addAttribute( $castopodSocialSignUpelement->addAttribute(
@ -224,7 +192,7 @@ if (! function_exists('get_rss_feed')) {
$fundingPlatformElement = $channel->addChild( $fundingPlatformElement = $channel->addChild(
'funding', 'funding',
$fundingPlatform->account_id, $fundingPlatform->account_id,
$podcastNamespace, RssFeed::PODCAST_NAMESPACE,
); );
$fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug); $fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug);
if ($fundingPlatform->link_url !== null) { if ($fundingPlatform->link_url !== null) {
@ -234,7 +202,7 @@ if (! function_exists('get_rss_feed')) {
foreach ($podcast->persons as $person) { foreach ($podcast->persons as $person) {
foreach ($person->roles as $role) { foreach ($person->roles as $role) {
$personElement = $channel->addChild('person', $person->full_name, $podcastNamespace); $personElement = $channel->addChild('person', $person->full_name, RssFeed::PODCAST_NAMESPACE);
$personElement->addAttribute('img', get_avatar_url($person, 'medium')); $personElement->addAttribute('img', get_avatar_url($person, 'medium'));
@ -263,29 +231,26 @@ if (! function_exists('get_rss_feed')) {
$channel->addChild( $channel->addChild(
'explicit', 'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false', $podcast->parental_advisory === 'explicit' ? 'true' : 'false',
$itunesNamespace, RssFeed::ITUNES_NAMESPACE,
); );
$channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, $itunesNamespace, false); $channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
$channel->addChild('link', $podcast->link); $channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunesNamespace); $owner = $channel->addChild('owner', null, RssFeed::ITUNES_NAMESPACE);
$owner->addChild('name', $podcast->owner_name, $itunesNamespace, false); $owner->addChild('name', $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
$owner->addChild('email', $podcast->owner_email, RssFeed::ITUNES_NAMESPACE);
if (! $podcast->is_owner_email_removed_from_feed) { $channel->addChild('type', $podcast->type, RssFeed::ITUNES_NAMESPACE);
$owner->addChild('email', $podcast->owner_email, $itunesNamespace);
}
$channel->addChild('type', $podcast->type, $itunesNamespace);
$podcast->copyright && $podcast->copyright &&
$channel->addChild('copyright', $podcast->copyright); $channel->addChild('copyright', $podcast->copyright);
if ($podcast->is_blocked || $subscription instanceof Subscription) { if ($podcast->is_blocked || $subscription instanceof Subscription) {
$channel->addChild('block', 'Yes', $itunesNamespace); $channel->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
} }
if ($podcast->is_completed) { if ($podcast->is_completed) {
$channel->addChild('complete', 'Yes', $itunesNamespace); $channel->addChild('complete', 'Yes', RssFeed::ITUNES_NAMESPACE);
} }
$image = $channel->addChild('image'); $image = $channel->addChild('image');
@ -293,12 +258,6 @@ if (! function_exists('get_rss_feed')) {
$image->addChild('title', $podcast->title, null, false); $image->addChild('title', $podcast->title, null, false);
$image->addChild('link', $podcast->link); $image->addChild('link', $podcast->link);
if ($podcast->custom_rss !== null) {
array_to_rss([
'elements' => $podcast->custom_rss,
], $channel);
}
// run plugins hook at the end // run plugins hook at the end
$plugins->rssAfterChannel($podcast, $channel); $plugins->rssAfterChannel($podcast, $channel);
@ -328,7 +287,7 @@ if (! function_exists('get_rss_feed')) {
$item->addChild('guid', $episode->guid); $item->addChild('guid', $episode->guid);
$item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123)); $item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123));
if ($episode->location instanceof Location) { if ($episode->location instanceof Location) {
$locationElement = $item->addChild('location', $episode->location->name, $podcastNamespace); $locationElement = $item->addChild('location', $episode->location->name, RssFeed::PODCAST_NAMESPACE);
if ($episode->location->geo !== null) { if ($episode->location->geo !== null) {
$locationElement->addAttribute('geo', $episode->location->geo); $locationElement->addAttribute('geo', $episode->location->geo);
} }
@ -338,10 +297,10 @@ if (! function_exists('get_rss_feed')) {
} }
} }
$item->addChildWithCDATA('description', $episode->getDescriptionHtml($serviceSlug)); $item->addChildWithCDATA('description', $episode->description_html);
$item->addChild('duration', (string) round($episode->audio->duration), $itunesNamespace); $item->addChild('duration', (string) round($episode->audio->duration), RssFeed::ITUNES_NAMESPACE);
$item->addChild('link', $episode->link); $item->addChild('link', $episode->link);
$episodeItunesImage = $item->addChild('image', null, $itunesNamespace); $episodeItunesImage = $item->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
$episodeItunesImage->addAttribute('href', $episode->cover->feed_url); $episodeItunesImage->addAttribute('href', $episode->cover->feed_url);
$episode->parental_advisory && $episode->parental_advisory &&
@ -350,18 +309,18 @@ if (! function_exists('get_rss_feed')) {
$episode->parental_advisory === 'explicit' $episode->parental_advisory === 'explicit'
? 'true' ? 'true'
: 'false', : 'false',
$itunesNamespace, RssFeed::ITUNES_NAMESPACE,
); );
$episode->number && $episode->number &&
$item->addChild('episode', (string) $episode->number, $itunesNamespace); $item->addChild('episode', (string) $episode->number, RssFeed::ITUNES_NAMESPACE);
$episode->season_number && $episode->season_number &&
$item->addChild('season', (string) $episode->season_number, $itunesNamespace); $item->addChild('season', (string) $episode->season_number, RssFeed::ITUNES_NAMESPACE);
$item->addChild('episodeType', $episode->type, $itunesNamespace); $item->addChild('episodeType', $episode->type, RssFeed::ITUNES_NAMESPACE);
// If episode is of type trailer, add podcast:trailer tag on channel level // If episode is of type trailer, add podcast:trailer tag on channel level
if ($episode->type === 'trailer') { if ($episode->type === 'trailer') {
$trailer = $channel->addChild('trailer', $episode->title, $podcastNamespace); $trailer = $channel->addChild('trailer', $episode->title, RssFeed::PODCAST_NAMESPACE);
$trailer->addAttribute('pubdate', $episode->published_at->format(DATE_RFC2822)); $trailer->addAttribute('pubdate', $episode->published_at->format(DATE_RFC2822));
$trailer->addAttribute( $trailer->addAttribute(
'url', 'url',
@ -374,21 +333,15 @@ if (! function_exists('get_rss_feed')) {
} }
} }
// add podcast namespace tags for season and episode
$episode->season_number &&
$item->addChild('season', (string) $episode->season_number, $podcastNamespace);
$episode->number &&
$item->addChild('episode', (string) $episode->number, $podcastNamespace);
// add link to episode comments as podcast-activity format // add link to episode comments as podcast-activity format
$comments = $item->addChild('comments', null, $podcastNamespace); $comments = $item->addChild('comments', null, RssFeed::PODCAST_NAMESPACE);
$comments->addAttribute('uri', url_to('episode-comments', $podcast->handle, $episode->slug)); $comments->addAttribute('uri', url_to('episode-comments', $podcast->handle, $episode->slug));
$comments->addAttribute('contentType', 'application/podcast-activity+json'); $comments->addAttribute('contentType', 'application/podcast-activity+json');
if ($episode->getPosts()) { if ($episode->getPosts()) {
$socialInteractUri = $episode->getPosts()[0] $socialInteractUri = $episode->getPosts()[0]
->uri; ->uri;
$socialInteractElement = $item->addChild('socialInteract', null, $podcastNamespace); $socialInteractElement = $item->addChild('socialInteract', null, RssFeed::PODCAST_NAMESPACE);
$socialInteractElement->addAttribute('uri', $socialInteractUri); $socialInteractElement->addAttribute('uri', $socialInteractUri);
$socialInteractElement->addAttribute('priority', '1'); $socialInteractElement->addAttribute('priority', '1');
$socialInteractElement->addAttribute('platform', 'castopod'); $socialInteractElement->addAttribute('platform', 'castopod');
@ -405,7 +358,7 @@ if (! function_exists('get_rss_feed')) {
} }
if ($episode->transcript instanceof Transcript) { if ($episode->transcript instanceof Transcript) {
$transcriptElement = $item->addChild('transcript', null, $podcastNamespace); $transcriptElement = $item->addChild('transcript', null, RssFeed::PODCAST_NAMESPACE);
$transcriptElement->addAttribute('url', $episode->transcript->file_url); $transcriptElement->addAttribute('url', $episode->transcript->file_url);
$transcriptElement->addAttribute( $transcriptElement->addAttribute(
'type', 'type',
@ -420,21 +373,21 @@ if (! function_exists('get_rss_feed')) {
} }
if ($episode->getChapters() instanceof Chapters) { if ($episode->getChapters() instanceof Chapters) {
$chaptersElement = $item->addChild('chapters', null, $podcastNamespace); $chaptersElement = $item->addChild('chapters', null, RssFeed::PODCAST_NAMESPACE);
$chaptersElement->addAttribute('url', $episode->chapters->file_url); $chaptersElement->addAttribute('url', $episode->chapters->file_url);
$chaptersElement->addAttribute('type', 'application/json+chapters'); $chaptersElement->addAttribute('type', 'application/json+chapters');
} }
foreach ($episode->soundbites as $soundbite) { foreach ($episode->soundbites as $soundbite) {
// TODO: differentiate video from soundbites? // TODO: differentiate video from soundbites?
$soundbiteElement = $item->addChild('soundbite', $soundbite->title, $podcastNamespace); $soundbiteElement = $item->addChild('soundbite', $soundbite->title, RssFeed::PODCAST_NAMESPACE);
$soundbiteElement->addAttribute('startTime', (string) $soundbite->start_time); $soundbiteElement->addAttribute('startTime', (string) $soundbite->start_time);
$soundbiteElement->addAttribute('duration', (string) round($soundbite->duration, 3)); $soundbiteElement->addAttribute('duration', (string) round($soundbite->duration, 3));
} }
foreach ($episode->persons as $person) { foreach ($episode->persons as $person) {
foreach ($person->roles as $role) { foreach ($person->roles as $role) {
$personElement = $item->addChild('person', esc($person->full_name), $podcastNamespace); $personElement = $item->addChild('person', esc($person->full_name), RssFeed::PODCAST_NAMESPACE);
$personElement->addAttribute( $personElement->addAttribute(
'role', 'role',
@ -455,13 +408,7 @@ if (! function_exists('get_rss_feed')) {
} }
if ($episode->is_blocked) { if ($episode->is_blocked) {
$item->addChild('block', 'Yes', $itunesNamespace); $item->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
if ($episode->custom_rss !== null) {
array_to_rss([
'elements' => $episode->custom_rss,
], $item);
} }
$plugins->rssAfterItem($episode, $item); $plugins->rssAfterItem($episode, $item);
@ -477,9 +424,7 @@ if (! function_exists('add_category_tag')) {
*/ */
function add_category_tag(SimpleXMLElement $node, Category $category): void function add_category_tag(SimpleXMLElement $node, Category $category): void
{ {
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; $itunesCategory = $node->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
$itunesCategory = $node->addChild('category', null, $itunesNamespace);
$itunesCategory->addAttribute( $itunesCategory->addAttribute(
'text', 'text',
$category->parent instanceof Category $category->parent instanceof Category
@ -488,7 +433,7 @@ if (! function_exists('add_category_tag')) {
); );
if ($category->parent instanceof Category) { if ($category->parent instanceof Category) {
$itunesCategoryChild = $itunesCategory->addChild('category', null, $itunesNamespace); $itunesCategoryChild = $itunesCategory->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
$itunesCategoryChild->addAttribute('text', $category->apple_category); $itunesCategoryChild->addAttribute('text', $category->apple_category);
$node->addChild('category', $category->parent->apple_category); $node->addChild('category', $category->parent->apple_category);
} }
@ -535,9 +480,9 @@ if (! function_exists('array_to_rss')) {
* Inserts array (converted to XML node) in XML node * Inserts array (converted to XML node) in XML node
* *
* @param array<string, mixed> $arrayNode * @param array<string, mixed> $arrayNode
* @param SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached * @param RssFeed $xmlNode The XML parent node where this arrayNode should be attached
*/ */
function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode): SimpleRSSElement function array_to_rss(array $arrayNode, RssFeed &$xmlNode): RssFeed
{ {
if (array_key_exists('elements', $arrayNode)) { if (array_key_exists('elements', $arrayNode)) {
foreach ($arrayNode['elements'] as $childArrayNode) { foreach ($arrayNode['elements'] as $childArrayNode) {

View File

@ -65,10 +65,6 @@ if (! function_exists('get_podcast_metatags')) {
'href' => url_to('podcast-activity', esc($podcast->handle)), 'href' => url_to('podcast-activity', esc($podcast->handle)),
]); ]);
if ($podcast->payment_pointer) {
$metatags->meta('monetization', $podcast->payment_pointer);
}
return '<link type="application/rss+xml" rel="alternate" title="' . esc( return '<link type="application/rss+xml" rel="alternate" title="' . esc(
$podcast->title $podcast->title
) . '" href="' . $podcast->feed_url . '" />' . PHP_EOL . $metatags->__toString() . PHP_EOL . $schema->__toString(); ) . '" href="' . $podcast->feed_url . '" />' . PHP_EOL . $metatags->__toString() . PHP_EOL . $schema->__toString();
@ -123,10 +119,6 @@ if (! function_exists('get_episode_metatags')) {
'href' => url_to('episode', $episode->podcast->handle, $episode->slug), 'href' => url_to('episode', $episode->podcast->handle, $episode->slug),
]); ]);
if ($episode->podcast->payment_pointer) {
$metatags->meta('monetization', $episode->podcast->payment_pointer);
}
return $metatags->__toString() . PHP_EOL . '<link rel="alternate" type="application/json+oembed" href="' . base_url( return $metatags->__toString() . PHP_EOL . '<link rel="alternate" type="application/json+oembed" href="' . base_url(
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug) route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug)
) . '" title="' . esc( ) . '" title="' . esc(

View File

@ -14,8 +14,31 @@ use DOMDocument;
use Override; use Override;
use SimpleXMLElement; use SimpleXMLElement;
class SimpleRSSElement extends SimpleXMLElement class RssFeed extends SimpleXMLElement
{ {
public const ATOM_NS = 'atom';
public const ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom';
public const ITUNES_NS = 'itunes';
public const ITUNES_NAMESPACE = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
public const PODCAST_NS = 'podcast';
public const PODCAST_NAMESPACE = 'https://podcastindex.org/namespace/1.0';
public function __construct(string $contents = '')
{
parent::__construct(sprintf(
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:atom='%s' xmlns:itunes='%s' xmlns:podcast='%s' xmlns:content='http://purl.org/rss/1.0/modules/content/'>%s</rss>",
$this::ATOM_NAMESPACE,
$this::ITUNES_NAMESPACE,
$this::PODCAST_NAMESPACE,
$contents
));
}
/** /**
* Adds a child with $value inside CDATA * Adds a child with $value inside CDATA
* *
@ -67,4 +90,37 @@ class SimpleRSSElement extends SimpleXMLElement
return $newChild; return $newChild;
} }
/**
* Add RssFeed code into a RssFeed
*
* adapted from: https://stackoverflow.com/a/23527002
*
* @param self|array<self> $nodes
*/
public function appendNodes(self|array $nodes): void
{
if (! is_array($nodes)) {
$nodes = [$nodes];
}
foreach ($nodes as $element) {
$namespaces = $element->getNamespaces();
$namespace = $namespaces[array_key_first($namespaces)] ?? null;
if (trim((string) $element) === '') {
$simpleRSS = $this->addChild($element->getName(), null, $namespace);
} else {
$simpleRSS = $this->addChild($element->getName(), (string) $element, $namespace);
}
foreach ($element->children() as $child) {
$simpleRSS->appendNodes($child);
}
foreach ($element->attributes() as $name => $value) {
$simpleRSS->addAttribute($name, (string) $value);
}
}
}
} }

View File

@ -87,7 +87,6 @@ class EpisodeModel extends UuidModel
'location_name', 'location_name',
'location_geo', 'location_geo',
'location_osm', 'location_osm',
'custom_rss',
'is_published_on_hubs', 'is_published_on_hubs',
'posts_count', 'posts_count',
'comments_count', 'comments_count',

View File

@ -38,8 +38,6 @@ class PodcastModel extends Model
'handle', 'handle',
'description_markdown', 'description_markdown',
'description_html', 'description_html',
'episode_description_footer_markdown',
'episode_description_footer_html',
'cover_id', 'cover_id',
'banner_id', 'banner_id',
'language_code', 'language_code',
@ -47,10 +45,8 @@ class PodcastModel extends Model
'parental_advisory', 'parental_advisory',
'owner_name', 'owner_name',
'owner_email', 'owner_email',
'is_owner_email_removed_from_feed',
'publisher', 'publisher',
'type', 'type',
'medium',
'copyright', 'copyright',
'imported_feed_url', 'imported_feed_url',
'new_feed_url', 'new_feed_url',
@ -60,13 +56,7 @@ class PodcastModel extends Model
'location_name', 'location_name',
'location_geo', 'location_geo',
'location_osm', 'location_osm',
'verify_txt',
'payment_pointer',
'custom_rss',
'is_published_on_hubs', 'is_published_on_hubs',
'partner_id',
'partner_link_url',
'partner_image_url',
'is_premium_by_default', 'is_premium_by_default',
'published_at', 'published_at',
'created_by', 'created_by',

View File

@ -5,7 +5,7 @@ import {
syntaxHighlighting, syntaxHighlighting,
} from "@codemirror/language"; } from "@codemirror/language";
import { Compartment, EditorState } from "@codemirror/state"; import { Compartment, EditorState } from "@codemirror/state";
import { keymap } from "@codemirror/view"; import { keymap, ViewUpdate } from "@codemirror/view";
import { basicSetup, EditorView } from "codemirror"; import { basicSetup, EditorView } from "codemirror";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, queryAssignedNodes, state } from "lit/decorators.js"; import { customElement, queryAssignedNodes, state } from "lit/decorators.js";
@ -63,6 +63,12 @@ export class XMLEditor extends LitElement {
language.of(xml()), language.of(xml()),
minHeightEditor, minHeightEditor,
syntaxHighlighting(defaultHighlightStyle), syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged) {
// Document changed, update textarea value
this._textarea[0].value = viewUpdate.state.doc.toString();
}
}),
], ],
}); });
@ -72,20 +78,11 @@ export class XMLEditor extends LitElement {
parent: this.shadowRoot as ShadowRoot, parent: this.shadowRoot as ShadowRoot,
}); });
this._textarea[0].hidden = true; // hide textarea
if (this._textarea[0].form) { this._textarea[0].style.position = "absolute";
this._textarea[0].form.addEventListener("submit", () => { this._textarea[0].style.opacity = "0";
this._textarea[0].value = this.editorView.state.doc.toString(); this._textarea[0].style.zIndex = "-9999";
}); this._textarea[0].style.pointerEvents = "none";
}
}
disconnectedCallback(): void {
if (this._textarea[0].form) {
this._textarea[0].form.removeEventListener("submit", () => {
this._textarea[0].value = this.editorView.state.doc.toString();
});
}
} }
static styles = css` static styles = css`

View File

@ -9,7 +9,7 @@ use ViewComponents\Component;
class Alert extends Component class Alert extends Component
{ {
protected array $props = ['glyph', 'title']; protected array $props = ['glyph', 'title', 'variant'];
protected string $glyph = ''; protected string $glyph = '';

View File

@ -9,6 +9,8 @@ use ViewComponents\Component;
class ChartsComponent extends Component class ChartsComponent extends Component
{ {
protected array $props = ['title', 'subtitle', 'dataUrl', 'type'];
protected string $title; protected string $title;
protected string $subtitle = ''; protected string $subtitle = '';

View File

@ -9,7 +9,7 @@ use Override;
class Checkbox extends FormComponent class Checkbox extends FormComponent
{ {
protected array $props = ['hint', 'isChecked']; protected array $props = ['hint', 'helper', 'isChecked'];
protected array $casts = [ protected array $casts = [
'isChecked' => 'boolean', 'isChecked' => 'boolean',

View File

@ -15,8 +15,8 @@ class Field extends Component
'isRequired', 'isRequired',
'isReadonly', 'isReadonly',
'as', 'as',
'helper',
'hint', 'hint',
'helper',
]; ];
protected array $casts = [ protected array $casts = [
@ -34,10 +34,10 @@ class Field extends Component
protected string $as = 'Input'; protected string $as = 'Input';
protected string $helper = '';
protected string $hint = ''; protected string $hint = '';
protected string $helper = '';
#[Override] #[Override]
public function render(): string public function render(): string
{ {

View File

@ -8,7 +8,7 @@ use Override;
class RadioButton extends FormComponent class RadioButton extends FormComponent
{ {
protected array $props = ['isChecked', 'hint']; protected array $props = ['isSelected', 'description'];
protected array $casts = [ protected array $casts = [
'isSelected' => 'boolean', 'isSelected' => 'boolean',

View File

@ -9,7 +9,7 @@ use Override;
class Toggler extends FormComponent class Toggler extends FormComponent
{ {
protected array $props = ['size', 'hint', 'isChecked']; protected array $props = ['size', 'hint', 'helper', 'isChecked'];
protected array $casts = [ protected array $casts = [
'isChecked' => 'boolean', 'isChecked' => 'boolean',

View File

@ -32,7 +32,7 @@ class XMLEditor extends FormComponent
$textarea = form_textarea($this->attributes, $this->content); $textarea = form_textarea($this->attributes, $this->content);
return <<<HTML return <<<HTML
<xml-editor>{$textarea}</time-ago> <xml-editor>{$textarea}</xml-editor>
HTML; HTML;
} }
} }

View File

@ -6,10 +6,10 @@ namespace App\Views\Components;
class IconButton extends Button class IconButton extends Button
{ {
public string $glyph;
protected array $props = ['glyph']; protected array $props = ['glyph'];
protected string $glyph;
public function __construct(array $attributes) public function __construct(array $attributes)
{ {
$iconButtonAttributes = [ $iconButtonAttributes = [

View File

@ -9,18 +9,18 @@ use ViewComponents\Component;
class Pill extends Component class Pill extends Component
{ {
protected array $props = ['size', 'variant', 'icon', 'iconClass', 'hint'];
/** /**
* @var 'small'|'base' * @var 'small'|'base'
*/ */
public string $size = 'base'; protected string $size = 'base';
public string $variant = 'default'; protected string $variant = 'default';
public string $icon = ''; protected string $icon = '';
public string $iconClass = ''; protected string $iconClass = '';
protected array $props = ['size', 'variant', 'icon', 'iconClass', 'hint'];
protected string $hint = ''; protected string $hint = '';

View File

@ -180,14 +180,6 @@ $routes->group(
], ],
); );
}); });
$routes->get('monetization-other', 'PodcastController::monetizationOther/$1', [
'as' => 'podcast-monetization-other',
'filter' => 'permission:podcast$1.edit',
]);
$routes->post('monetization-other', 'PodcastController::monetizationOtherAction/$1', [
'as' => 'podcast-monetization-other',
'filter' => 'permission:podcast$1.edit',
]);
$routes->group('analytics', static function ($routes): void { $routes->group('analytics', static function ($routes): void {
$routes->get('/', 'PodcastController::viewAnalytics/$1', [ $routes->get('/', 'PodcastController::viewAnalytics/$1', [
'as' => 'podcast-analytics', 'as' => 'podcast-analytics',

View File

@ -190,9 +190,6 @@ class EpisodeController extends BaseController
->with('error', lang('Episode.messages.sameSlugError')); ->with('error', lang('Episode.messages.sameSlugError'));
} }
$db = db_connect();
$db->transStart();
$newEpisode = new Episode([ $newEpisode = new Episode([
'created_by' => user_id(), 'created_by' => user_id(),
'updated_by' => user_id(), 'updated_by' => user_id(),
@ -217,11 +214,10 @@ class EpisodeController extends BaseController
'season_number' => $this->request->getPost('season_number') 'season_number' => $this->request->getPost('season_number')
? (int) $this->request->getPost('season_number') ? (int) $this->request->getPost('season_number')
: null, : null,
'type' => $this->request->getPost('type'), 'type' => $this->request->getPost('type'),
'is_blocked' => $this->request->getPost('block') === 'yes', 'is_blocked' => $this->request->getPost('block') === 'yes',
'custom_rss_string' => $this->request->getPost('custom_rss'), 'is_premium' => $this->request->getPost('premium') === 'yes',
'is_premium' => $this->request->getPost('premium') === 'yes', 'published_at' => null,
'published_at' => null,
]); ]);
$transcriptChoice = $this->request->getPost('transcript-choice'); $transcriptChoice = $this->request->getPost('transcript-choice');
@ -244,32 +240,12 @@ class EpisodeController extends BaseController
$episodeModel = new EpisodeModel(); $episodeModel = new EpisodeModel();
if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) { if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
$db->transRollback();
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
->with('errors', $episodeModel->errors()); ->with('errors', $episodeModel->errors());
} }
// update podcast's episode_description_footer_markdown if changed
$this->podcast->episode_description_footer_markdown = $this->request->getPost(
'description_footer'
) === '' ? null : $this->request->getPost('description_footer');
if ($this->podcast->hasChanged('episode_description_footer_markdown')) {
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $newEpisodeId])->with( return redirect()->route('episode-view', [$this->podcast->id, $newEpisodeId])->with(
'message', 'message',
lang('Episode.messages.createSuccess') lang('Episode.messages.createSuccess')
@ -330,7 +306,6 @@ class EpisodeController extends BaseController
$this->episode->season_number = $this->request->getPost('season_number') ?: null; $this->episode->season_number = $this->request->getPost('season_number') ?: null;
$this->episode->type = $this->request->getPost('type'); $this->episode->type = $this->request->getPost('type');
$this->episode->is_blocked = $this->request->getPost('block') === 'yes'; $this->episode->is_blocked = $this->request->getPost('block') === 'yes';
$this->episode->custom_rss_string = $this->request->getPost('custom_rss');
$this->episode->is_premium = $this->request->getPost('premium') === 'yes'; $this->episode->is_premium = $this->request->getPost('premium') === 'yes';
$this->episode->updated_by = (int) user_id(); $this->episode->updated_by = (int) user_id();
@ -376,39 +351,14 @@ class EpisodeController extends BaseController
$this->episode->chapters_remote_url = $chaptersRemoteUrl === '' ? null : $chaptersRemoteUrl; $this->episode->chapters_remote_url = $chaptersRemoteUrl === '' ? null : $chaptersRemoteUrl;
} }
$db = db_connect();
$db->transStart();
$episodeModel = new EpisodeModel(); $episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) { if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
->with('errors', $episodeModel->errors()); ->with('errors', $episodeModel->errors());
} }
// update podcast's episode_description_footer_markdown if changed
$this->podcast->episode_description_footer_markdown = $this->request->getPost(
'description_footer'
) === '' ? null : $this->request->getPost('description_footer');
if ($this->podcast->hasChanged('episode_description_footer_markdown')) {
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
}
$db->transComplete();
return redirect()->route('episode-edit', [$this->podcast->id, $this->episode->id])->with( return redirect()->route('episode-edit', [$this->podcast->id, $this->episode->id])->with(
'message', 'message',
lang('Episode.messages.editSuccess') lang('Episode.messages.editSuccess')

View File

@ -213,18 +213,14 @@ class PodcastController extends BaseController
'parental_advisory' => $this->request->getPost('parental_advisory') !== 'undefined' 'parental_advisory' => $this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory') ? $this->request->getPost('parental_advisory')
: null, : null,
'owner_name' => $this->request->getPost('owner_name'), 'owner_name' => $this->request->getPost('owner_name'),
'owner_email' => $this->request->getPost('owner_email'), 'owner_email' => $this->request->getPost('owner_email'),
'is_owner_email_removed_from_feed' => $this->request->getPost('is_owner_email_removed_from_feed') === 'yes', 'publisher' => $this->request->getPost('publisher'),
'publisher' => $this->request->getPost('publisher'), 'type' => $this->request->getPost('type'),
'type' => $this->request->getPost('type'), 'copyright' => $this->request->getPost('copyright'),
'medium' => $this->request->getPost('medium'), 'location' => $this->request->getPost('location_name') === '' ? null : new Location(
'copyright' => $this->request->getPost('copyright'),
'location' => $this->request->getPost('location_name') === '' ? null : new Location(
$this->request->getPost('location_name') $this->request->getPost('location_name')
), ),
'verify_txt' => $this->request->getPost('verify_txt'),
'custom_rss_string' => $this->request->getPost('custom_rss'),
'is_blocked' => $this->request->getPost('block') === 'yes', 'is_blocked' => $this->request->getPost('block') === 'yes',
'is_completed' => $this->request->getPost('complete') === 'yes', 'is_completed' => $this->request->getPost('complete') === 'yes',
'is_locked' => $this->request->getPost('lock') === 'yes', 'is_locked' => $this->request->getPost('lock') === 'yes',
@ -253,10 +249,6 @@ class PodcastController extends BaseController
$this->request->getPost('other_categories') ?? [], $this->request->getPost('other_categories') ?? [],
); );
// OP3
service('settings')
->set('Analytics.enableOP3', $this->request->getPost('enable_op3') === 'yes', 'podcast:' . $newPodcastId);
$db->transComplete(); $db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId])->with( return redirect()->route('podcast-view', [$newPodcastId])->with(
@ -314,19 +306,11 @@ class PodcastController extends BaseController
$this->podcast->publisher = $this->request->getPost('publisher'); $this->podcast->publisher = $this->request->getPost('publisher');
$this->podcast->owner_name = $this->request->getPost('owner_name'); $this->podcast->owner_name = $this->request->getPost('owner_name');
$this->podcast->owner_email = $this->request->getPost('owner_email'); $this->podcast->owner_email = $this->request->getPost('owner_email');
$this->podcast->is_owner_email_removed_from_feed = $this->request->getPost(
'is_owner_email_removed_from_feed'
) === 'yes';
$this->podcast->type = $this->request->getPost('type'); $this->podcast->type = $this->request->getPost('type');
$this->podcast->medium = $this->request->getPost('medium');
$this->podcast->copyright = $this->request->getPost('copyright'); $this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->location = $this->request->getPost('location_name') === '' ? null : new Location( $this->podcast->location = $this->request->getPost('location_name') === '' ? null : new Location(
$this->request->getPost('location_name') $this->request->getPost('location_name')
); );
$this->podcast->verify_txt = $this->request->getPost('verify_txt') === '' ? null : $this->request->getPost(
'verify_txt'
);
$this->podcast->custom_rss_string = $this->request->getPost('custom_rss');
$this->podcast->new_feed_url = $this->request->getPost('new_feed_url') === '' ? null : $this->request->getPost( $this->podcast->new_feed_url = $this->request->getPost('new_feed_url') === '' ? null : $this->request->getPost(
'new_feed_url' 'new_feed_url'
); );
@ -359,14 +343,6 @@ class PodcastController extends BaseController
$this->request->getPost('other_categories') ?? [], $this->request->getPost('other_categories') ?? [],
); );
// enable/disable OP3?
service('settings')
->set(
'Analytics.enableOP3',
$this->request->getPost('enable_op3') === 'yes',
'podcast:' . $this->podcast->id
);
$db->transComplete(); $db->transComplete();
return redirect()->route('podcast-edit', [$this->podcast->id])->with( return redirect()->route('podcast-edit', [$this->podcast->id])->with(
@ -375,53 +351,6 @@ class PodcastController extends BaseController
); );
} }
public function monetizationOther(): string
{
helper('form');
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->at_handle,
]);
return view('podcast/monetization_other', $data);
}
public function monetizationOtherAction(): RedirectResponse
{
if (
($partnerId = $this->request->getPost('partner_id')) === '' ||
($partnerLinkUrl = $this->request->getPost('partner_link_url')) === '' ||
($partnerImageUrl = $this->request->getPost('partner_image_url')) === '') {
$partnerId = null;
$partnerLinkUrl = null;
$partnerImageUrl = null;
}
$this->podcast->payment_pointer = $this->request->getPost(
'payment_pointer'
) === '' ? null : $this->request->getPost('payment_pointer');
$this->podcast->partner_id = $partnerId;
$this->podcast->partner_link_url = $partnerLinkUrl;
$this->podcast->partner_image_url = $partnerImageUrl;
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
return redirect()->route('podcast-monetization-other', [$this->podcast->id])->with(
'message',
lang('Podcast.messages.editSuccess')
);
}
public function deleteBanner(): RedirectResponse public function deleteBanner(): RedirectResponse
{ {
if (! $this->podcast->banner instanceof Image) { if (! $this->podcast->banner instanceof Image) {

View File

@ -37,17 +37,4 @@ class Analytics extends BaseConfig
* Z&|qECKBrwgaaD>~;U/tXG1U%tSe_oi5Tzy)h>}5NC2npSrjvM0w_Q>cs=0o=H]* * Z&|qECKBrwgaaD>~;U/tXG1U%tSe_oi5Tzy)h>}5NC2npSrjvM0w_Q>cs=0o=H]*
*/ */
public string $salt = ''; public string $salt = '';
/**
* --------------------------------------------------------------------------
* The Open Podcast Prefix Project Config
* --------------------------------------------------------------------------
*
* @var array<string, string>
*/
public array $OP3 = [
'host' => 'https://op3.dev/',
];
public bool $enableOP3 = false;
} }

View File

@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Analytics;
use App\Entities\Episode;
class OP3
{
protected string $host;
/**
* @param array<string, string> $config
*/
public function __construct(array $config)
{
$this->host = rtrim($config['host'], '/');
}
public function wrap(string $audioURL, Episode $episode): string
{
// remove scheme from audioURI if https
$audioURIWithoutHTTPS = preg_replace('(^https://)', '', $audioURL);
return $this->host . '/e,pg=' . $episode->podcast->guid . '/' . $audioURIWithoutHTTPS;
}
}

View File

@ -72,7 +72,6 @@ class EpisodeController extends Controller
{ {
$episode->cover_url = $episode->getCover() $episode->cover_url = $episode->getCover()
->file_url; ->file_url;
$episode->audio_url = $episode->getAudioUrl();
$episode->duration = round($episode->audio->duration); $episode->duration = round($episode->audio->duration);
return $episode; return $episode;

View File

@ -16,6 +16,7 @@ use Modules\Admin\Controllers\BaseController;
use Modules\Plugins\Core\BasePlugin; use Modules\Plugins\Core\BasePlugin;
use Modules\Plugins\Core\Markdown; use Modules\Plugins\Core\Markdown;
use Modules\Plugins\Core\Plugins; use Modules\Plugins\Core\Plugins;
use Modules\Plugins\Core\RSS;
use Modules\Plugins\Manifest\Field; use Modules\Plugins\Manifest\Field;
class PluginController extends BaseController class PluginController extends BaseController
@ -330,6 +331,7 @@ class PluginController extends BaseController
$this->request->getPost('client_timezone') $this->request->getPost('client_timezone')
)->setTimezone(app_timezone()), )->setTimezone(app_timezone()),
'markdown' => new Markdown($value), 'markdown' => new Markdown($value),
'rss' => new RSS($value),
default => $value, default => $value,
}; };
} }

View File

@ -6,7 +6,7 @@ namespace Modules\Plugins\Core;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement; use App\Libraries\RssFeed;
use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\URI;
use League\CommonMark\Environment\Environment; use League\CommonMark\Environment\Environment;
use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Event\DocumentParsedEvent;
@ -86,7 +86,7 @@ abstract class BasePlugin implements PluginInterface
} }
#[Override] #[Override]
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void public function rssAfterChannel(Podcast $podcast, RssFeed $channel): void
{ {
} }
@ -96,7 +96,7 @@ abstract class BasePlugin implements PluginInterface
} }
#[Override] #[Override]
public function rssAfterItem(Episode $episode, SimpleRSSElement $item): void public function rssAfterItem(Episode $episode, RssFeed $item): void
{ {
} }

View File

@ -6,17 +6,17 @@ namespace Modules\Plugins\Core;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement; use App\Libraries\RssFeed;
interface PluginInterface interface PluginInterface
{ {
public function rssBeforeChannel(Podcast $podcast): void; public function rssBeforeChannel(Podcast $podcast): void;
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void; public function rssAfterChannel(Podcast $podcast, RssFeed $channel): void;
public function rssBeforeItem(Episode $episode): void; public function rssBeforeItem(Episode $episode): void;
public function rssAfterItem(Episode $episode, SimpleRSSElement $item): void; public function rssAfterItem(Episode $episode, RssFeed $item): void;
public function siteHead(): void; public function siteHead(): void;
} }

View File

@ -6,15 +6,15 @@ namespace Modules\Plugins\Core;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement; use App\Libraries\RssFeed;
use Config\Database; use Config\Database;
use Modules\Plugins\Config\Plugins as PluginsConfig; use Modules\Plugins\Config\Plugins as PluginsConfig;
/** /**
* @method void rssBeforeChannel(Podcast $podcast) * @method void rssBeforeChannel(Podcast $podcast)
* @method void rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel) * @method void rssAfterChannel(Podcast $podcast, RssFeed $channel)
* @method void rssBeforeItem(Episode $episode) * @method void rssBeforeItem(Episode $episode)
* @method void rssAfterItem(Episode $episode, SimpleRSSElement $item) * @method void rssAfterItem(Episode $episode, RssFeed $item)
* @method void siteHead() * @method void siteHead()
*/ */
class Plugins class Plugins
@ -28,25 +28,27 @@ class Plugins
'checkbox' => ['permit_empty'], 'checkbox' => ['permit_empty'],
'datetime' => ['valid_date[Y-m-d H:i]'], 'datetime' => ['valid_date[Y-m-d H:i]'],
'email' => ['valid_email'], 'email' => ['valid_email'],
'group' => ['permit_empty', 'is_list'],
'markdown' => ['string'], 'markdown' => ['string'],
'number' => ['integer'], 'number' => ['integer'],
'radio-group' => ['string'], 'radio-group' => ['string'],
'rss' => ['string'],
'select' => ['string'], 'select' => ['string'],
'select-multiple' => ['permit_empty', 'is_list'], 'select-multiple' => ['permit_empty', 'is_list'],
'text' => ['string'], 'text' => ['string'],
'textarea' => ['string'], 'textarea' => ['string'],
'toggler' => ['permit_empty'], 'toggler' => ['permit_empty'],
'url' => ['valid_url_strict'], 'url' => ['valid_url_strict'],
'group' => ['permit_empty', 'is_list'],
]; ];
public const FIELDS_CASTS = [ public const FIELDS_CASTS = [
'checkbox' => 'bool', 'checkbox' => 'bool',
'datetime' => 'datetime', 'datetime' => 'datetime',
'markdown' => 'markdown',
'number' => 'int', 'number' => 'int',
'rss' => 'rss',
'toggler' => 'bool', 'toggler' => 'bool',
'url' => 'uri', 'url' => 'uri',
'markdown' => 'markdown',
]; ];
/** /**

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Core;
use App\Libraries\RssFeed;
use Exception;
use Override;
use Stringable;
class RSS implements Stringable
{
public function __construct(
protected string $rss
) {
}
#[Override]
public function __toString(): string
{
return $this->rss;
}
/**
* @return ?RssFeed[]
*/
public function toSimpleRSS(): ?array
{
try {
$rssFeed = new RssFeed("{$this->rss}");
} catch (Exception) {
return null;
}
return [
...$rssFeed->children(),
...$rssFeed->children(RssFeed::ATOM_NS, true),
...$rssFeed->children(RssFeed::ITUNES_NS, true),
...$rssFeed->children(RssFeed::PODCAST_NS, true),
];
}
}

View File

@ -7,7 +7,7 @@ namespace Modules\Plugins\Manifest;
use Override; use Override;
/** /**
* @property 'checkbox'|'datetime'|'email'|'markdown'|'number'|'radio-group'|'select-multiple'|'select'|'text'|'textarea'|'toggler'|'url'|'group' $type * @property 'checkbox'|'datetime'|'email'|'group'|'markdown'|'number'|'radio-group'|'rss'|'select-multiple'|'select'|'text'|'textarea'|'toggler'|'url' $type
* @property string $key * @property string $key
* @property string $label * @property string $label
* @property string $hint * @property string $hint
@ -20,7 +20,7 @@ use Override;
class Field extends ManifestObject class Field extends ManifestObject
{ {
protected const VALIDATION_RULES = [ protected const VALIDATION_RULES = [
'type' => 'permit_empty|in_list[checkbox,datetime,email,markdown,number,radio-group,select-multiple,select,text,textarea,toggler,url,group]', 'type' => 'permit_empty|in_list[checkbox,datetime,email,group,markdown,number,radio-group,rss,select-multiple,select,text,textarea,toggler,url]',
'key' => 'required|alpha_dash', 'key' => 'required|alpha_dash',
'label' => 'required|string', 'label' => 'required|string',
'hint' => 'permit_empty|string', 'hint' => 'permit_empty|string',

View File

@ -14,7 +14,7 @@ use CodeIgniter\HTTP\URI;
class Repository extends ManifestObject class Repository extends ManifestObject
{ {
protected const VALIDATION_RULES = [ protected const VALIDATION_RULES = [
'type' => 'required|in_list[git]', 'type' => 'permit_empty|in_list[git]',
'url' => 'required|valid_url_strict', 'url' => 'required|valid_url_strict',
'directory' => 'permit_empty', 'directory' => 'permit_empty',
]; ];
@ -26,7 +26,7 @@ class Repository extends ManifestObject
'url' => URI::class, 'url' => URI::class,
]; ];
protected string $type; protected string $type = 'git';
protected URI $url; protected URI $url;

View File

@ -76,6 +76,7 @@
"monetization", "monetization",
"podcasting2", "podcasting2",
"privacy", "privacy",
"productivity",
"seo" "seo"
] ]
} }
@ -173,13 +174,14 @@
"properties": { "properties": {
"type": { "type": {
"enum": [ "enum": [
"group",
"checkbox", "checkbox",
"datetime", "datetime",
"email", "email",
"group",
"markdown", "markdown",
"number", "number",
"radio-group", "radio-group",
"rss",
"select-multiple", "select-multiple",
"select", "select",
"text", "text",

View File

@ -104,53 +104,46 @@ class FakeSinglePodcastApiSeeder extends Seeder
} }
/** /**
* @return array{id: int, guid: string, actor_id: int, handle: string, title: string, description_markdown: string, description_html: string, cover_id: int, banner_id: int, language_code: string, category_id: int, parental_advisory: null, owner_name: string, owner_email: string, publisher: string, type: string, copyright: string, episode_description_footer_markdown: null, episode_description_footer_html: null, is_blocked: int, is_completed: int, is_locked: int, imported_feed_url: null, new_feed_url: null, payment_pointer: null, location_name: null, location_geo: null, location_osm: null, custom_rss: null, is_published_on_hubs: int, partner_id: null, partner_link_url: null, partner_image_url: null, created_by: int, updated_by: int, created_at: string, updated_at: string} * @return array{id: int, guid: string, actor_id: int, handle: string, title: string, description_markdown: string, description_html: string, cover_id: int, banner_id: int, language_code: string, category_id: int, parental_advisory: null, owner_name: string, owner_email: string, publisher: string, type: string, copyright: string, is_blocked: int, is_completed: int, is_locked: int, imported_feed_url: null, new_feed_url: null, location_name: null, location_geo: null, location_osm: null, is_published_on_hubs: int, created_by: int, updated_by: int, created_at: string, updated_at: string}
*/ */
public static function podcast(): array public static function podcast(): array
{ {
return [ return [
'id' => 1, 'id' => 1,
'guid' => '0d341200-0234-5de7-99a6-a7d02bea4ce2', 'guid' => '0d341200-0234-5de7-99a6-a7d02bea4ce2',
'actor_id' => 1, 'actor_id' => 1,
'handle' => 'Handle', 'handle' => 'Handle',
'title' => 'Title', 'title' => 'Title',
'description_markdown' => 'description', 'description_markdown' => 'description',
'description_html' => '<p>description</p>', 'description_html' => '<p>description</p>',
'cover_id' => 1, 'cover_id' => 1,
'banner_id' => 2, 'banner_id' => 2,
'language_code' => 'en', 'language_code' => 'en',
'category_id' => 1, 'category_id' => 1,
'parental_advisory' => null, 'parental_advisory' => null,
'owner_name' => 'Owner', 'owner_name' => 'Owner',
'owner_email' => 'Owner@gmail.com', 'owner_email' => 'Owner@gmail.com',
'publisher' => '', 'publisher' => '',
'type' => 'episodic', 'type' => 'episodic',
'copyright' => '', 'copyright' => '',
'episode_description_footer_markdown' => null, 'is_blocked' => 0,
'episode_description_footer_html' => null, 'is_completed' => 0,
'is_blocked' => 0, 'is_locked' => 1,
'is_completed' => 0, 'imported_feed_url' => null,
'is_locked' => 1, 'new_feed_url' => null,
'imported_feed_url' => null, 'location_name' => null,
'new_feed_url' => null, 'location_geo' => null,
'payment_pointer' => null, 'location_osm' => null,
'location_name' => null, 'is_published_on_hubs' => 0,
'location_geo' => null, 'created_by' => 1,
'location_osm' => null, 'updated_by' => 1,
'custom_rss' => null, 'created_at' => '2022-06-13 8:00:00',
'is_published_on_hubs' => 0, 'updated_at' => '2022-06-13 8:00:00',
'partner_id' => null,
'partner_link_url' => null,
'partner_image_url' => null,
'created_by' => 1,
'updated_by' => 1,
'created_at' => '2022-06-13 8:00:00',
'updated_at' => '2022-06-13 8:00:00',
]; ];
} }
/** /**
* @return array{id: int, podcast_id: int, guid: string, title: string, slug: string, audio_id: int, description_markdown: string, description_html: string, cover_id: int, transcript_id: null, transcript_remote_url: null, chapters_id: null, chapters_remote_url: null, parental_advisory: null, number: int, season_number: null, type: string, is_blocked: false, location_name: null, location_geo: null, location_osm: null, custom_rss: null, is_published_on_hubs: false, posts_count: int, comments_count: int, is_premium: false, created_by: int, updated_by: int, published_at: null, created_at: string, updated_at: string} * @return array{id:int,podcast_id:int,guid:string,title:string,slug:string,audio_id:int,description_markdown:string,description_html:string,cover_id:int,transcript_id:null,transcript_remote_url:null,chapters_id:null,chapters_remote_url:null,parental_advisory:null,number:int,season_number:null,type:string,is_blocked:false,location_name:null,location_geo:null,location_osm:null,is_published_on_hubs:false,posts_count:int,comments_count:int,is_premium:false,created_by:int,updated_by:int,published_at:null,created_at:string,updated_at:string}
*/ */
public static function episode(): array public static function episode(): array
{ {
@ -176,7 +169,6 @@ class FakeSinglePodcastApiSeeder extends Seeder
'location_name' => null, 'location_name' => null,
'location_geo' => null, 'location_geo' => null,
'location_osm' => null, 'location_osm' => null,
'custom_rss' => null,
'is_published_on_hubs' => false, 'is_published_on_hubs' => false,
'posts_count' => 0, 'posts_count' => 0,
'comments_count' => 0, 'comments_count' => 0,

View File

@ -6,7 +6,7 @@ namespace Tests\Modules\Plugins;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement; use App\Libraries\RssFeed;
use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\DatabaseTestTrait;
use Modules\Plugins\Config\Plugins as PluginsConfig; use Modules\Plugins\Config\Plugins as PluginsConfig;
@ -96,7 +96,7 @@ final class PluginsTest extends CIUnitTestCase
self::$plugins->runHook('rssBeforeChannel', [$podcast]); self::$plugins->runHook('rssBeforeChannel', [$podcast]);
$this->assertEquals('Podcast test', $podcast->title); $this->assertEquals('Podcast test', $podcast->title);
$channel = new SimpleRSSElement('<channel></channel>'); $channel = new RssFeed('<channel></channel>');
$this->assertTrue(empty($channel->foo)); $this->assertTrue(empty($channel->foo));
self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]); self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]);
$this->assertFalse(empty($channel->foo)); $this->assertFalse(empty($channel->foo));
@ -106,7 +106,7 @@ final class PluginsTest extends CIUnitTestCase
self::$plugins->runHook('rssBeforeItem', [$episode]); self::$plugins->runHook('rssBeforeItem', [$episode]);
$this->assertEquals('Episode test', $episode->title); $this->assertEquals('Episode test', $episode->title);
$item = new SimpleRSSElement('<item></item>'); $item = new RssFeed('<item></item>');
$this->assertTrue(empty($item->efoo)); $this->assertTrue(empty($item->efoo));
self::$plugins->runHook('rssAfterItem', [$episode, $item]); self::$plugins->runHook('rssAfterItem', [$episode, $item]);
$this->assertFalse(empty($item->efoo)); $this->assertFalse(empty($item->efoo));
@ -133,7 +133,7 @@ final class PluginsTest extends CIUnitTestCase
self::$plugins->runHook('rssBeforeChannel', [$podcast]); self::$plugins->runHook('rssBeforeChannel', [$podcast]);
$this->assertEquals('', $podcast->title); $this->assertEquals('', $podcast->title);
$channel = new SimpleRSSElement('<channel></channel>'); $channel = new RssFeed('<channel></channel>');
$this->assertTrue(empty($channel->foo)); $this->assertTrue(empty($channel->foo));
self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]); self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]);
$this->assertTrue(empty($channel->foo)); $this->assertTrue(empty($channel->foo));
@ -143,7 +143,7 @@ final class PluginsTest extends CIUnitTestCase
self::$plugins->runHook('rssBeforeItem', [$episode]); self::$plugins->runHook('rssBeforeItem', [$episode]);
$this->assertEquals('', $episode->title); $this->assertEquals('', $episode->title);
$item = new SimpleRSSElement('<item></item>'); $item = new RssFeed('<item></item>');
$this->assertTrue(empty($item->efoo)); $this->assertTrue(empty($item->efoo));
self::$plugins->runHook('rssAfterItem', [$episode, $item]); self::$plugins->runHook('rssAfterItem', [$episode, $item]);
$this->assertTrue(empty($item->efoo)); $this->assertTrue(empty($item->efoo));
@ -167,7 +167,7 @@ final class PluginsTest extends CIUnitTestCase
$this->assertEquals('Podcast test undeclared', $podcast->title); $this->assertEquals('Podcast test undeclared', $podcast->title);
// rssAfterChannel has not been declared in plugin manifest, should not be running // rssAfterChannel has not been declared in plugin manifest, should not be running
$channel = new SimpleRSSElement('<channel></channel>'); $channel = new RssFeed('<channel></channel>');
$this->assertTrue(empty($channel->foo)); $this->assertTrue(empty($channel->foo));
self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]); self::$plugins->runHook('rssAfterChannel', [$podcast, $channel]);
$this->assertTrue(empty($channel->foo)); $this->assertTrue(empty($channel->foo));

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement; use App\Libraries\RssFeed;
use Modules\Plugins\Core\BasePlugin; use Modules\Plugins\Core\BasePlugin;
class AcmeAllHooksPlugin extends BasePlugin class AcmeAllHooksPlugin extends BasePlugin
@ -16,7 +16,7 @@ class AcmeAllHooksPlugin extends BasePlugin
} }
#[Override] #[Override]
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void public function rssAfterChannel(Podcast $podcast, RssFeed $channel): void
{ {
$channel->addChild('foo', 'bar'); $channel->addChild('foo', 'bar');
} }
@ -28,7 +28,7 @@ class AcmeAllHooksPlugin extends BasePlugin
} }
#[Override] #[Override]
public function rssAfterItem(Episode $episode, SimpleRSSElement $item): void public function rssAfterItem(Episode $episode, RssFeed $item): void
{ {
$item->addChild('efoo', 'ebar'); $item->addChild('efoo', 'ebar');
} }

View File

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement; use App\Libraries\RssFeed;
use Modules\Plugins\Core\BasePlugin; use Modules\Plugins\Core\BasePlugin;
class AcmeUndeclaredHookPlugin extends BasePlugin class AcmeUndeclaredHookPlugin extends BasePlugin
@ -15,7 +15,7 @@ class AcmeUndeclaredHookPlugin extends BasePlugin
} }
#[Override] #[Override]
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void public function rssAfterChannel(Podcast $podcast, RssFeed $channel): void
{ {
$channel->addChild('foo', 'bar'); $channel->addChild('foo', 'bar');
} }

View File

@ -11,6 +11,12 @@ $episodeNavigation = [
'embed-add' => 'episodes.edit', 'embed-add' => 'episodes.edit',
], ],
], ],
'plugins' => [
'icon' => 'puzzle-fill', // @icon('puzzle-fill')
'items' => [],
'items-labels' => [],
'items-permissions' => [],
],
'clips' => [ 'clips' => [
'icon' => 'clapperboard-fill', // @icon('clapperboard-fill') 'icon' => 'clapperboard-fill', // @icon('clapperboard-fill')
'items' => ['video-clips-list', 'video-clips-create', 'soundbites-list', 'soundbites-create'], 'items' => ['video-clips-list', 'video-clips-create', 'soundbites-list', 'soundbites-create'],
@ -24,12 +30,6 @@ $episodeNavigation = [
'count-route' => 'video-clips-list', 'count-route' => 'video-clips-list',
'add-cta' => 'video-clips-create', 'add-cta' => 'video-clips-create',
], ],
'plugins' => [
'icon' => 'puzzle-fill', // @icon('puzzle-fill')
'items' => [],
'items-labels' => [],
'items-permissions' => [],
],
]; ];
foreach (plugins()->getPluginsWithEpisodeSettings() as $plugin) { foreach (plugins()->getPluginsWithEpisodeSettings() as $plugin) {

View File

@ -127,14 +127,6 @@
isRequired="true" isRequired="true"
disallowList="header,quote" /> disallowList="header,quote" />
<x-Forms.Field
as="MarkdownEditor"
name="description_footer"
label="<?= esc(lang('Episode.form.description_footer')) ?>"
hint="<?= esc(lang('Episode.form.description_footer_hint')) ?>"
value="<?= esc($podcast->episode_description_footer_markdown) ?? '' ?>"
disallowList="header,quote" />
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section title="<?= lang('Episode.form.premium_title') ?>"> <x-Forms.Section title="<?= lang('Episode.form.premium_title') ?>">
@ -211,12 +203,6 @@
title="<?= lang('Episode.form.advanced_section_title') ?>" title="<?= lang('Episode.form.advanced_section_title') ?>"
subtitle="<?= lang('Episode.form.advanced_section_subtitle') ?>" subtitle="<?= lang('Episode.form.advanced_section_subtitle') ?>"
> >
<x-Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Episode.form.custom_rss')) ?>"
hint="<?= esc(lang('Episode.form.custom_rss_hint')) ?>"
/>
<x-Forms.Toggler name="block" isChecked="false" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></x-Forms.Toggler> <x-Forms.Toggler name="block" isChecked="false" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></x-Forms.Toggler>

View File

@ -132,14 +132,6 @@
isRequired="true" isRequired="true"
disallowList="header,quote" /> disallowList="header,quote" />
<x-Forms.Field
as="MarkdownEditor"
name="description_footer"
label="<?= esc(lang('Episode.form.description_footer')) ?>"
hint="<?= esc(lang('Episode.form.description_footer_hint')) ?>"
value="<?= esc($podcast->episode_description_footer_markdown) ?? '' ?>"
disallowList="header,quote" />
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section title="<?= lang('Episode.form.premium_title') ?>" > <x-Forms.Section title="<?= lang('Episode.form.premium_title') ?>" >
@ -279,13 +271,6 @@
title="<?= lang('Episode.form.advanced_section_title') ?>" title="<?= lang('Episode.form.advanced_section_title') ?>"
subtitle="<?= lang('Episode.form.advanced_section_subtitle') ?>" subtitle="<?= lang('Episode.form.advanced_section_subtitle') ?>"
> >
<x-Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Episode.form.custom_rss')) ?>"
hint="<?= esc(lang('Episode.form.custom_rss_hint')) ?>"
content="<?= esc($episode->custom_rss_string) ?>"
/>
<x-Forms.Toggler id="block" name="block" isChecked="<?= $episode->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></x-Forms.Toggler> <x-Forms.Toggler id="block" name="block" isChecked="<?= $episode->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></x-Forms.Toggler>

View File

@ -118,6 +118,18 @@ case 'markdown': ?>
value="<?= $value ?>" value="<?= $value ?>"
/> />
<?php break; <?php break;
case 'rss': ?>
<x-Forms.Field
as="XMLEditor"
class="<?= $class ?>"
name="<?= $name ?>"
label="<?= $label ?>"
hint="<?= $hint ?>"
helper="<?= $helper ?>"
isRequired="<?= $optional ? 'false' : 'true' ?>"
content="<?= htmlspecialchars($value) ?>"
/>
<?php break;
case 'datetime': ?> case 'datetime': ?>
<x-Forms.Field <x-Forms.Field
as="DatetimePicker" as="DatetimePicker"

View File

@ -67,13 +67,11 @@ $podcastNavigation = [
'subscription-list', 'subscription-list',
'subscription-create', 'subscription-create',
'platforms-funding', 'platforms-funding',
'podcast-monetization-other',
], ],
'items-permissions' => [ 'items-permissions' => [
'subscription-list' => 'manage-subscriptions', 'subscription-list' => 'manage-subscriptions',
'subscription-create' => 'manage-subscriptions', 'subscription-create' => 'manage-subscriptions',
'platforms-funding' => 'manage-platforms', 'platforms-funding' => 'manage-platforms',
'podcast-monetization-other' => 'edit',
], ],
], ],
'contributors' => [ 'contributors' => [
@ -135,15 +133,6 @@ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) {
'class' => 'text-sm opacity-60', 'class' => 'text-sm opacity-60',
]) ?> ]) ?>
</a> </a>
<?php if ($podcast->is_op3_enabled): ?>
<a href="<?= $podcast->op3_url ?>" class="inline-flex items-center text-xs gap-x-1 group hover:underline" data-tooltip="bottom" target="_blank" rel="noopener noreferrer" title="<?= lang('Podcast.form.op3_link') ?>">
<?= icon('line-chart-fill', [
'class' => 'text-xl text-white inline-flex items-center justify-center rounded',
]) . 'OP3' . icon('external-link-fill', [
'class' => 'text-sm opacity-60',
]) ?>
</a>
<?php endif; ?>
</div> </div>
</div> </div>
</div> </div>

View File

@ -58,29 +58,6 @@
])) ?>" ])) ?>"
isRequired="true" isRequired="true"
/> />
<x-Forms.RadioGroup
label="<?= lang('Podcast.form.medium.label') ?>"
name="medium"
options="<?= esc(json_encode([
[
'label' => lang('Podcast.form.medium.podcast'),
'value' => 'podcast',
'description' => lang('Podcast.form.medium.podcast_description'),
],
[
'label' => lang('Podcast.form.medium.music'),
'value' => 'music',
'description' => lang('Podcast.form.medium.music_description'),
],
[
'label' => lang('Podcast.form.medium.audiobook'),
'value' => 'audiobook',
'description' => lang('Podcast.form.medium.audiobook_description'),
],
])) ?>"
isRequired="true"
/>
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section <x-Forms.Section
@ -148,9 +125,6 @@
hint="<?= esc(lang('Podcast.form.owner_email_hint')) ?>" hint="<?= esc(lang('Podcast.form.owner_email_hint')) ?>"
isRequired="true" /> isRequired="true" />
<x-Forms.Toggler class="mt-2" name="is_owner_email_removed_from_feed" isChecked="true" hint="<?= esc(lang('Podcast.form.is_owner_email_removed_from_feed_hint')) ?>">
<?= lang('Podcast.form.is_owner_email_removed_from_feed') ?></x-Forms.Toggler>
<x-Forms.Field <x-Forms.Field
name="publisher" name="publisher"
label="<?= esc(lang('Podcast.form.publisher')) ?>" label="<?= esc(lang('Podcast.form.publisher')) ?>"
@ -188,16 +162,6 @@
<?= lang('Podcast.form.premium_by_default') ?></x-Forms.Toggler> <?= lang('Podcast.form.premium_by_default') ?></x-Forms.Toggler>
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section
title="<?= lang('Podcast.form.op3') ?>"
subtitle="<?= lang('Podcast.form.op3_hint') ?>">
<a href="https://op3.dev" target="_blank" rel="noopener noreferrer" class="inline-flex self-start text-xs font-semibold underline gap-x-1 text-skin-muted hover:no-underline"><?= icon('link', [
'class' => 'text-sm',
]) ?>op3.dev</a>
<x-Forms.Toggler name="enable_op3" isChecked="false" hint="<?= esc(lang('Podcast.form.op3_enable_hint')) ?>"><?= lang('Podcast.form.op3_enable') ?></x-Forms.Toggler>
</x-Forms.Section>
<x-Forms.Section <x-Forms.Section
title="<?= lang('Podcast.form.location_section_title') ?>" title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" > subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
@ -212,21 +176,6 @@
<x-Forms.Section <x-Forms.Section
title="<?= lang('Podcast.form.advanced_section_title') ?>" > title="<?= lang('Podcast.form.advanced_section_title') ?>" >
<x-Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Podcast.form.custom_rss')) ?>"
hint="<?= esc(lang('Podcast.form.custom_rss_hint')) ?>"
rows="8" />
<x-Forms.Field
as="Textarea"
name="verify_txt"
label="<?= esc(lang('Podcast.form.verify_txt')) ?>"
hint="<?= esc(lang('Podcast.form.verify_txt_hint')) ?>"
helper="<?= esc(lang('Podcast.form.verify_txt_helper')) ?>"
rows="5" />
<x-Forms.Toggler class="mb-2" name="lock" isChecked="true" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>"> <x-Forms.Toggler class="mb-2" name="lock" isChecked="true" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>">
<?= lang('Podcast.form.lock') ?> <?= lang('Podcast.form.lock') ?>
</x-Forms.Toggler> </x-Forms.Toggler>

View File

@ -83,29 +83,6 @@
isRequired="true" isRequired="true"
/> />
<x-Forms.RadioGroup
label="<?= lang('Podcast.form.medium.label') ?>"
name="medium"
value="<?= $podcast->medium ?>"
options="<?= esc(json_encode([
[
'label' => lang('Podcast.form.medium.podcast'),
'value' => 'podcast',
'description' => lang('Podcast.form.medium.podcast_description'),
],
[
'label' => lang('Podcast.form.medium.music'),
'value' => 'music',
'description' => lang('Podcast.form.medium.music_description'),
],
[
'label' => lang('Podcast.form.medium.audiobook'),
'value' => 'audiobook',
'description' => lang('Podcast.form.medium.audiobook_description'),
],
])) ?>"
isRequired="true"
/>
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section <x-Forms.Section
@ -178,9 +155,6 @@
hint="<?= esc(lang('Podcast.form.owner_email_hint')) ?>" hint="<?= esc(lang('Podcast.form.owner_email_hint')) ?>"
isRequired="true" /> isRequired="true" />
<x-Forms.Toggler class="mt-2" name="is_owner_email_removed_from_feed" isChecked="<?= $podcast->is_owner_email_removed_from_feed ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.is_owner_email_removed_from_feed_hint')) ?>">
<?= lang('Podcast.form.is_owner_email_removed_from_feed') ?></x-Forms.Toggler>
<x-Forms.Field <x-Forms.Field
name="publisher" name="publisher"
label="<?= esc(lang('Podcast.form.publisher')) ?>" label="<?= esc(lang('Podcast.form.publisher')) ?>"
@ -221,17 +195,6 @@
<?= lang('Podcast.form.premium_by_default') ?></x-Forms.Toggler> <?= lang('Podcast.form.premium_by_default') ?></x-Forms.Toggler>
</x-Forms.Section> </x-Forms.Section>
<x-Forms.Section
title="<?= lang('Podcast.form.op3') ?>"
subtitle="<?= lang('Podcast.form.op3_hint') ?>">
<a href="https://op3.dev" target="_blank" rel="noopener noreferrer" class="inline-flex self-start text-xs font-semibold underline gap-x-1 text-skin-muted hover:no-underline"><?= icon('link', [
'class' => 'text-sm',
]) ?>op3.dev</a>
<x-Forms.Toggler name="enable_op3" isChecked="<?= service('settings')
->get('Analytics.enableOP3', 'podcast:' . $podcast->id) ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.op3_enable_hint')) ?>"><?= lang('Podcast.form.op3_enable') ?></x-Forms.Toggler>
</x-Forms.Section>
<x-Forms.Section <x-Forms.Section
title="<?= lang('Podcast.form.location_section_title') ?>" title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" > subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
@ -248,23 +211,6 @@
title="<?= lang('Podcast.form.advanced_section_title') ?>" title="<?= lang('Podcast.form.advanced_section_title') ?>"
subtitle="<?= lang('Podcast.form.advanced_section_subtitle') ?>" > subtitle="<?= lang('Podcast.form.advanced_section_subtitle') ?>" >
<x-Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Podcast.form.custom_rss')) ?>"
hint="<?= esc(lang('Podcast.form.custom_rss_hint')) ?>"
content="<?= esc($podcast->custom_rss_string) ?>"
rows="8" />
<x-Forms.Field
as="Textarea"
name="verify_txt"
label="<?= esc(lang('Podcast.form.verify_txt')) ?>"
hint="<?= esc(lang('Podcast.form.verify_txt_hint')) ?>"
helper="<?= esc(lang('Podcast.form.verify_txt_helper')) ?>"
value="<?= esc($podcast->verify_txt) ?>"
rows="5" />
<x-Forms.Field <x-Forms.Field
name="new_feed_url" name="new_feed_url"
type="url" type="url"

View File

@ -1,47 +0,0 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.all_podcasts') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Podcast.monetization_other') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<form id="podcast-edit-form" action="<?= route_to('podcast-monetization-edit', $podcast->id) ?>" method="POST" class="flex flex-col w-full max-w-xl gap-y-6">
<?= csrf_field() ?>
<x-Forms.Section
title="<?= lang('Podcast.form.monetization_section_title') ?>"
subtitle="<?= lang('Podcast.form.monetization_section_subtitle') ?>" >
<x-Forms.Field
name="payment_pointer"
label="<?= esc(lang('Podcast.form.payment_pointer')) ?>"
value="<?= esc($podcast->payment_pointer) ?>"
hint="<?= esc(lang('Podcast.form.payment_pointer_hint')) ?>" />
<fieldset class="flex flex-col items-start p-4 rounded bg-base">
<x-Heading tagName="legend" class="float-left" size="small"><?= lang('Podcast.form.partnership') ?></x-Heading>
<div class="flex flex-col w-full clear-left gap-x-2 gap-y-4 md:flex-row">
<div class="flex flex-col flex-shrink w-32">
<x-Forms.Label for="partner_id" hint="<?= esc(lang('Podcast.form.partner_id_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_id') ?></x-Forms.Label>
<x-Forms.Input name="partner_id" value="<?= esc($podcast->partner_id) ?>" />
</div>
<div class="flex flex-col flex-1">
<x-Forms.Label for="partner_link_url" hint="<?= esc(lang('Podcast.form.partner_link_url_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_link_url') ?></x-Forms.Label>
<x-Forms.Input name="partner_link_url" value="<?= esc($podcast->partner_link_url) ?>" />
</div>
</div>
<div class="flex flex-col w-full mt-2">
<x-Forms.Label for="partner_image_url" hint="<?= esc(lang('Podcast.form.partner_image_url_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_image_url') ?></x-Forms.Label>
<x-Forms.Input name="partner_image_url" value="<?= esc($podcast->partner_image_url) ?>" />
</div>
</fieldset>
</x-Forms.Section>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Common.forms.save') ?></x-Button>
</form>
<?= $this->endSection() ?>

View File

@ -148,9 +148,9 @@
<div class="col-start-2 px-8 py-4 text-white bg-header"> <div class="col-start-2 px-8 py-4 text-white bg-header">
<h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2> <h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2>
<?php if (substr_count($episode->description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?> <?php if (substr_count($episode->description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?>
<x-SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></x-SeeMore> <x-SeeMore class="max-w-xl prose-sm text-white"><?= $episode->description_html ?></x-SeeMore>
<?php else: ?> <?php else: ?>
<div class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></div> <div class="max-w-xl prose-sm text-white"><?= $episode->description_html ?></div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?= $this->include('episode/_partials/navigation') ?> <?= $this->include('episode/_partials/navigation') ?>

View File

@ -141,9 +141,9 @@
<div class="col-start-2 px-8 py-4 text-white bg-header"> <div class="col-start-2 px-8 py-4 text-white bg-header">
<h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2> <h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2>
<?php if (substr_count($episode->description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?> <?php if (substr_count($episode->description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?>
<x-SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></x-SeeMore> <x-SeeMore class="max-w-xl prose-sm text-white"><?= $episode->description_html ?></x-SeeMore>
<?php else: ?> <?php else: ?>
<div class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></div> <div class="max-w-xl prose-sm text-white"><?= $episode->description_html ?></div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?= $this->include('episode/_partials/navigation') ?> <?= $this->include('episode/_partials/navigation') ?>