Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found
Select Git revision
  • alpha
  • beta
  • develop
  • docs/fix-readme
  • docs/update-vitepress
  • draft/rss-feed
  • feat/dashboard
  • feat/episodes-page-ux
  • feat/generator-user-agent
  • feat/headliner
  • feat/new-languages
  • feat/plugins
  • fix/federation
  • fix/forms-ux
  • i18n
  • main
  • next
  • refactor/transcripts
  • v1.0.0
  • v1.0.0-alpha.1
  • v1.0.0-alpha.10
  • v1.0.0-alpha.11
  • v1.0.0-alpha.12
  • v1.0.0-alpha.13
  • v1.0.0-alpha.14
  • v1.0.0-alpha.15
  • v1.0.0-alpha.16
  • v1.0.0-alpha.17
  • v1.0.0-alpha.18
  • v1.0.0-alpha.19
  • v1.0.0-alpha.2
  • v1.0.0-alpha.20
  • v1.0.0-alpha.21
  • v1.0.0-alpha.22
  • v1.0.0-alpha.23
  • v1.0.0-alpha.24
  • v1.0.0-alpha.25
  • v1.0.0-alpha.26
  • v1.0.0-alpha.27
  • v1.0.0-alpha.28
  • v1.0.0-alpha.29
  • v1.0.0-alpha.3
  • v1.0.0-alpha.30
  • v1.0.0-alpha.31
  • v1.0.0-alpha.32
  • v1.0.0-alpha.33
  • v1.0.0-alpha.34
  • v1.0.0-alpha.35
  • v1.0.0-alpha.36
  • v1.0.0-alpha.37
  • v1.0.0-alpha.38
  • v1.0.0-alpha.39
  • v1.0.0-alpha.4
  • v1.0.0-alpha.40
  • v1.0.0-alpha.41
  • v1.0.0-alpha.42
  • v1.0.0-alpha.43
  • v1.0.0-alpha.44
  • v1.0.0-alpha.45
  • v1.0.0-alpha.46
  • v1.0.0-alpha.47
  • v1.0.0-alpha.48
  • v1.0.0-alpha.49
  • v1.0.0-alpha.5
  • v1.0.0-alpha.50
  • v1.0.0-alpha.51
  • v1.0.0-alpha.52
  • v1.0.0-alpha.53
  • v1.0.0-alpha.54
  • v1.0.0-alpha.55
  • v1.0.0-alpha.56
  • v1.0.0-alpha.57
  • v1.0.0-alpha.58
  • v1.0.0-alpha.59
  • v1.0.0-alpha.6
  • v1.0.0-alpha.60
  • v1.0.0-alpha.61
  • v1.0.0-alpha.62
  • v1.0.0-alpha.63
  • v1.0.0-alpha.64
  • v1.0.0-alpha.65
  • v1.0.0-alpha.66
  • v1.0.0-alpha.67
  • v1.0.0-alpha.68
  • v1.0.0-alpha.69
  • v1.0.0-alpha.7
  • v1.0.0-alpha.70
  • v1.0.0-alpha.71
  • v1.0.0-alpha.72
  • v1.0.0-alpha.73
  • v1.0.0-alpha.74
  • v1.0.0-alpha.75
  • v1.0.0-alpha.76
  • v1.0.0-alpha.77
  • v1.0.0-alpha.78
  • v1.0.0-alpha.79
  • v1.0.0-alpha.8
  • v1.0.0-alpha.80
  • v1.0.0-alpha.9
  • v1.0.0-beta.1
  • v1.0.0-beta.10
  • v1.0.0-beta.11
  • v1.0.0-beta.12
  • v1.0.0-beta.13
  • v1.0.0-beta.14
  • v1.0.0-beta.15
  • v1.0.0-beta.16
  • v1.0.0-beta.17
  • v1.0.0-beta.18
  • v1.0.0-beta.19
  • v1.0.0-beta.2
  • v1.0.0-beta.20
  • v1.0.0-beta.21
  • v1.0.0-beta.22
  • v1.0.0-beta.23
  • v1.0.0-beta.24
  • v1.0.0-beta.3
  • v1.0.0-beta.4
118 results

Target

Select target project
  • adaures/castopod
  • mkljczk/castopod-host
  • spaetz/castopod-host
  • PatrykMis/castopod
  • jonas/castopod
  • ajeremias/castopod
  • misuzu/castopod
  • KrzysztofDomanczyk/castopod
  • Behel/castopod
  • nebulon/castopod
  • ewen/castopod
  • NeoluxConsulting/castopod
  • nateritter/castopod-og
  • prcutler/castopod
14 results
Select Git revision
  • alpha
  • beta
  • develop
  • docs/starlight
  • docs/update-vitepress
  • feat/dashboard
  • feat/episodes-page-ux
  • feat/headliner
  • feat/new-languages
  • fix/federation
  • fix/forms-ux
  • i18n
  • main
  • v1.0.0
  • v1.0.0-alpha.1
  • v1.0.0-alpha.10
  • v1.0.0-alpha.11
  • v1.0.0-alpha.12
  • v1.0.0-alpha.13
  • v1.0.0-alpha.14
  • v1.0.0-alpha.15
  • v1.0.0-alpha.16
  • v1.0.0-alpha.17
  • v1.0.0-alpha.18
  • v1.0.0-alpha.19
  • v1.0.0-alpha.2
  • v1.0.0-alpha.20
  • v1.0.0-alpha.21
  • v1.0.0-alpha.22
  • v1.0.0-alpha.23
  • v1.0.0-alpha.24
  • v1.0.0-alpha.25
  • v1.0.0-alpha.26
  • v1.0.0-alpha.27
  • v1.0.0-alpha.28
  • v1.0.0-alpha.29
  • v1.0.0-alpha.3
  • v1.0.0-alpha.30
  • v1.0.0-alpha.31
  • v1.0.0-alpha.32
  • v1.0.0-alpha.33
  • v1.0.0-alpha.34
  • v1.0.0-alpha.35
  • v1.0.0-alpha.36
  • v1.0.0-alpha.37
  • v1.0.0-alpha.38
  • v1.0.0-alpha.39
  • v1.0.0-alpha.4
  • v1.0.0-alpha.40
  • v1.0.0-alpha.41
  • v1.0.0-alpha.42
  • v1.0.0-alpha.43
  • v1.0.0-alpha.44
  • v1.0.0-alpha.45
  • v1.0.0-alpha.46
  • v1.0.0-alpha.47
  • v1.0.0-alpha.48
  • v1.0.0-alpha.49
  • v1.0.0-alpha.5
  • v1.0.0-alpha.50
  • v1.0.0-alpha.51
  • v1.0.0-alpha.52
  • v1.0.0-alpha.53
  • v1.0.0-alpha.54
  • v1.0.0-alpha.55
  • v1.0.0-alpha.56
  • v1.0.0-alpha.57
  • v1.0.0-alpha.58
  • v1.0.0-alpha.59
  • v1.0.0-alpha.6
  • v1.0.0-alpha.60
  • v1.0.0-alpha.61
  • v1.0.0-alpha.62
  • v1.0.0-alpha.63
  • v1.0.0-alpha.64
  • v1.0.0-alpha.65
  • v1.0.0-alpha.66
  • v1.0.0-alpha.67
  • v1.0.0-alpha.68
  • v1.0.0-alpha.69
  • v1.0.0-alpha.7
  • v1.0.0-alpha.70
  • v1.0.0-alpha.71
  • v1.0.0-alpha.72
  • v1.0.0-alpha.73
  • v1.0.0-alpha.74
  • v1.0.0-alpha.75
  • v1.0.0-alpha.76
  • v1.0.0-alpha.77
  • v1.0.0-alpha.78
  • v1.0.0-alpha.79
  • v1.0.0-alpha.8
  • v1.0.0-alpha.80
  • v1.0.0-alpha.9
  • v1.0.0-beta.1
  • v1.0.0-beta.10
  • v1.0.0-beta.11
  • v1.0.0-beta.12
  • v1.0.0-beta.13
  • v1.0.0-beta.14
  • v1.0.0-beta.15
  • v1.0.0-beta.16
  • v1.0.0-beta.17
  • v1.0.0-beta.18
  • v1.0.0-beta.19
  • v1.0.0-beta.2
  • v1.0.0-beta.20
  • v1.0.0-beta.21
  • v1.0.0-beta.22
  • v1.0.0-beta.23
  • v1.0.0-beta.24
  • v1.0.0-beta.3
  • v1.0.0-beta.4
