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;
use App\Entities\Clip\Soundbite;
use App\Libraries\SimpleRSSElement;
use App\Models\ClipModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
@ -29,14 +28,13 @@ use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Modules\Analytics\OP3;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Image;
use Modules\Media\Entities\Transcript;
use Modules\Media\Models\MediaModel;
use Override;
use RuntimeException;
use SimpleXMLElement;
/**
* @property int $id
@ -73,8 +71,6 @@ use SimpleXMLElement;
* @property string|null $location_name
* @property string|null $location_geo
* @property string|null $location_osm
* @property array|null $custom_rss
* @property string $custom_rss_string
* @property bool $is_published_on_hubs
* @property int $posts_count
* @property int $comments_count
@ -94,19 +90,19 @@ use SimpleXMLElement;
*/
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 string $audio_url;
protected string $audio_web_url;
protected string $audio_opengraph_url;
protected string $embed_url;
protected string $embed_url = '';
protected ?Image $cover = null;
@ -140,8 +136,6 @@ class Episode extends Entity
protected ?Location $location = null;
protected string $custom_rss_string;
protected ?string $publication_status = null;
/**
@ -176,7 +170,6 @@ class Episode extends Entity
'location_name' => '?string',
'location_geo' => '?string',
'location_osm' => '?string',
'custom_rss' => '?json-array',
'is_published_on_hubs' => 'boolean',
'posts_count' => 'integer',
'comments_count' => 'integer',
@ -185,6 +178,31 @@ class Episode extends Entity
'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
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
@ -342,40 +360,6 @@ class Episode extends Entity
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.
*/
@ -468,11 +452,6 @@ class Episode extends Entity
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
{
return $theme
@ -482,7 +461,7 @@ class Episode extends Entity
public function setGuid(?string $guid = null): static
{
$this->attributes['guid'] = $guid ?? $this->getLink();
$this->attributes['guid'] = $guid ?? $this->link;
return $this;
}
@ -513,34 +492,6 @@ class Episode extends Entity
return $this;
}
public function getDescriptionHtml(?string $serviceSlug = null): string
{
$descriptionHtml = '';
if (
$this->getPodcast()
->partner_id !== null &&
$this->getPodcast()
->partner_link_url !== null &&
$this->getPodcast()
->partner_image_url !== null
) {
$descriptionHtml .= "<div><a href=\"{$this->getPartnerLink(
$serviceSlug,
)}\" rel=\"sponsored noopener noreferrer\" target=\"_blank\"><img src=\"{$this->getPartnerImageUrl(
$serviceSlug,
)}\" alt=\"Partner image\" /></a></div>";
}
$descriptionHtml .= $this->attributes['description_html'];
if ($this->getPodcast()->episode_description_footer_html) {
$descriptionHtml .= "<footer>{$this->getPodcast()
->episode_description_footer_html}</footer>";
}
return $descriptionHtml;
}
public function getDescription(): string
{
if ($this->description === null) {
@ -609,91 +560,6 @@ class Episode extends Entity
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
{
if ($this->preview_id === null) {

View File

@ -10,7 +10,6 @@ declare(strict_types=1);
namespace App\Entities;
use App\Libraries\SimpleRSSElement;
use App\Models\ActorModel;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
@ -62,12 +61,8 @@ use RuntimeException;
* @property string|null $publisher
* @property string $owner_name
* @property string $owner_email
* @property bool $is_owner_email_removed_from_feed
* @property string $type
* @property string $medium
* @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_completed
* @property bool $is_locked
@ -77,15 +72,7 @@ use RuntimeException;
* @property string|null $location_name
* @property string|null $location_geo
* @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 string|null $partner_id
* @property string|null $partner_link_url
* @property string|null $partner_image_url
* @property int $created_by
* @property int $updated_by
* @property string $publication_status
@ -166,8 +153,6 @@ class Podcast extends Entity
protected ?Location $location = null;
protected string $custom_rss_string;
protected ?string $publication_status = null;
/**
@ -180,44 +165,35 @@ class Podcast extends Entity
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'guid' => 'string',
'actor_id' => 'integer',
'handle' => 'string',
'title' => 'string',
'description_markdown' => 'string',
'description_html' => 'string',
'cover_id' => 'int',
'banner_id' => '?int',
'language_code' => 'string',
'category_id' => 'integer',
'parental_advisory' => '?string',
'publisher' => '?string',
'owner_name' => 'string',
'owner_email' => 'string',
'is_owner_email_removed_from_feed' => 'boolean',
'type' => 'string',
'medium' => 'string',
'copyright' => '?string',
'episode_description_footer_markdown' => '?string',
'episode_description_footer_html' => '?string',
'is_blocked' => 'boolean',
'is_completed' => 'boolean',
'is_locked' => 'boolean',
'is_premium_by_default' => 'boolean',
'imported_feed_url' => '?string',
'new_feed_url' => '?string',
'location_name' => '?string',
'location_geo' => '?string',
'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',
'id' => 'integer',
'guid' => 'string',
'actor_id' => 'integer',
'handle' => 'string',
'title' => 'string',
'description_markdown' => 'string',
'description_html' => 'string',
'cover_id' => 'int',
'banner_id' => '?int',
'language_code' => 'string',
'category_id' => 'integer',
'parental_advisory' => '?string',
'publisher' => '?string',
'owner_name' => 'string',
'owner_email' => 'string',
'type' => 'string',
'copyright' => '?string',
'is_blocked' => 'boolean',
'is_completed' => 'boolean',
'is_locked' => 'boolean',
'is_premium_by_default' => 'boolean',
'imported_feed_url' => '?string',
'new_feed_url' => '?string',
'location_name' => '?string',
'location_geo' => '?string',
'location_osm' => '?string',
'is_published_on_hubs' => 'boolean',
'created_by' => 'integer',
'updated_by' => 'integer',
];
public function getAtHandle(): string
@ -454,42 +430,6 @@ class Podcast extends Entity
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
{
if ($this->description === null) {
@ -638,68 +578,9 @@ class Podcast extends Entity
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
{
// podcast is premium if at least one of its episodes is set as premium
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\Location;
use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement;
use App\Libraries\RssFeed;
use App\Models\PodcastModel;
use CodeIgniter\I18n\Time;
use Config\Mimes;
@ -37,21 +37,13 @@ if (! function_exists('get_rss_feed')) {
$episodes = $podcast->episodes;
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$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>"
);
$rss = new RssFeed();
$plugins->rssBeforeChannel($podcast);
$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('rel', 'self');
$atomLink->addAttribute('type', 'application/rss+xml');
@ -60,14 +52,14 @@ if (! function_exists('get_rss_feed')) {
$websubHubs = config('WebSub')
->hubs;
foreach ($websubHubs as $websubHub) {
$atomLinkHub = $channel->addChild('link', null, $atomNamespace);
$atomLinkHub = $channel->addChild('link', null, RssFeed::ATOM_NAMESPACE);
$atomLinkHub->addAttribute('href', $websubHub);
$atomLinkHub->addAttribute('rel', 'hub');
$atomLinkHub->addAttribute('type', 'application/rss+xml');
}
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
@ -85,19 +77,17 @@ if (! function_exists('get_rss_feed')) {
(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->addChildWithCDATA('description', $podcast->description_html);
$channel->addChild('medium', $podcast->medium, $podcastNamespace);
$itunesImage = $channel->addChild('image', null, $itunesNamespace);
$itunesImage = $channel->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
$itunesImage->addAttribute('href', $podcast->cover->feed_url);
$channel->addChild('language', $podcast->language_code);
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) {
$locationElement->addAttribute('geo', $podcast->location->geo);
}
@ -107,38 +97,16 @@ if (! function_exists('get_rss_feed')) {
}
}
if ($podcast->payment_pointer !== null) {
$valueElement = $channel->addChild('value', null, $podcastNamespace);
$valueElement->addAttribute('type', 'webmonetization');
$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');
}
$channel
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', RssFeed::PODCAST_NAMESPACE)
->addAttribute('owner', $podcast->owner_email);
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) {
$podcastingPlatformElement = $channel->addChild('id', null, $podcastNamespace);
$podcastingPlatformElement = $channel->addChild('id', null, RssFeed::PODCAST_NAMESPACE);
$podcastingPlatformElement->addAttribute('platform', $podcastingPlatform->slug);
if ($podcastingPlatform->account_id !== null) {
$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('platform', 'castopod');
$castopodSocialElement->addAttribute('protocol', 'activitypub');
@ -157,7 +125,7 @@ if (! function_exists('get_rss_feed')) {
$castopodSocialElement->addAttribute('accountUrl', $podcast->link);
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('platform', $socialPlatform->slug);
@ -181,7 +149,7 @@ if (! function_exists('get_rss_feed')) {
}
if ($socialPlatform->slug === 'mastodon') {
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, $podcastNamespace);
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, RssFeed::PODCAST_NAMESPACE);
$socialSignUpelement->addAttribute('priority', '1');
$socialSignUpelement->addAttribute(
'homeUrl',
@ -200,7 +168,7 @@ if (! function_exists('get_rss_feed')) {
$castopodSocialSignUpelement = $castopodSocialElement->addChild(
'socialSignUp',
null,
$podcastNamespace
RssFeed::PODCAST_NAMESPACE
);
$castopodSocialSignUpelement->addAttribute('priority', '1');
$castopodSocialSignUpelement->addAttribute(
@ -224,7 +192,7 @@ if (! function_exists('get_rss_feed')) {
$fundingPlatformElement = $channel->addChild(
'funding',
$fundingPlatform->account_id,
$podcastNamespace,
RssFeed::PODCAST_NAMESPACE,
);
$fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug);
if ($fundingPlatform->link_url !== null) {
@ -234,7 +202,7 @@ if (! function_exists('get_rss_feed')) {
foreach ($podcast->persons as $person) {
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'));
@ -263,29 +231,26 @@ if (! function_exists('get_rss_feed')) {
$channel->addChild(
'explicit',
$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);
$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) {
$owner->addChild('email', $podcast->owner_email, $itunesNamespace);
}
$channel->addChild('type', $podcast->type, $itunesNamespace);
$channel->addChild('type', $podcast->type, RssFeed::ITUNES_NAMESPACE);
$podcast->copyright &&
$channel->addChild('copyright', $podcast->copyright);
if ($podcast->is_blocked || $subscription instanceof Subscription) {
$channel->addChild('block', 'Yes', $itunesNamespace);
$channel->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
if ($podcast->is_completed) {
$channel->addChild('complete', 'Yes', $itunesNamespace);
$channel->addChild('complete', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
$image = $channel->addChild('image');
@ -293,12 +258,6 @@ if (! function_exists('get_rss_feed')) {
$image->addChild('title', $podcast->title, null, false);
$image->addChild('link', $podcast->link);
if ($podcast->custom_rss !== null) {
array_to_rss([
'elements' => $podcast->custom_rss,
], $channel);
}
// run plugins hook at the end
$plugins->rssAfterChannel($podcast, $channel);
@ -328,7 +287,7 @@ if (! function_exists('get_rss_feed')) {
$item->addChild('guid', $episode->guid);
$item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123));
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) {
$locationElement->addAttribute('geo', $episode->location->geo);
}
@ -338,10 +297,10 @@ if (! function_exists('get_rss_feed')) {
}
}
$item->addChildWithCDATA('description', $episode->getDescriptionHtml($serviceSlug));
$item->addChild('duration', (string) round($episode->audio->duration), $itunesNamespace);
$item->addChildWithCDATA('description', $episode->description_html);
$item->addChild('duration', (string) round($episode->audio->duration), RssFeed::ITUNES_NAMESPACE);
$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);
$episode->parental_advisory &&
@ -350,18 +309,18 @@ if (! function_exists('get_rss_feed')) {
$episode->parental_advisory === 'explicit'
? 'true'
: 'false',
$itunesNamespace,
RssFeed::ITUNES_NAMESPACE,
);
$episode->number &&
$item->addChild('episode', (string) $episode->number, $itunesNamespace);
$item->addChild('episode', (string) $episode->number, RssFeed::ITUNES_NAMESPACE);
$episode->season_number &&
$item->addChild('season', (string) $episode->season_number, $itunesNamespace);
$item->addChild('episodeType', $episode->type, $itunesNamespace);
$item->addChild('season', (string) $episode->season_number, RssFeed::ITUNES_NAMESPACE);
$item->addChild('episodeType', $episode->type, RssFeed::ITUNES_NAMESPACE);
// If episode is of type trailer, add podcast:trailer tag on channel level
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(
'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
$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('contentType', 'application/podcast-activity+json');
if ($episode->getPosts()) {
$socialInteractUri = $episode->getPosts()[0]
->uri;
$socialInteractElement = $item->addChild('socialInteract', null, $podcastNamespace);
$socialInteractElement = $item->addChild('socialInteract', null, RssFeed::PODCAST_NAMESPACE);
$socialInteractElement->addAttribute('uri', $socialInteractUri);
$socialInteractElement->addAttribute('priority', '1');
$socialInteractElement->addAttribute('platform', 'castopod');
@ -405,7 +358,7 @@ if (! function_exists('get_rss_feed')) {
}
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(
'type',
@ -420,21 +373,21 @@ if (! function_exists('get_rss_feed')) {
}
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('type', 'application/json+chapters');
}
foreach ($episode->soundbites as $soundbite) {
// 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('duration', (string) round($soundbite->duration, 3));
}
foreach ($episode->persons as $person) {
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(
'role',
@ -455,13 +408,7 @@ if (! function_exists('get_rss_feed')) {
}
if ($episode->is_blocked) {
$item->addChild('block', 'Yes', $itunesNamespace);
}
if ($episode->custom_rss !== null) {
array_to_rss([
'elements' => $episode->custom_rss,
], $item);
$item->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
$plugins->rssAfterItem($episode, $item);
@ -477,9 +424,7 @@ if (! function_exists('add_category_tag')) {
*/
function add_category_tag(SimpleXMLElement $node, Category $category): void
{
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$itunesCategory = $node->addChild('category', null, $itunesNamespace);
$itunesCategory = $node->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
$itunesCategory->addAttribute(
'text',
$category->parent instanceof Category
@ -488,7 +433,7 @@ if (! function_exists('add_category_tag')) {
);
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);
$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
*
* @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)) {
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)),
]);
if ($podcast->payment_pointer) {
$metatags->meta('monetization', $podcast->payment_pointer);
}
return '<link type="application/rss+xml" rel="alternate" title="' . esc(
$podcast->title
) . '" 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),
]);
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(
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug)
) . '" title="' . esc(

View File

@ -14,8 +14,31 @@ use DOMDocument;
use Override;
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
*
@ -67,4 +90,37 @@ class SimpleRSSElement extends SimpleXMLElement
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_geo',
'location_osm',
'custom_rss',
'is_published_on_hubs',
'posts_count',
'comments_count',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,18 +9,18 @@ use ViewComponents\Component;
class Pill extends Component
{
protected array $props = ['size', 'variant', 'icon', 'iconClass', 'hint'];
/**
* @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 array $props = ['size', 'variant', 'icon', 'iconClass', 'hint'];
protected string $iconClass = '';
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->get('/', 'PodcastController::viewAnalytics/$1', [
'as' => 'podcast-analytics',

View File

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

View File

@ -213,18 +213,14 @@ class PodcastController extends BaseController
'parental_advisory' => $this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null,
'owner_name' => $this->request->getPost('owner_name'),
'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'),
'type' => $this->request->getPost('type'),
'medium' => $this->request->getPost('medium'),
'copyright' => $this->request->getPost('copyright'),
'location' => $this->request->getPost('location_name') === '' ? null : new Location(
'owner_name' => $this->request->getPost('owner_name'),
'owner_email' => $this->request->getPost('owner_email'),
'publisher' => $this->request->getPost('publisher'),
'type' => $this->request->getPost('type'),
'copyright' => $this->request->getPost('copyright'),
'location' => $this->request->getPost('location_name') === '' ? null : new Location(
$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_completed' => $this->request->getPost('complete') === 'yes',
'is_locked' => $this->request->getPost('lock') === 'yes',
@ -253,10 +249,6 @@ class PodcastController extends BaseController
$this->request->getPost('other_categories') ?? [],
);
// OP3
service('settings')
->set('Analytics.enableOP3', $this->request->getPost('enable_op3') === 'yes', 'podcast:' . $newPodcastId);
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId])->with(
@ -314,19 +306,11 @@ class PodcastController extends BaseController
$this->podcast->publisher = $this->request->getPost('publisher');
$this->podcast->owner_name = $this->request->getPost('owner_name');
$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->medium = $this->request->getPost('medium');
$this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->location = $this->request->getPost('location_name') === '' ? null : new Location(
$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(
'new_feed_url'
);
@ -359,14 +343,6 @@ class PodcastController extends BaseController
$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();
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
{
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]*
*/
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()
->file_url;
$episode->audio_url = $episode->getAudioUrl();
$episode->duration = round($episode->audio->duration);
return $episode;

View File

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

View File

@ -6,7 +6,7 @@ namespace Modules\Plugins\Core;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement;
use App\Libraries\RssFeed;
use CodeIgniter\HTTP\URI;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Event\DocumentParsedEvent;
@ -86,7 +86,7 @@ abstract class BasePlugin implements PluginInterface
}
#[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]
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\Podcast;
use App\Libraries\SimpleRSSElement;
use App\Libraries\RssFeed;
interface PluginInterface
{
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 rssAfterItem(Episode $episode, SimpleRSSElement $item): void;
public function rssAfterItem(Episode $episode, RssFeed $item): void;
public function siteHead(): void;
}

View File

@ -6,15 +6,15 @@ namespace Modules\Plugins\Core;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement;
use App\Libraries\RssFeed;
use Config\Database;
use Modules\Plugins\Config\Plugins as PluginsConfig;
/**
* @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 rssAfterItem(Episode $episode, SimpleRSSElement $item)
* @method void rssAfterItem(Episode $episode, RssFeed $item)
* @method void siteHead()
*/
class Plugins
@ -28,25 +28,27 @@ class Plugins
'checkbox' => ['permit_empty'],
'datetime' => ['valid_date[Y-m-d H:i]'],
'email' => ['valid_email'],
'group' => ['permit_empty', 'is_list'],
'markdown' => ['string'],
'number' => ['integer'],
'radio-group' => ['string'],
'rss' => ['string'],
'select' => ['string'],
'select-multiple' => ['permit_empty', 'is_list'],
'text' => ['string'],
'textarea' => ['string'],
'toggler' => ['permit_empty'],
'url' => ['valid_url_strict'],
'group' => ['permit_empty', 'is_list'],
];
public const FIELDS_CASTS = [
'checkbox' => 'bool',
'datetime' => 'datetime',
'markdown' => 'markdown',
'number' => 'int',
'rss' => 'rss',
'toggler' => 'bool',
'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;
/**
* @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 $label
* @property string $hint
@ -20,7 +20,7 @@ use Override;
class Field extends ManifestObject
{
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',
'label' => 'required|string',
'hint' => 'permit_empty|string',

View File

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

View File

@ -76,6 +76,7 @@
"monetization",
"podcasting2",
"privacy",
"productivity",
"seo"
]
}
@ -173,13 +174,14 @@
"properties": {
"type": {
"enum": [
"group",
"checkbox",
"datetime",
"email",
"group",
"markdown",
"number",
"radio-group",
"rss",
"select-multiple",
"select",
"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
{
return [
'id' => 1,
'guid' => '0d341200-0234-5de7-99a6-a7d02bea4ce2',
'actor_id' => 1,
'handle' => 'Handle',
'title' => 'Title',
'description_markdown' => 'description',
'description_html' => '<p>description</p>',
'cover_id' => 1,
'banner_id' => 2,
'language_code' => 'en',
'category_id' => 1,
'parental_advisory' => null,
'owner_name' => 'Owner',
'owner_email' => 'Owner@gmail.com',
'publisher' => '',
'type' => 'episodic',
'copyright' => '',
'episode_description_footer_markdown' => null,
'episode_description_footer_html' => null,
'is_blocked' => 0,
'is_completed' => 0,
'is_locked' => 1,
'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' => 0,
'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',
'id' => 1,
'guid' => '0d341200-0234-5de7-99a6-a7d02bea4ce2',
'actor_id' => 1,
'handle' => 'Handle',
'title' => 'Title',
'description_markdown' => 'description',
'description_html' => '<p>description</p>',
'cover_id' => 1,
'banner_id' => 2,
'language_code' => 'en',
'category_id' => 1,
'parental_advisory' => null,
'owner_name' => 'Owner',
'owner_email' => 'Owner@gmail.com',
'publisher' => '',
'type' => 'episodic',
'copyright' => '',
'is_blocked' => 0,
'is_completed' => 0,
'is_locked' => 1,
'imported_feed_url' => null,
'new_feed_url' => null,
'location_name' => null,
'location_geo' => null,
'location_osm' => null,
'is_published_on_hubs' => 0,
'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
{
@ -176,7 +169,6 @@ class FakeSinglePodcastApiSeeder extends Seeder
'location_name' => null,
'location_geo' => null,
'location_osm' => null,
'custom_rss' => null,
'is_published_on_hubs' => false,
'posts_count' => 0,
'comments_count' => 0,

View File

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

View File

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

View File

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

View File

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

View File

@ -127,14 +127,6 @@
isRequired="true"
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 title="<?= lang('Episode.form.premium_title') ?>">
@ -211,12 +203,6 @@
title="<?= lang('Episode.form.advanced_section_title') ?>"
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>

View File

@ -132,14 +132,6 @@
isRequired="true"
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 title="<?= lang('Episode.form.premium_title') ?>" >
@ -279,13 +271,6 @@
title="<?= lang('Episode.form.advanced_section_title') ?>"
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>

View File

@ -118,6 +118,18 @@ case 'markdown': ?>
value="<?= $value ?>"
/>
<?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': ?>
<x-Forms.Field
as="DatetimePicker"

View File

@ -67,13 +67,11 @@ $podcastNavigation = [
'subscription-list',
'subscription-create',
'platforms-funding',
'podcast-monetization-other',
],
'items-permissions' => [
'subscription-list' => 'manage-subscriptions',
'subscription-create' => 'manage-subscriptions',
'platforms-funding' => 'manage-platforms',
'podcast-monetization-other' => 'edit',
'subscription-list' => 'manage-subscriptions',
'subscription-create' => 'manage-subscriptions',
'platforms-funding' => 'manage-platforms',
],
],
'contributors' => [
@ -135,15 +133,6 @@ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) {
'class' => 'text-sm opacity-60',
]) ?>
</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>

View File

@ -58,29 +58,6 @@
])) ?>"
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
@ -148,9 +125,6 @@
hint="<?= esc(lang('Podcast.form.owner_email_hint')) ?>"
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
name="publisher"
label="<?= esc(lang('Podcast.form.publisher')) ?>"
@ -188,16 +162,6 @@
<?= lang('Podcast.form.premium_by_default') ?></x-Forms.Toggler>
</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
title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
@ -212,21 +176,6 @@
<x-Forms.Section
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')) ?>">
<?= lang('Podcast.form.lock') ?>
</x-Forms.Toggler>

View File

@ -83,29 +83,6 @@
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
@ -178,9 +155,6 @@
hint="<?= esc(lang('Podcast.form.owner_email_hint')) ?>"
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
name="publisher"
label="<?= esc(lang('Podcast.form.publisher')) ?>"
@ -221,17 +195,6 @@
<?= lang('Podcast.form.premium_by_default') ?></x-Forms.Toggler>
</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
title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
@ -248,23 +211,6 @@
title="<?= lang('Podcast.form.advanced_section_title') ?>"
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
name="new_feed_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">
<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): ?>
<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: ?>
<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; ?>
</div>
<?= $this->include('episode/_partials/navigation') ?>

View File

@ -141,9 +141,9 @@
<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>
<?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: ?>
<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; ?>
</div>
<?= $this->include('episode/_partials/navigation') ?>