113 results
Show changes
Showing
with 398 additions and 668 deletions
...@@ -3,26 +3,25 @@ ...@@ -3,26 +3,25 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* Class AppSeeder Calls all required seeders for castopod to work properly
*
* @copyright 2020 Ad Aures * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Controllers; namespace App\Database\Seeds;
use App\Models\PlatformModel; use CodeIgniter\Database\Seeder;
use CodeIgniter\Controller; use Override;
use CodeIgniter\HTTP\ResponseInterface;
/* class DevSeeder extends Seeder
* Provide public access to all platforms so that they can be exported
*/
class PlatformController extends Controller
{ {
public function index(): ResponseInterface #[Override]
public function run(): void
{ {
$model = new PlatformModel(); $this->call('CategorySeeder');
$this->call('LanguageSeeder');
return $this->response->setJSON($model->getPlatforms()); $this->call('DevSuperadminSeeder');
} }
} }
...@@ -13,37 +13,38 @@ declare(strict_types=1); ...@@ -13,37 +13,38 @@ declare(strict_types=1);
namespace App\Database\Seeds; namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Models\UserModel;
use Override;
class TestSeeder extends Seeder class DevSuperadminSeeder extends Seeder
{ {
#[Override]
public function run(): void public function run(): void
{ {
helper('setting'); if ((new UserModel())->where('is_owner', true)->first() instanceof User) {
return;
}
/** /**
* Inserts an owner with the following credentials: admin: `admin@example.com` password: `AGUehL3P` * Inserts an owner with the following credentials: admin: `admin@example.com` password: `castopod`
*/ */
$this->db->table('users')
->insert([ // Get the User Provider (UserModel by default)
'id' => 1, $users = auth()
'username' => 'admin', ->getProvider();
'is_owner' => 1,
]); $user = new User([
'username' => 'admin',
$this->db->table('auth_identities') 'email' => 'admin@castopod.local',
->insert([ 'password' => 'castopod',
'id' => 1, 'is_owner' => true,
'user_id' => 1, ]);
'type' => 'email_password', $users->save($user);
'secret' => 'admin@example.com',
'secret2' => '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6', // To get the complete user object with ID, we need to get from the database
]); $user = $users->findById($users->getInsertID());
$this->db $user->addGroup(setting('AuthGroups.mostPowerfulGroup'));
->table('auth_groups_users')
->insert([
'user_id' => 1,
'group' => setting('AuthGroups.mostPowerfulGroup'),
]);
} }
} }
...@@ -19,11 +19,12 @@ use App\Models\PodcastModel; ...@@ -19,11 +19,12 @@ use App\Models\PodcastModel;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use Exception; use Exception;
use GeoIp2\Database\Reader; use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException; use GeoIp2\Exception\AddressNotFoundException;
use Override;
class FakePodcastsAnalyticsSeeder extends Seeder class FakePodcastsAnalyticsSeeder extends Seeder
{ {
#[Override]
public function run(): void public function run(): void
{ {
$jsonUserAgents = json_decode( $jsonUserAgents = json_decode(
...@@ -78,34 +79,32 @@ class FakePodcastsAnalyticsSeeder extends Seeder ...@@ -78,34 +79,32 @@ class FakePodcastsAnalyticsSeeder extends Seeder
for ( for (
$lineNumber = 0; $lineNumber = 0;
$lineNumber < rand(1, (int) $probability1); $lineNumber < random_int(1, (int) $probability1);
++$lineNumber ++$lineNumber
) { ) {
$probability2 = floor(exp(6 - $age / 20)) + 10; $probability2 = floor(exp(6 - $age / 20)) + 10;
$player = $player =
$jsonUserAgents[ $jsonUserAgents[
rand(1, count($jsonUserAgents) - 1) random_int(1, count($jsonUserAgents) - 1)
]; ];
$service = $service =
$jsonRSSUserAgents[ $jsonRSSUserAgents[
rand(1, count($jsonRSSUserAgents) - 1) random_int(1, count($jsonRSSUserAgents) - 1)
]['slug']; ]['slug'];
$app = isset($player['app']) ? $player['app'] : ''; $app = $player['app'] ?? '';
$device = isset($player['device']) $device = $player['device'] ?? '';
? $player['device'] $os = $player['os'] ?? '';
: ''; $isBot = $player['bot'] ?? 0;
$os = isset($player['os']) ? $player['os'] : '';
$isBot = isset($player['bot']) ? $player['bot'] : 0;
$fakeIp = $fakeIp =
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255); random_int(0, 255);
$cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb'); $cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb');
...@@ -116,9 +115,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder ...@@ -116,9 +115,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
try { try {
$city = $cityReader->city($fakeIp); $city = $cityReader->city($fakeIp);
$countryCode = $city->country->isoCode === null $countryCode = $city->country->isoCode ?? 'N/A';
? 'N/A'
: $city->country->isoCode;
$regionCode = $city->subdivisions === [] $regionCode = $city->subdivisions === []
? 'N/A' ? 'N/A'
...@@ -129,20 +126,20 @@ class FakePodcastsAnalyticsSeeder extends Seeder ...@@ -129,20 +126,20 @@ class FakePodcastsAnalyticsSeeder extends Seeder
//Bad luck, bad IP, nothing to do. //Bad luck, bad IP, nothing to do.
} }
$hits = rand(0, (int) $probability2); $hits = random_int(0, (int) $probability2);
$analyticsPodcasts[] = [ $analyticsPodcasts[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'duration' => rand(60, 3600), 'duration' => random_int(60, 3600),
'bandwidth' => rand(1000000, 10000000), 'bandwidth' => random_int(1000000, 10000000),
'hits' => $hits, 'hits' => $hits,
'unique_listeners' => $hits, 'unique_listeners' => $hits,
]; ];
$analyticsPodcastsByHour[] = [ $analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'hour' => rand(0, 23), 'hour' => random_int(0, 23),
'hits' => $hits, 'hits' => $hits,
]; ];
$analyticsPodcastsByCountry[] = [ $analyticsPodcastsByCountry[] = [
......
...@@ -16,9 +16,9 @@ use App\Entities\Episode; ...@@ -16,9 +16,9 @@ use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use Exception; use Exception;
use Override;
class FakeWebsiteAnalyticsSeeder extends Seeder class FakeWebsiteAnalyticsSeeder extends Seeder
{ {
...@@ -182,6 +182,7 @@ class FakeWebsiteAnalyticsSeeder extends Seeder ...@@ -182,6 +182,7 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
'WOSBrowser', 'WOSBrowser',
]; ];
#[Override]
public function run(): void public function run(): void
{ {
$podcast = (new PodcastModel())->first(); $podcast = (new PodcastModel())->first();
...@@ -217,23 +218,23 @@ class FakeWebsiteAnalyticsSeeder extends Seeder ...@@ -217,23 +218,23 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
for ( for (
$lineNumber = 0; $lineNumber = 0;
$lineNumber < rand(1, $probability1); $lineNumber < random_int(1, $probability1);
++$lineNumber ++$lineNumber
) { ) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10; $probability2 = (int) floor(exp(6 - $age / 20)) + 10;
$domain = $domain =
$this->domains[rand(0, count($this->domains) - 1)]; $this->domains[random_int(0, count($this->domains) - 1)];
$keyword = $keyword =
$this->keywords[ $this->keywords[
rand(0, count($this->keywords) - 1) random_int(0, count($this->keywords) - 1)
]; ];
$browser = $browser =
$this->browsers[ $this->browsers[
rand(0, count($this->browsers) - 1) random_int(0, count($this->browsers) - 1)
]; ];
$hits = rand(0, $probability2); $hits = random_int(0, $probability2);
$websiteByBrowser[] = [ $websiteByBrowser[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
......
...@@ -18,9 +18,11 @@ declare(strict_types=1); ...@@ -18,9 +18,11 @@ declare(strict_types=1);
namespace App\Database\Seeds; namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use Override;
class LanguageSeeder extends Seeder class LanguageSeeder extends Seeder
{ {
#[Override]
public function run(): void public function run(): void
{ {
$data = [ $data = [
......
...@@ -12,6 +12,7 @@ namespace App\Entities; ...@@ -12,6 +12,7 @@ namespace App\Entities;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use Modules\Fediverse\Entities\Actor as FediverseActor; use Modules\Fediverse\Entities\Actor as FediverseActor;
use Override;
use RuntimeException; use RuntimeException;
/** /**
...@@ -42,6 +43,7 @@ class Actor extends FediverseActor ...@@ -42,6 +43,7 @@ class Actor extends FediverseActor
return $this->podcast; return $this->podcast;
} }
#[Override]
public function getAvatarImageUrl(): string public function getAvatarImageUrl(): string
{ {
if ($this->podcast instanceof Podcast) { if ($this->podcast instanceof Podcast) {
...@@ -51,6 +53,7 @@ class Actor extends FediverseActor ...@@ -51,6 +53,7 @@ class Actor extends FediverseActor
return parent::getAvatarImageUrl(); return parent::getAvatarImageUrl();
} }
#[Override]
public function getAvatarImageMimetype(): string public function getAvatarImageMimetype(): string
{ {
if ($this->podcast instanceof Podcast) { if ($this->podcast instanceof Podcast) {
......
...@@ -36,7 +36,7 @@ use Modules\Media\Models\MediaModel; ...@@ -36,7 +36,7 @@ use Modules\Media\Models\MediaModel;
* @property string $type * @property string $type
* @property int|null $media_id * @property int|null $media_id
* @property Video|Audio|null $media * @property Video|Audio|null $media
* @property array|null $metadata * @property array<mixed>|null $metadata
* @property string $status * @property string $status
* @property string $logs * @property string $logs
* @property User $user * @property User $user
...@@ -84,7 +84,7 @@ class BaseClip extends Entity ...@@ -84,7 +84,7 @@ class BaseClip extends Entity
/** /**
* @param array<string, mixed>|null $data * @param array<string, mixed>|null $data
*/ */
public function __construct(array $data = null) public function __construct(?array $data = null)
{ {
parent::__construct($data); parent::__construct($data);
} }
...@@ -136,7 +136,7 @@ class BaseClip extends Entity ...@@ -136,7 +136,7 @@ class BaseClip extends Entity
$media = new Audio([ $media = new Audio([
'file_key' => $fileKey, 'file_key' => $fileKey,
'language_code' => $this->getPodcast() 'language_code' => $this->getPodcast()
->language_code, ->language_code,
'uploaded_by' => $this->attributes['updated_by'], 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'], 'updated_by' => $this->attributes['updated_by'],
]); ]);
......
...@@ -13,9 +13,10 @@ namespace App\Entities\Clip; ...@@ -13,9 +13,10 @@ namespace App\Entities\Clip;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use Modules\Media\Entities\Video; use Modules\Media\Entities\Video;
use Modules\Media\Models\MediaModel; use Modules\Media\Models\MediaModel;
use Override;
/** /**
* @property array $theme * @property array{name:string,preview:string} $theme
* @property string $format * @property string $format
*/ */
class VideoClip extends BaseClip class VideoClip extends BaseClip
...@@ -25,7 +26,7 @@ class VideoClip extends BaseClip ...@@ -25,7 +26,7 @@ class VideoClip extends BaseClip
/** /**
* @param array<string, mixed>|null $data * @param array<string, mixed>|null $data
*/ */
public function __construct(array $data = null) public function __construct(?array $data = null)
{ {
parent::__construct($data); parent::__construct($data);
...@@ -36,7 +37,7 @@ class VideoClip extends BaseClip ...@@ -36,7 +37,7 @@ class VideoClip extends BaseClip
} }
/** /**
* @param array<string, string> $theme * @param array{name:string,preview:string} $theme
*/ */
public function setTheme(array $theme): self public function setTheme(array $theme): self
{ {
...@@ -63,6 +64,7 @@ class VideoClip extends BaseClip ...@@ -63,6 +64,7 @@ class VideoClip extends BaseClip
return $this; return $this;
} }
#[Override]
public function setMedia(File $file, string $fileKey): static public function setMedia(File $file, string $fileKey): static
{ {
if ($this->attributes['media_id'] !== null) { if ($this->attributes['media_id'] !== null) {
...@@ -73,7 +75,7 @@ class VideoClip extends BaseClip ...@@ -73,7 +75,7 @@ class VideoClip extends BaseClip
$video = new Video([ $video = new Video([
'file_key' => $fileKey, 'file_key' => $fileKey,
'language_code' => $this->getPodcast() 'language_code' => $this->getPodcast()
->language_code, ->language_code,
'uploaded_by' => $this->attributes['created_by'], 'uploaded_by' => $this->attributes['created_by'],
'updated_by' => $this->attributes['created_by'], 'updated_by' => $this->attributes['created_by'],
]); ]);
......
...@@ -11,7 +11,6 @@ declare(strict_types=1); ...@@ -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;
...@@ -22,7 +21,6 @@ use CodeIgniter\Entity\Entity; ...@@ -22,7 +21,6 @@ use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Config\Images;
use Exception; use Exception;
use League\CommonMark\Environment\Environment; use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension; use League\CommonMark\Extension\Autolink\AutolinkExtension;
...@@ -35,8 +33,8 @@ use Modules\Media\Entities\Chapters; ...@@ -35,8 +33,8 @@ 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,12 +71,10 @@ use SimpleXMLElement; ...@@ -73,12 +71,10 @@ 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 $downloads_count
* @property int $posts_count * @property int $posts_count
* @property int $comments_count * @property int $comments_count
* @property int $downloads
* @property EpisodeComment[]|null $comments * @property EpisodeComment[]|null $comments
* @property bool $is_premium * @property bool $is_premium
* @property int $created_by * @property int $created_by
...@@ -94,19 +90,19 @@ use SimpleXMLElement; ...@@ -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 = '';
protected ?Audio $audio = null; public string $audio_web_url = '';
protected string $audio_url; public string $audio_opengraph_url = '';
protected string $audio_web_url; protected Podcast $podcast;
protected string $audio_opengraph_url; protected ?Audio $audio = null;
protected string $embed_url; protected string $embed_url = '';
protected ?Image $cover = null; protected ?Image $cover = null;
...@@ -116,8 +112,6 @@ class Episode extends Entity ...@@ -116,8 +112,6 @@ class Episode extends Entity
protected ?Chapters $chapters = null; protected ?Chapters $chapters = null;
protected int $downloads = 0;
/** /**
* @var Person[]|null * @var Person[]|null
*/ */
...@@ -140,8 +134,6 @@ class Episode extends Entity ...@@ -140,8 +134,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,8 +168,8 @@ class Episode extends Entity ...@@ -176,8 +168,8 @@ 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',
'downloads_count' => 'integer',
'posts_count' => 'integer', 'posts_count' => 'integer',
'comments_count' => 'integer', 'comments_count' => 'integer',
'is_premium' => 'boolean', 'is_premium' => 'boolean',
...@@ -185,7 +177,32 @@ class Episode extends Entity ...@@ -185,7 +177,32 @@ class Episode extends Entity
'updated_by' => 'integer', 'updated_by' => 'integer',
]; ];
public function setCover(UploadedFile | File $file = null): self /**
* @param array<string, mixed> $data
*/
#[Override]
public function injectRawData(array $data): static
{
parent::injectRawData($data);
$this->link = url_to('episode', esc($this->getPodcast()->handle, 'url'), esc($this->attributes['slug'], 'url'));
$this->audio_url = url_to(
'episode-audio',
$this->getPodcast()
->handle,
$this->slug,
$this->getAudio()
->file_extension,
);
$this->audio_opengraph_url = $this->audio_url . '?_from=-+Open+Graph+-';
$this->audio_web_url = $this->audio_url . '?_from=-+Website+-';
return $this;
}
public function setCover(UploadedFile | File|null $file = null): self
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
...@@ -200,8 +217,8 @@ class Episode extends Entity ...@@ -200,8 +217,8 @@ class Episode extends Entity
} else { } else {
$cover = new Image([ $cover = new Image([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(), 'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
'sizes' => config(Images::class) 'sizes' => config('Images')
->podcastCoverSizes, ->podcastCoverSizes,
'uploaded_by' => $this->attributes['updated_by'], 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'], 'updated_by' => $this->attributes['updated_by'],
]); ]);
...@@ -231,7 +248,7 @@ class Episode extends Entity ...@@ -231,7 +248,7 @@ class Episode extends Entity
return $this->cover; return $this->cover;
} }
public function setAudio(UploadedFile | File $file = null): self public function setAudio(UploadedFile | File|null $file = null): self
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
...@@ -268,7 +285,7 @@ class Episode extends Entity ...@@ -268,7 +285,7 @@ class Episode extends Entity
return $this->audio; return $this->audio;
} }
public function setTranscript(UploadedFile | File $file = null): self public function setTranscript(UploadedFile | File|null $file = null): self
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
...@@ -284,7 +301,7 @@ class Episode extends Entity ...@@ -284,7 +301,7 @@ class Episode extends Entity
$transcript = new Transcript([ $transcript = new Transcript([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(), 'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
'language_code' => $this->getPodcast() 'language_code' => $this->getPodcast()
->language_code, ->language_code,
'uploaded_by' => $this->attributes['updated_by'], 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'], 'updated_by' => $this->attributes['updated_by'],
]); ]);
...@@ -305,7 +322,7 @@ class Episode extends Entity ...@@ -305,7 +322,7 @@ class Episode extends Entity
return $this->transcript; return $this->transcript;
} }
public function setChapters(UploadedFile | File $file = null): self public function setChapters(UploadedFile | File|null $file = null): self
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
...@@ -321,7 +338,7 @@ class Episode extends Entity ...@@ -321,7 +338,7 @@ class Episode extends Entity
$chapters = new Chapters([ $chapters = new Chapters([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(), 'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
'language_code' => $this->getPodcast() 'language_code' => $this->getPodcast()
->language_code, ->language_code,
'uploaded_by' => $this->attributes['updated_by'], 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'], 'updated_by' => $this->attributes['updated_by'],
]); ]);
...@@ -342,21 +359,6 @@ class Episode extends Entity ...@@ -342,21 +359,6 @@ class Episode extends Entity
return $this->chapters; return $this->chapters;
} }
public function getAudioUrl(): string
{
return url_to('episode-audio', $this->getPodcast()->handle, $this->slug, $this->getAudio()->file_extension);
}
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.
*/ */
...@@ -449,12 +451,7 @@ class Episode extends Entity ...@@ -449,12 +451,7 @@ class Episode extends Entity
return $this->comments; return $this->comments;
} }
public function getLink(): string public function getEmbedUrl(?string $theme = null): string
{
return url_to('episode', esc($this->getPodcast()->handle), esc($this->attributes['slug']));
}
public function getEmbedUrl(string $theme = null): string
{ {
return $theme return $theme
? url_to('embed-theme', esc($this->getPodcast()->handle), esc($this->attributes['slug']), $theme) ? url_to('embed-theme', esc($this->getPodcast()->handle), esc($this->attributes['slug']), $theme)
...@@ -463,7 +460,7 @@ class Episode extends Entity ...@@ -463,7 +460,7 @@ class Episode extends Entity
public function setGuid(?string $guid = null): static public function setGuid(?string $guid = null): static
{ {
$this->attributes['guid'] = $guid === null ? $this->getLink() : $guid; $this->attributes['guid'] = $guid ?? $this->link;
return $this; return $this;
} }
...@@ -494,39 +491,11 @@ class Episode extends Entity ...@@ -494,39 +491,11 @@ 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) {
$this->description = trim( $this->description = trim(
preg_replace('~\s+~', ' ', strip_tags((string) $this->attributes['description_html'])), (string) preg_replace('~\s+~', ' ', strip_tags((string) $this->attributes['description_html'])),
); );
} }
...@@ -590,91 +559,6 @@ class Episode extends Entity ...@@ -590,91 +559,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($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($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) {
......
...@@ -11,7 +11,6 @@ declare(strict_types=1); ...@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use Config\Services;
/** /**
* @property string $url * @property string $url
...@@ -23,15 +22,9 @@ use Config\Services; ...@@ -23,15 +22,9 @@ use Config\Services;
*/ */
class Location extends Entity class Location extends Entity
{ {
/** private const string OSM_URL = 'https://www.openstreetmap.org/';
* @var string
*/
private const OSM_URL = 'https://www.openstreetmap.org/';
/** private const string NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
* @var string
*/
private const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
public function __construct( public function __construct(
protected string $name, protected string $name,
...@@ -85,7 +78,7 @@ class Location extends Entity ...@@ -85,7 +78,7 @@ class Location extends Entity
*/ */
public function fetchOsmLocation(): static public function fetchOsmLocation(): static
{ {
$client = Services::curlrequest(); $client = service('curlrequest');
$response = $client->request( $response = $client->request(
'GET', 'GET',
...@@ -101,7 +94,7 @@ class Location extends Entity ...@@ -101,7 +94,7 @@ class Location extends Entity
], ],
); );
$places = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR); $places = json_decode((string) $response->getBody(), false, 512, JSON_THROW_ON_ERROR);
if ($places === []) { if ($places === []) {
return $this; return $this;
...@@ -109,14 +102,14 @@ class Location extends Entity ...@@ -109,14 +102,14 @@ class Location extends Entity
if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists( if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists(
$places[0], $places[0],
'lon' 'lon',
) && $places[0]->lon !== null)) { ) && $places[0]->lon !== null)) {
$this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}"; $this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}";
} }
if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists( if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists(
$places[0], $places[0],
'osm_id' 'osm_id',
) && $places[0]->osm_id !== null)) { ) && $places[0]->osm_id !== null)) {
$this->attributes['osm'] = strtoupper(substr((string) $places[0]->osm_type, 0, 1)) . $places[0]->osm_id; $this->attributes['osm'] = strtoupper(substr((string) $places[0]->osm_type, 0, 1)) . $places[0]->osm_id;
} }
......
...@@ -14,7 +14,6 @@ use App\Models\PersonModel; ...@@ -14,7 +14,6 @@ use App\Models\PersonModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
use Config\Images;
use Modules\Media\Entities\Image; use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel; use Modules\Media\Models\MediaModel;
use RuntimeException; use RuntimeException;
...@@ -57,7 +56,7 @@ class Person extends Entity ...@@ -57,7 +56,7 @@ class Person extends Entity
/** /**
* Saves the person avatar in `public/media/persons/` * Saves the person avatar in `public/media/persons/`
*/ */
public function setAvatar(UploadedFile | File $file = null): static public function setAvatar(UploadedFile | File|null $file = null): static
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
...@@ -72,8 +71,8 @@ class Person extends Entity ...@@ -72,8 +71,8 @@ class Person extends Entity
} else { } else {
$avatar = new Image([ $avatar = new Image([
'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(), 'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
'sizes' => config(Images::class) 'sizes' => config('Images')
->personAvatarSizes, ->personAvatarSizes,
'uploaded_by' => $this->attributes['updated_by'], 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'], 'updated_by' => $this->attributes['updated_by'],
]); ]);
...@@ -111,7 +110,7 @@ class Person extends Entity ...@@ -111,7 +110,7 @@ class Person extends Entity
$this->roles = (new PersonModel())->getPersonRoles( $this->roles = (new PersonModel())->getPersonRoles(
$this->id, $this->id,
(int) $this->attributes['podcast_id'], (int) $this->attributes['podcast_id'],
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null,
); );
} }
......
...@@ -10,18 +10,15 @@ declare(strict_types=1); ...@@ -10,18 +10,15 @@ 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;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PlatformModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Entities\User;
use Config\Images;
use Exception; use Exception;
use League\CommonMark\Environment\Environment; use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension; use League\CommonMark\Extension\Autolink\AutolinkExtension;
...@@ -32,6 +29,8 @@ use League\CommonMark\MarkdownConverter; ...@@ -32,6 +29,8 @@ use League\CommonMark\MarkdownConverter;
use Modules\Auth\Models\UserModel; use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Image; use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel; use Modules\Media\Models\MediaModel;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Models\PlatformModel;
use Modules\PremiumPodcasts\Entities\Subscription; use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel; use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException; use RuntimeException;
...@@ -62,11 +61,8 @@ use RuntimeException; ...@@ -62,11 +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|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
...@@ -76,13 +72,7 @@ use RuntimeException; ...@@ -76,13 +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 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
...@@ -122,9 +112,9 @@ class Podcast extends Entity ...@@ -122,9 +112,9 @@ class Podcast extends Entity
protected ?array $other_categories = null; protected ?array $other_categories = null;
/** /**
* @var string[]|null * @var int[]
*/ */
protected ?array $other_categories_ids = null; protected array $other_categories_ids = [];
/** /**
* @var Episode[]|null * @var Episode[]|null
...@@ -163,8 +153,6 @@ class Podcast extends Entity ...@@ -163,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;
/** /**
...@@ -177,43 +165,35 @@ class Podcast extends Entity ...@@ -177,43 +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',
'copyright' => '?string', 'is_blocked' => 'boolean',
'episode_description_footer_markdown' => '?string', 'is_completed' => 'boolean',
'episode_description_footer_html' => '?string', 'is_locked' => 'boolean',
'is_blocked' => 'boolean', 'is_premium_by_default' => 'boolean',
'is_completed' => 'boolean', 'imported_feed_url' => '?string',
'is_locked' => 'boolean', 'new_feed_url' => '?string',
'is_premium_by_default' => 'boolean', 'location_name' => '?string',
'imported_feed_url' => '?string', 'location_geo' => '?string',
'new_feed_url' => '?string', 'location_osm' => '?string',
'location_name' => '?string', 'is_published_on_hubs' => 'boolean',
'location_geo' => '?string', 'created_by' => 'integer',
'location_osm' => '?string', 'updated_by' => 'integer',
'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
...@@ -235,7 +215,7 @@ class Podcast extends Entity ...@@ -235,7 +215,7 @@ class Podcast extends Entity
return $this->actor; return $this->actor;
} }
public function setCover(UploadedFile | File $file = null): self public function setCover(UploadedFile | File|null $file = null): self
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
...@@ -250,8 +230,8 @@ class Podcast extends Entity ...@@ -250,8 +230,8 @@ class Podcast extends Entity
} else { } else {
$cover = new Image([ $cover = new Image([
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(), 'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
'sizes' => config(Images::class) 'sizes' => config('Images')
->podcastCoverSizes, ->podcastCoverSizes,
'uploaded_by' => $this->attributes['updated_by'], 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'], 'updated_by' => $this->attributes['updated_by'],
]); ]);
...@@ -278,7 +258,7 @@ class Podcast extends Entity ...@@ -278,7 +258,7 @@ class Podcast extends Entity
return $this->cover; return $this->cover;
} }
public function setBanner(UploadedFile | File $file = null): self public function setBanner(UploadedFile | File|null $file = null): self
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this; return $this;
...@@ -293,8 +273,8 @@ class Podcast extends Entity ...@@ -293,8 +273,8 @@ class Podcast extends Entity
} else { } else {
$banner = new Image([ $banner = new Image([
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(), 'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
'sizes' => config(Images::class) 'sizes' => config('Images')
->podcastBannerSizes, ->podcastBannerSizes,
'uploaded_by' => $this->attributes['updated_by'], 'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'], 'updated_by' => $this->attributes['updated_by'],
]); ]);
...@@ -450,42 +430,6 @@ class Podcast extends Entity ...@@ -450,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) {
...@@ -524,7 +468,7 @@ class Podcast extends Entity ...@@ -524,7 +468,7 @@ class Podcast extends Entity
} }
if ($this->podcasting_platforms === null) { if ($this->podcasting_platforms === null) {
$this->podcasting_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'podcasting'); $this->podcasting_platforms = (new PlatformModel())->getPlatforms($this->id, 'podcasting');
} }
return $this->podcasting_platforms; return $this->podcasting_platforms;
...@@ -542,7 +486,7 @@ class Podcast extends Entity ...@@ -542,7 +486,7 @@ class Podcast extends Entity
} }
if ($this->social_platforms === null) { if ($this->social_platforms === null) {
$this->social_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'social'); $this->social_platforms = (new PlatformModel())->getPlatforms($this->id, 'social');
} }
return $this->social_platforms; return $this->social_platforms;
...@@ -560,7 +504,7 @@ class Podcast extends Entity ...@@ -560,7 +504,7 @@ class Podcast extends Entity
} }
if ($this->funding_platforms === null) { if ($this->funding_platforms === null) {
$this->funding_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'funding'); $this->funding_platforms = (new PlatformModel())->getPlatforms($this->id, 'funding');
} }
return $this->funding_platforms; return $this->funding_platforms;
...@@ -583,11 +527,11 @@ class Podcast extends Entity ...@@ -583,11 +527,11 @@ class Podcast extends Entity
} }
/** /**
* @return int[]|string[] * @return int[]
*/ */
public function getOtherCategoriesIds(): array public function getOtherCategoriesIds(): array
{ {
if ($this->other_categories_ids === null) { if ($this->other_categories_ids === []) {
$this->other_categories_ids = array_column($this->getOtherCategories(), 'id'); $this->other_categories_ids = array_column($this->getOtherCategories(), 'id');
} }
...@@ -634,55 +578,6 @@ class Podcast extends Entity ...@@ -634,55 +578,6 @@ 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
......
...@@ -7,21 +7,28 @@ namespace App\Filters; ...@@ -7,21 +7,28 @@ namespace App\Filters;
use CodeIgniter\Filters\FilterInterface; use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use Override;
class AllowCorsFilter implements FilterInterface class AllowCorsFilter implements FilterInterface
{ {
/** /**
* @param string[]|null $arguments * @param list<string>|null $arguments
*
* @return RequestInterface|ResponseInterface|string|null
*/ */
public function before(RequestInterface $request, $arguments = null): void #[Override]
public function before(RequestInterface $request, $arguments = null)
{ {
// Do something here return null;
} }
/** /**
* @param string[]|null $arguments * @param list<string>|null $arguments
*
* @return ResponseInterface|null
*/ */
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void #[Override]
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{ {
if (! $response->hasHeader('Cache-Control')) { if (! $response->hasHeader('Cache-Control')) {
$response->setHeader('Cache-Control', 'public, max-age=86400'); $response->setHeader('Cache-Control', 'public, max-age=86400');
...@@ -31,5 +38,7 @@ class AllowCorsFilter implements FilterInterface ...@@ -31,5 +38,7 @@ class AllowCorsFilter implements FilterInterface
->setHeader('Access-Control-Allow-Headers', '*') // for allowing any headers, insecure ->setHeader('Access-Control-Allow-Headers', '*') // for allowing any headers, insecure
->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') // allows GET and OPTIONS methods only ->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') // allows GET and OPTIONS methods only
->setHeader('Access-Control-Max-Age', '86400'); ->setHeader('Access-Control-Max-Age', '86400');
return $response;
} }
} }
...@@ -2,14 +2,6 @@ ...@@ -2,14 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use Config\Services;
if (! function_exists('render_breadcrumb')) { if (! function_exists('render_breadcrumb')) {
/** /**
* Renders the breadcrumb navigation through the Breadcrumb service * Renders the breadcrumb navigation through the Breadcrumb service
...@@ -17,20 +9,18 @@ if (! function_exists('render_breadcrumb')) { ...@@ -17,20 +9,18 @@ if (! function_exists('render_breadcrumb')) {
* @param string|null $class to be added to the breadcrumb nav * @param string|null $class to be added to the breadcrumb nav
* @return string html breadcrumb * @return string html breadcrumb
*/ */
function render_breadcrumb(string $class = null): string function render_breadcrumb(?string $class = null): string
{ {
$breadcrumb = Services::breadcrumb(); return service('breadcrumb')->render($class);
return $breadcrumb->render($class);
} }
} }
if (! function_exists('replace_breadcrumb_params')) { if (! function_exists('replace_breadcrumb_params')) {
/** /**
* @param string[] $newParams * @param array<string|int,string> $newParams
*/ */
function replace_breadcrumb_params(array $newParams): void function replace_breadcrumb_params(array $newParams): void
{ {
$breadcrumb = Services::breadcrumb(); service('breadcrumb')->replaceParams($newParams);
$breadcrumb->replaceParams(esc($newParams));
} }
} }
...@@ -16,31 +16,6 @@ use CodeIgniter\View\Table; ...@@ -16,31 +16,6 @@ use CodeIgniter\View\Table;
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('hint_tooltip')) {
/**
* Hint component
*
* Used to produce tooltip with a question mark icon for hint texts
*
* @param string $hintText The hint text
*/
function hint_tooltip(string $hintText = '', string $class = ''): string
{
$tooltip =
'<span data-tooltip="bottom" tabindex="0" title="' .
$hintText .
'" class="inline-block align-middle opacity-75 focus:ring-accent';
if ($class !== '') {
$tooltip .= ' ' . $class;
}
return $tooltip . '">' . icon('question') . '</span>';
}
}
// ------------------------------------------------------------------------
if (! function_exists('data_table')) { if (! function_exists('data_table')) {
/** /**
* Data table component * Data table component
...@@ -113,12 +88,12 @@ if (! function_exists('publication_pill')) { ...@@ -113,12 +88,12 @@ if (! function_exists('publication_pill')) {
*/ */
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
{ {
$class = match ($publicationStatus) { $variant = match ($publicationStatus) {
'published' => 'text-pine-500 border-pine-500 bg-pine-50', 'published' => 'success',
'scheduled' => 'text-red-600 border-red-600 bg-red-50', 'scheduled' => 'warning',
'with_podcast' => 'text-blue-600 border-blue-600 bg-blue-50', 'with_podcast' => 'info',
'not_published' => 'text-gray-600 border-gray-600 bg-gray-50', 'not_published' => 'default',
default => 'text-gray-600 border-gray-600 bg-gray-50', default => 'default',
}; };
$title = match ($publicationStatus) { $title = match ($publicationStatus) {
...@@ -130,14 +105,12 @@ if (! function_exists('publication_pill')) { ...@@ -130,14 +105,12 @@ if (! function_exists('publication_pill')) {
$label = lang('Episode.publication_status.' . $publicationStatus); $label = lang('Episode.publication_status.' . $publicationStatus);
return '<span ' . ($title === '' ? '' : 'title="' . $title . '"') . ' class="flex items-center px-1 font-semibold border rounded w-max ' . // @icon("error-warning-fill")
$class . return '<x-Pill ' . ($title === '' ? '' : 'title="' . $title . '"') . ' variant="' . $variant . '" class="' . $customClass .
' ' . '">' . $label . ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
$customClass . 'class' => 'flex-shrink-0 ml-1 text-lg',
'">' . ]) : '') .
$label . '</x-Pill>';
($publicationStatus === 'with_podcast' ? '<Icon glyph="error-warning" class="flex-shrink-0 ml-1 text-lg" />' : '') .
'</span>';
} }
} }
...@@ -156,20 +129,20 @@ if (! function_exists('publication_button')) { ...@@ -156,20 +129,20 @@ if (! function_exists('publication_button')) {
$label = lang('Episode.publish'); $label = lang('Episode.publish');
$route = route_to('episode-publish', $podcastId, $episodeId); $route = route_to('episode-publish', $podcastId, $episodeId);
$variant = 'primary'; $variant = 'primary';
$iconLeft = 'upload-cloud'; $iconLeft = 'upload-cloud-fill'; // @icon("upload-cloud-fill")
break; break;
case 'with_podcast': case 'with_podcast':
case 'scheduled': case 'scheduled':
$label = lang('Episode.publish_edit'); $label = lang('Episode.publish_edit');
$route = route_to('episode-publish_edit', $podcastId, $episodeId); $route = route_to('episode-publish_edit', $podcastId, $episodeId);
$variant = 'warning'; $variant = 'warning';
$iconLeft = 'upload-cloud'; $iconLeft = 'upload-cloud-fill'; // @icon("upload-cloud-fill")
break; break;
case 'published': case 'published':
$label = lang('Episode.unpublish'); $label = lang('Episode.unpublish');
$route = route_to('episode-unpublish', $podcastId, $episodeId); $route = route_to('episode-unpublish', $podcastId, $episodeId);
$variant = 'danger'; $variant = 'danger';
$iconLeft = 'cloud-off'; $iconLeft = 'cloud-off-fill'; // @icon("cloud-off-fill")
break; break;
default: default:
$label = ''; $label = '';
...@@ -180,7 +153,7 @@ if (! function_exists('publication_button')) { ...@@ -180,7 +153,7 @@ if (! function_exists('publication_button')) {
} }
return <<<HTML return <<<HTML
<Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</Button> <x-Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</x-Button>
HTML; HTML;
} }
} }
...@@ -261,7 +234,7 @@ if (! function_exists('episode_publication_status_banner')) { ...@@ -261,7 +234,7 @@ if (! function_exists('episode_publication_status_banner')) {
$bannerText = lang('Episode.publication_status_banner.text', [ $bannerText = lang('Episode.publication_status_banner.text', [
'publication_status' => $episode->publication_status, 'publication_status' => $episode->publication_status,
'publication_date' => $episode->published_at instanceof Time ? local_datetime( 'publication_date' => $episode->published_at instanceof Time ? local_datetime(
$episode->published_at $episode->published_at,
) : null, ) : null,
]); ]);
$previewLinkLabel = lang('Episode.publication_status_banner.preview'); $previewLinkLabel = lang('Episode.publication_status_banner.preview');
...@@ -294,7 +267,7 @@ if (! function_exists('episode_numbering')) { ...@@ -294,7 +267,7 @@ if (! function_exists('episode_numbering')) {
?int $episodeNumber = null, ?int $episodeNumber = null,
?int $seasonNumber = null, ?int $seasonNumber = null,
string $class = '', string $class = '',
bool $isAbbr = false bool $isAbbr = false,
): string { ): string {
if (! $episodeNumber && ! $seasonNumber) { if (! $episodeNumber && ! $seasonNumber) {
return ''; return '';
...@@ -350,9 +323,11 @@ if (! function_exists('location_link')) { ...@@ -350,9 +323,11 @@ if (! function_exists('location_link')) {
return anchor( return anchor(
$location->url, $location->url,
icon('map-pin', 'mr-2 flex-shrink-0') . '<span class="truncate">' . esc($location->name) . '</span>', icon('map-pin-2-fill', [
'class' => 'mr-2 flex-shrink-0',
]) . '<span class="truncate">' . esc($location->name) . '</span>',
[ [
'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline focus:ring-accent' . 'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline' .
($class === '' ? '' : " {$class}"), ($class === '' ? '' : " {$class}"),
'target' => '_blank', 'target' => '_blank',
'rel' => 'noreferrer noopener', 'rel' => 'noreferrer noopener',
...@@ -377,7 +352,6 @@ if (! function_exists('audio_player')) { ...@@ -377,7 +352,6 @@ if (! function_exists('audio_player')) {
id="castopod-vm-player" id="castopod-vm-player"
theme="light" theme="light"
language="{$language}" language="{$language}"
icons="castopod-icons"
class="{$class} relative z-0" class="{$class} relative z-0"
style="--vm-player-box-shadow:0; --vm-player-theme: hsl(var(--color-accent-base)); --vm-control-focus-color: hsl(var(--color-accent-contrast)); --vm-control-spacing: 4px; --vm-menu-item-focus-bg: hsl(var(--color-background-highlight));" style="--vm-player-box-shadow:0; --vm-player-theme: hsl(var(--color-accent-base)); --vm-control-focus-color: hsl(var(--color-accent-contrast)); --vm-control-spacing: 4px; --vm-menu-item-focus-bg: hsl(var(--color-background-highlight));"
> >
...@@ -385,7 +359,7 @@ if (! function_exists('audio_player')) { ...@@ -385,7 +359,7 @@ if (! function_exists('audio_player')) {
<source src="{$source}" type="{$mediaType}" /> <source src="{$source}" type="{$mediaType}" />
</vm-audio> </vm-audio>
<vm-ui> <vm-ui>
<vm-icon-library name="castopod-icons"></vm-icon-library> <vm-icon-library></vm-icon-library>
<vm-controls full-width> <vm-controls full-width>
<vm-playback-control></vm-playback-control> <vm-playback-control></vm-playback-control>
<vm-volume-control></vm-volume-control> <vm-volume-control></vm-volume-control>
...@@ -407,7 +381,7 @@ if (! function_exists('relative_time')) { ...@@ -407,7 +381,7 @@ if (! function_exists('relative_time')) {
function relative_time(Time $time, string $class = ''): string function relative_time(Time $time, string $class = ''): string
{ {
$formatter = new IntlDateFormatter(service( $formatter = new IntlDateFormatter(service(
'request' 'request',
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE); )->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
$translatedDate = $time->toLocalizedString($formatter->getPattern()); $translatedDate = $time->toLocalizedString($formatter->getPattern());
$datetime = $time->format(DateTime::ATOM); $datetime = $time->format(DateTime::ATOM);
...@@ -428,7 +402,7 @@ if (! function_exists('local_datetime')) { ...@@ -428,7 +402,7 @@ if (! function_exists('local_datetime')) {
function local_datetime(Time $time): string function local_datetime(Time $time): string
{ {
$formatter = new IntlDateFormatter(service( $formatter = new IntlDateFormatter(service(
'request' 'request',
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG); )->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
$translatedDate = $time->toLocalizedString($formatter->getPattern()); $translatedDate = $time->toLocalizedString($formatter->getPattern());
$datetime = $time->format(DateTime::ATOM); $datetime = $time->format(DateTime::ATOM);
...@@ -457,7 +431,7 @@ if (! function_exists('local_date')) { ...@@ -457,7 +431,7 @@ if (! function_exists('local_date')) {
function local_date(Time $time): string function local_date(Time $time): string
{ {
$formatter = new IntlDateFormatter(service( $formatter = new IntlDateFormatter(service(
'request' 'request',
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE); )->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
$translatedDate = $time->toLocalizedString($formatter->getPattern()); $translatedDate = $time->toLocalizedString($formatter->getPattern());
...@@ -467,7 +441,6 @@ if (! function_exists('local_date')) { ...@@ -467,7 +441,6 @@ if (! function_exists('local_date')) {
} }
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('explicit_badge')) { if (! function_exists('explicit_badge')) {
...@@ -486,7 +459,6 @@ if (! function_exists('explicit_badge')) { ...@@ -486,7 +459,6 @@ if (! function_exists('explicit_badge')) {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
if (! function_exists('category_label')) { if (! function_exists('category_label')) {
function category_label(Category $category): string function category_label(Category $category): string
{ {
......
...@@ -22,26 +22,25 @@ if (! function_exists('form_textarea')) { ...@@ -22,26 +22,25 @@ if (! function_exists('form_textarea')) {
// Unsets default rows and cols if defined in extra field as array or string. // Unsets default rows and cols if defined in extra field as array or string.
if ((is_array($extra) && array_key_exists('rows', $extra)) || (is_string($extra) && stripos( if ((is_array($extra) && array_key_exists('rows', $extra)) || (is_string($extra) && stripos(
preg_replace('~\s+~', '', $extra), (string) preg_replace('~\s+~', '', $extra),
'rows=' 'rows=',
) !== false)) { ) !== false)) {
unset($defaults['rows']); unset($defaults['rows']);
} }
if ((is_array($extra) && array_key_exists('cols', $extra)) || (is_string($extra) && stripos( if ((is_array($extra) && array_key_exists('cols', $extra)) || (is_string($extra) && stripos(
preg_replace('~\s+~', '', $extra), (string) preg_replace('~\s+~', '', $extra),
'cols=' 'cols=',
) !== false)) { ) !== false)) {
unset($defaults['cols']); unset($defaults['cols']);
} }
return '<textarea ' . rtrim(parse_form_attributes($data, $defaults)) . stringify_attributes( return '<textarea ' . rtrim(parse_form_attributes($data, $defaults)) . stringify_attributes(
$extra $extra,
) . '>' . $val . "</textarea>\n"; ) . '>' . $val . "</textarea>\n";
} }
} }
if (! function_exists('parse_form_attributes')) { if (! function_exists('parse_form_attributes')) {
/** /**
* Parse the form attributes * Parse the form attributes
......
...@@ -8,7 +8,6 @@ declare(strict_types=1); ...@@ -8,7 +8,6 @@ declare(strict_types=1);
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
use App\Entities\Episode; use App\Entities\Episode;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use JamesHeinrich\GetID3\WriteTags; use JamesHeinrich\GetID3\WriteTags;
use Modules\Media\FileManagers\FileManagerInterface; use Modules\Media\FileManagers\FileManagerInterface;
...@@ -43,24 +42,16 @@ if (! function_exists('write_audio_file_tags')) { ...@@ -43,24 +42,16 @@ if (! function_exists('write_audio_file_tags')) {
// populate data array // populate data array
$TagData = [ $TagData = [
'title' => [esc($episode->title)], 'title' => [esc($episode->title)],
'artist' => [ 'artist' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
$episode->podcast->publisher === null
? esc($episode->podcast->owner_name)
: $episode->podcast->publisher,
],
'album' => [esc($episode->podcast->title)], 'album' => [esc($episode->podcast->title)],
'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''], 'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''],
'genre' => ['Podcast'], 'genre' => ['Podcast'],
'comment' => [$episode->description], 'comment' => [$episode->description],
'track_number' => [(string) $episode->number], 'track_number' => [(string) $episode->number],
'copyright_message' => [$episode->podcast->copyright], 'copyright_message' => [$episode->podcast->copyright],
'publisher' => [ 'publisher' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
$episode->podcast->publisher === null 'encoded_by' => ['Castopod'],
? esc($episode->podcast->owner_name)
: $episode->podcast->publisher,
],
'encoded_by' => ['Castopod'],
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it // TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
// 'website' => [$podcast_url], // 'website' => [$podcast_url],
......
...@@ -5,7 +5,6 @@ declare(strict_types=1); ...@@ -5,7 +5,6 @@ declare(strict_types=1);
use App\Entities\Person; use App\Entities\Person;
use App\Entities\Podcast; use App\Entities\Podcast;
use Cocur\Slugify\Slugify; use Cocur\Slugify\Slugify;
use Config\App;
use Config\Images; use Config\Images;
use Modules\Media\Entities\Image; use Modules\Media\Entities\Image;
...@@ -15,7 +14,6 @@ use Modules\Media\Entities\Image; ...@@ -15,7 +14,6 @@ use Modules\Media\Entities\Image;
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
if (! function_exists('get_browser_language')) { if (! function_exists('get_browser_language')) {
/** /**
* Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE`. Returns Castopod's default * Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE`. Returns Castopod's default
...@@ -26,7 +24,7 @@ if (! function_exists('get_browser_language')) { ...@@ -26,7 +24,7 @@ if (! function_exists('get_browser_language')) {
function get_browser_language(?string $httpAcceptLanguage = null): string function get_browser_language(?string $httpAcceptLanguage = null): string
{ {
if ($httpAcceptLanguage === null) { if ($httpAcceptLanguage === null) {
return config(App::class)->defaultLocale; return config('App')->defaultLocale;
} }
$langs = explode(',', $httpAcceptLanguage); $langs = explode(',', $httpAcceptLanguage);
...@@ -82,7 +80,6 @@ if (! function_exists('format_duration')) { ...@@ -82,7 +80,6 @@ if (! function_exists('format_duration')) {
} }
} }
if (! function_exists('format_duration_symbol')) { if (! function_exists('format_duration_symbol')) {
/** /**
* Formats duration in seconds to an hh(h) mm(min) ss(s) string. Doesn't show leading zeros if any. * Formats duration in seconds to an hh(h) mm(min) ss(s) string. Doesn't show leading zeros if any.
...@@ -131,7 +128,6 @@ if (! function_exists('generate_random_salt')) { ...@@ -131,7 +128,6 @@ if (! function_exists('generate_random_salt')) {
//-------------------------------------------------------------------- //--------------------------------------------------------------------
if (! function_exists('file_upload_max_size')) { if (! function_exists('file_upload_max_size')) {
/** /**
* Returns a file size limit in bytes based on the PHP upload_max_filesize and post_max_size Adapted from: * Returns a file size limit in bytes based on the PHP upload_max_filesize and post_max_size Adapted from:
...@@ -167,7 +163,7 @@ if (! function_exists('parse_size')) { ...@@ -167,7 +163,7 @@ if (! function_exists('parse_size')) {
$size = (float) preg_replace('~[^0-9\.]~', '', $size); // Remove the non-numeric characters from the size. $size = (float) preg_replace('~[^0-9\.]~', '', $size); // Remove the non-numeric characters from the size.
if ($unit !== '') { if ($unit !== '') {
// Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by. // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
return round($size * pow(1024, (float) stripos('bkmgtpezy', $unit[0]))); return round($size * 1024 ** ((float) stripos('bkmgtpezy', $unit[0])));
} }
return round($size); return round($size);
...@@ -186,17 +182,16 @@ if (! function_exists('format_bytes')) { ...@@ -186,17 +182,16 @@ if (! function_exists('format_bytes')) {
$pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000)); $pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000));
$pow = min($pow, count($units) - 1); $pow = min($pow, count($units) - 1);
$bytes /= pow($is_binary ? 1024 : 1000, $pow); $bytes /= ($is_binary ? 1024 : 1000) ** $pow;
return round($bytes, $precision) . $units[$pow]; return round($bytes, $precision) . $units[$pow];
} }
} }
if (! function_exists('get_site_icon_url')) { if (! function_exists('get_site_icon_url')) {
function get_site_icon_url(string $size): string function get_site_icon_url(string $size): string
{ {
if (config(App::class)->siteIcon['ico'] === service('settings')->get('App.siteIcon')['ico']) { if (config('App')->siteIcon['ico'] === service('settings')->get('App.siteIcon')['ico']) {
// return default site icon url // return default site icon url
return base_url(service('settings')->get('App.siteIcon')[$size]); return base_url(service('settings')->get('App.siteIcon')[$size]);
} }
...@@ -205,25 +200,24 @@ if (! function_exists('get_site_icon_url')) { ...@@ -205,25 +200,24 @@ if (! function_exists('get_site_icon_url')) {
} }
} }
if (! function_exists('get_podcast_banner')) { if (! function_exists('get_podcast_banner')) {
function get_podcast_banner_url(Podcast $podcast, string $size): string function get_podcast_banner_url(Podcast $podcast, string $size): string
{ {
if (! $podcast->banner instanceof Image) { if (! $podcast->banner instanceof Image) {
$defaultBanner = config(Images::class) $defaultBanner = config('Images')
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config( ->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
Images::class Images::class,
)->podcastBannerDefaultPaths['default']; )->podcastBannerDefaultPaths['default'];
$sizes = config(Images::class) $sizes = config('Images')
->podcastBannerSizes; ->podcastBannerSizes;
$sizeConfig = $sizes[$size]; $sizeConfig = $sizes[$size];
helper('filesystem'); helper('filesystem');
// return default site icon url // return default site icon url
return base_url( return base_url(
change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null) change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null),
); );
} }
...@@ -236,15 +230,15 @@ if (! function_exists('get_podcast_banner_mimetype')) { ...@@ -236,15 +230,15 @@ if (! function_exists('get_podcast_banner_mimetype')) {
function get_podcast_banner_mimetype(Podcast $podcast, string $size): string function get_podcast_banner_mimetype(Podcast $podcast, string $size): string
{ {
if (! $podcast->banner instanceof Image) { if (! $podcast->banner instanceof Image) {
$sizes = config(Images::class) $sizes = config('Images')
->podcastBannerSizes; ->podcastBannerSizes;
$sizeConfig = $sizes[$size]; $sizeConfig = $sizes[$size];
helper('filesystem'); helper('filesystem');
// return default site icon url // return default site icon url
return array_key_exists('mimetype', $sizeConfig) ? $sizeConfig['mimetype'] : config( return array_key_exists('mimetype', $sizeConfig) ? $sizeConfig['mimetype'] : config(
Images::class Images::class,
)->podcastBannerDefaultMimeType; )->podcastBannerDefaultMimeType;
} }
...@@ -257,11 +251,11 @@ if (! function_exists('get_avatar_url')) { ...@@ -257,11 +251,11 @@ if (! function_exists('get_avatar_url')) {
function get_avatar_url(Person $person, string $size): string function get_avatar_url(Person $person, string $size): string
{ {
if (! $person->avatar instanceof Image) { if (! $person->avatar instanceof Image) {
$defaultAvatarPath = config(Images::class) $defaultAvatarPath = config('Images')
->avatarDefaultPath; ->avatarDefaultPath;
$sizes = config(Images::class) $sizes = config('Images')
->personAvatarSizes; ->personAvatarSizes;
$sizeConfig = $sizes[$size]; $sizeConfig = $sizes[$size];
......
...@@ -9,7 +9,6 @@ declare(strict_types=1); ...@@ -9,7 +9,6 @@ declare(strict_types=1);
*/ */
use App\Models\PageModel; use App\Models\PageModel;
use Config\App;
if (! function_exists('render_page_links')) { if (! function_exists('render_page_links')) {
/** /**
...@@ -17,34 +16,34 @@ if (! function_exists('render_page_links')) { ...@@ -17,34 +16,34 @@ if (! function_exists('render_page_links')) {
* *
* @return string html pages navigation * @return string html pages navigation
*/ */
function render_page_links(string $class = null, string $podcastHandle = null): string function render_page_links(?string $class = null, ?string $podcastHandle = null): string
{ {
$pages = (new PageModel())->findAll(); $pages = (new PageModel())->findAll();
$links = anchor(route_to('home'), lang('Common.home'), [ $links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', 'class' => 'px-2 py-1 underline hover:no-underline',
]); ]);
if ($podcastHandle !== null) { if ($podcastHandle !== null) {
$links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [ $links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', 'class' => 'px-2 py-1 underline hover:no-underline',
]); ]);
} }
$links .= anchor(route_to('credits'), lang('Person.credits'), [ $links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', 'class' => 'px-2 py-1 underline hover:no-underline',
]); ]);
$links .= anchor(route_to('map'), lang('Page.map.title'), [ $links .= anchor(route_to('map'), lang('Page.map.title'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', 'class' => 'px-2 py-1 underline hover:no-underline',
]); ]);
foreach ($pages as $page) { foreach ($pages as $page) {
$links .= anchor($page->link, esc($page->title), [ $links .= anchor($page->link, esc($page->title), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', 'class' => 'px-2 py-1 underline hover:no-underline',
]); ]);
} }
// if set in .env, add legal notice link at the end of page links // if set in .env, add legal notice link at the end of page links
if (config(App::class)->legalNoticeURL !== null) { if (config('App')->legalNoticeURL !== null) {
$links .= anchor(config(App::class)->legalNoticeURL, lang('Common.legal_notice'), [ $links .= anchor(config('App')->legalNoticeURL, lang('Common.legal_notice'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', 'class' => 'px-2 py-1 underline hover:no-underline',
'target' => '_blank', 'target' => '_blank',
'rel' => 'noopener noreferrer', 'rel' => 'noopener noreferrer',
]); ]);
......
...@@ -10,14 +10,14 @@ declare(strict_types=1); ...@@ -10,14 +10,14 @@ 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\RssFeed;
use App\Libraries\SimpleRSSElement; use App\Models\PodcastModel;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Config\Mimes; use Config\Mimes;
use Modules\Media\Entities\Chapters; use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Transcript; use Modules\Media\Entities\Transcript;
use Modules\Plugins\Core\Plugins;
use Modules\PremiumPodcasts\Entities\Subscription; use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\WebSub\Config\WebSub;
if (! function_exists('get_rss_feed')) { if (! function_exists('get_rss_feed')) {
/** /**
...@@ -29,24 +29,21 @@ if (! function_exists('get_rss_feed')) { ...@@ -29,24 +29,21 @@ if (! function_exists('get_rss_feed')) {
function get_rss_feed( function get_rss_feed(
Podcast $podcast, Podcast $podcast,
string $serviceSlug = '', string $serviceSlug = '',
Subscription $subscription = null, ?Subscription $subscription = null,
string $token = null ?string $token = null,
): string { ): string {
$episodes = $podcast->episodes; /** @var Plugins $plugins */
$plugins = service('plugins');
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; $episodes = $podcast->episodes;
$podcastNamespace = 'https://podcastindex.org/namespace/1.0'; $rss = new RssFeed();
$atomNamespace = 'http://www.w3.org/2005/Atom'; $plugins->rssBeforeChannel($podcast);
$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>"
);
$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');
...@@ -55,14 +52,14 @@ if (! function_exists('get_rss_feed')) { ...@@ -55,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
...@@ -70,17 +67,27 @@ if (! function_exists('get_rss_feed')) { ...@@ -70,17 +67,27 @@ if (! function_exists('get_rss_feed')) {
$channel->addChild('generator', 'Castopod - https://castopod.org/'); $channel->addChild('generator', 'Castopod - https://castopod.org/');
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html'); $channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
$channel->addChild('guid', $podcast->guid, $podcastNamespace); if ($podcast->guid === '') {
// FIXME: guid shouldn't be empty here as it should be filled upon Podcast creation
$uuid = service('uuid');
// 'ead4c236-bf58-58c6-a2c6-a6b28d128cb6' is the uuid of the podcast namespace
$podcast->guid = $uuid->uuid5('ead4c236-bf58-58c6-a2c6-a6b28d128cb6', $podcast->feed_url)
->toString();
(new PodcastModel())->save($podcast);
}
$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);
$itunesImage = $channel->addChild('image', null, $itunesNamespace); $itunesImage = $channel->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
$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);
} }
...@@ -90,32 +97,16 @@ if (! function_exists('get_rss_feed')) { ...@@ -90,32 +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->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);
...@@ -126,7 +117,7 @@ if (! function_exists('get_rss_feed')) { ...@@ -126,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');
...@@ -134,7 +125,7 @@ if (! function_exists('get_rss_feed')) { ...@@ -134,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);
...@@ -142,7 +133,7 @@ if (! function_exists('get_rss_feed')) { ...@@ -142,7 +133,7 @@ if (! function_exists('get_rss_feed')) {
if (in_array( if (in_array(
$socialPlatform->slug, $socialPlatform->slug,
['mastodon', 'peertube', 'funkwhale', 'misskey', 'mobilizon', 'pixelfed', 'plume', 'writefreely'], ['mastodon', 'peertube', 'funkwhale', 'misskey', 'mobilizon', 'pixelfed', 'plume', 'writefreely'],
true true,
)) { )) {
$socialElement->addAttribute('protocol', 'activitypub'); $socialElement->addAttribute('protocol', 'activitypub');
} else { } else {
...@@ -158,41 +149,41 @@ if (! function_exists('get_rss_feed')) { ...@@ -158,41 +149,41 @@ 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',
parse_url($socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url( parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
$socialPlatform->link_url, (string) $socialPlatform->link_url,
PHP_URL_HOST PHP_URL_HOST,
) . '/public' ) . '/public',
); );
$socialSignUpelement->addAttribute( $socialSignUpelement->addAttribute(
'signUpUrl', 'signUpUrl',
parse_url($socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url( parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
$socialPlatform->link_url, (string) $socialPlatform->link_url,
PHP_URL_HOST PHP_URL_HOST,
) . '/auth/sign_up' ) . '/auth/sign_up',
); );
$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(
'homeUrl', 'homeUrl',
parse_url($socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url( parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
$socialPlatform->link_url, (string) $socialPlatform->link_url,
PHP_URL_HOST PHP_URL_HOST,
) . '/public' ) . '/public',
); );
$castopodSocialSignUpelement->addAttribute( $castopodSocialSignUpelement->addAttribute(
'signUpUrl', 'signUpUrl',
parse_url($socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url( parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
$socialPlatform->link_url, (string) $socialPlatform->link_url,
PHP_URL_HOST PHP_URL_HOST,
) . '/auth/sign_up' ) . '/auth/sign_up',
); );
} }
} }
...@@ -201,7 +192,7 @@ if (! function_exists('get_rss_feed')) { ...@@ -201,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) {
...@@ -211,7 +202,7 @@ if (! function_exists('get_rss_feed')) { ...@@ -211,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'));
...@@ -240,34 +231,26 @@ if (! function_exists('get_rss_feed')) { ...@@ -240,34 +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( $channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunesNamespace,
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) { 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');
...@@ -275,17 +258,16 @@ if (! function_exists('get_rss_feed')) { ...@@ -275,17 +258,16 @@ 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) { // run plugins hook at the end
array_to_rss([ $plugins->rssAfterChannel($podcast, $channel);
'elements' => $podcast->custom_rss,
], $channel);
}
foreach ($episodes as $episode) { foreach ($episodes as $episode) {
if ($episode->is_premium && ! $subscription instanceof Subscription) { if ($episode->is_premium && ! $subscription instanceof Subscription) {
continue; continue;
} }
$plugins->rssBeforeItem($episode);
$item = $channel->addChild('item'); $item = $channel->addChild('item');
$item->addChild('title', $episode->title, null, false); $item->addChild('title', $episode->title, null, false);
$enclosure = $item->addChild('enclosure'); $enclosure = $item->addChild('enclosure');
...@@ -305,7 +287,7 @@ if (! function_exists('get_rss_feed')) { ...@@ -305,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);
} }
...@@ -315,10 +297,10 @@ if (! function_exists('get_rss_feed')) { ...@@ -315,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 &&
...@@ -327,46 +309,61 @@ if (! function_exists('get_rss_feed')) { ...@@ -327,46 +309,61 @@ 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->type === 'trailer') {
$trailer = $channel->addChild('trailer', $episode->title, RssFeed::PODCAST_NAMESPACE);
$trailer->addAttribute('pubdate', $episode->published_at->format(DATE_RFC2822));
$trailer->addAttribute(
'url',
$episode->audio_url . ($enclosureParams === '' ? '' : '?' . $enclosureParams),
);
$trailer->addAttribute('length', (string) $episode->audio->file_size);
$trailer->addAttribute('type', $episode->audio->file_mimetype);
if ($episode->season_number !== null) {
$trailer->addAttribute('season', (string) $episode->season_number);
}
}
// 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');
$socialInteractElement->addAttribute('protocol', 'activitypub'); $socialInteractElement->addAttribute('protocol', 'activitypub');
$socialInteractElement->addAttribute( $socialInteractElement->addAttribute(
'accountId', 'accountId',
"@{$podcast->actor->username}@{$podcast->actor->domain}" "@{$podcast->actor->username}@{$podcast->actor->domain}",
); );
$socialInteractElement->addAttribute( $socialInteractElement->addAttribute(
'pubDate', 'pubDate',
$episode->getPosts()[0] $episode->getPosts()[0]
->published_at->format(DateTime::ISO8601) ->published_at->format(DateTime::ISO8601),
); );
} }
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',
Mimes::guessTypeFromExtension( Mimes::guessTypeFromExtension(
pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION) pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION),
) ?? 'text/html', ) ?? 'text/html',
); );
// Castopod only allows for captions (SubRip files) // Castopod only allows for captions (SubRip files)
...@@ -376,21 +373,21 @@ if (! function_exists('get_rss_feed')) { ...@@ -376,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',
...@@ -411,14 +408,10 @@ if (! function_exists('get_rss_feed')) { ...@@ -411,14 +408,10 @@ 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) { $plugins->rssAfterItem($episode, $item);
array_to_rss([
'elements' => $episode->custom_rss,
], $item);
}
} }
return $rss->asXML(); return $rss->asXML();
...@@ -429,11 +422,9 @@ if (! function_exists('add_category_tag')) { ...@@ -429,11 +422,9 @@ if (! function_exists('add_category_tag')) {
/** /**
* Adds <itunes:category> and <category> tags to node for a given category * Adds <itunes:category> and <category> tags to node for a given category
*/ */
function add_category_tag(SimpleXMLElement $node, Category $category): void function add_category_tag(RssFeed $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
...@@ -442,7 +433,7 @@ if (! function_exists('add_category_tag')) { ...@@ -442,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);
} }
...@@ -450,71 +441,3 @@ if (! function_exists('add_category_tag')) { ...@@ -450,71 +441,3 @@ if (! function_exists('add_category_tag')) {
$node->addChild('category', $category->apple_category); $node->addChild('category', $category->apple_category);
} }
} }
if (! function_exists('rss_to_array')) {
/**
* Converts XML to array
*
* FIXME: param should be SimpleRSSElement
*
* @return array<string, mixed>
*/
function rss_to_array(SimpleXMLElement $rssNode): array
{
$nameSpaces = ['', 'http://www.itunes.com/dtds/podcast-1.0.dtd', 'https://podcastindex.org/namespace/1.0'];
$arrayNode = [];
$arrayNode['name'] = $rssNode->getName();
$arrayNode['namespace'] = $rssNode->getNamespaces(false);
foreach ($rssNode->attributes() as $key => $value) {
$arrayNode['attributes'][$key] = (string) $value;
}
$textcontent = trim((string) $rssNode);
if (strlen($textcontent) > 0) {
$arrayNode['content'] = $textcontent;
}
foreach ($nameSpaces as $currentNameSpace) {
foreach ($rssNode->children($currentNameSpace) as $childXmlNode) {
$arrayNode['elements'][] = rss_to_array($childXmlNode);
}
}
return $arrayNode;
}
}
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
*/
function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode): SimpleRSSElement
{
if (array_key_exists('elements', $arrayNode)) {
foreach ($arrayNode['elements'] as $childArrayNode) {
$childXmlNode = $xmlNode->addChild(
$childArrayNode['name'],
$childArrayNode['content'] ?? null,
$childArrayNode['namespace'] === []
? null
: current($childArrayNode['namespace'])
);
if (array_key_exists('attributes', $childArrayNode)) {
foreach (
$childArrayNode['attributes']
as $attributeKey => $attributeValue
) {
$childXmlNode->addAttribute($attributeKey, $attributeValue);
}
}
array_to_rss($childArrayNode, $childXmlNode);
}
}
return $xmlNode;
}
}