diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index 4d0d6300..f68f50de 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -49,6 +49,7 @@ class Autoload extends AutoloadConfig 'Modules\Analytics' => ROOTPATH . 'modules/Analytics/', 'Modules\Install' => ROOTPATH . 'modules/Install/', 'Modules\Fediverse' => ROOTPATH . 'modules/Fediverse/', + 'Modules\WebSub' => ROOTPATH . 'modules/WebSub/', 'Config' => APPPATH . 'Config/', 'ViewComponents' => APPPATH . 'Libraries/ViewComponents/', 'ViewThemes' => APPPATH . 'Libraries/ViewThemes/', diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 87a3d717..c706585c 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -69,6 +69,7 @@ use RuntimeException; * @property string|null $location_osm * @property array|null $custom_rss * @property string $custom_rss_string + * @property bool $is_published_on_hubs * @property int $posts_count * @property int $comments_count * @property int $created_by @@ -164,6 +165,7 @@ class Episode extends Entity 'location_geo' => '?string', 'location_osm' => '?string', 'custom_rss' => '?json-array', + 'is_published_on_hubs' => 'boolean', 'posts_count' => 'integer', 'comments_count' => 'integer', 'created_by' => 'integer', diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index 1b859e2c..a54b7f62 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -73,6 +73,7 @@ use RuntimeException; * @property string|null $payment_pointer * @property array|null $custom_rss * @property string $custom_rss_string + * @property bool $is_published_on_hubs * @property string|null $partner_id * @property string|null $partner_link_url * @property string|null $partner_image_url @@ -180,6 +181,7 @@ class Podcast extends Entity 'location_osm' => '?string', 'payment_pointer' => '?string', 'custom_rss' => '?json-array', + 'is_published_on_hubs' => 'boolean', 'partner_id' => '?string', 'partner_link_url' => '?string', 'partner_image_url' => '?string', diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php index 51297b73..0a82031c 100644 --- a/app/Helpers/rss_helper.php +++ b/app/Helpers/rss_helper.php @@ -41,6 +41,16 @@ if (! function_exists('get_rss_feed')) { $atomLink->addAttribute('rel', 'self'); $atomLink->addAttribute('type', 'application/rss+xml'); + // websub: add links to hubs defined in config + $websubHubs = config('WebSub') + ->hubs; + foreach ($websubHubs as $websubHub) { + $atomLinkHub = $channel->addChild('atom:link', null, 'http://www.w3.org/2005/Atom'); + $atomLinkHub->addAttribute('href', $websubHub); + $atomLinkHub->addAttribute('rel', 'hub'); + $atomLinkHub->addAttribute('type', 'application/rss+xml'); + } + if ($podcast->new_feed_url !== null) { $channel->addChild('new-feed-url', $podcast->new_feed_url, $itunesNamespace); } diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index 64fdc7e5..e9fb463f 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -81,6 +81,7 @@ class EpisodeModel extends Model 'location_geo', 'location_osm', 'custom_rss', + 'is_published_on_hubs', 'posts_count', 'comments_count', 'published_at', @@ -378,7 +379,7 @@ class EpisodeModel extends Model /** * @param mixed[] $data * - * @return array> + * @return mixed[] */ public function clearCache(array $data): array { @@ -404,7 +405,7 @@ class EpisodeModel extends Model /** * @param mixed[] $data * - * @return array> + * @return mixed[] */ protected function writeEnclosureMetadata(array $data): array { diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 1a5be2b6..d913fd68 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -60,6 +60,7 @@ class PodcastModel extends Model 'location_osm', 'payment_pointer', 'custom_rss', + 'is_published_on_hubs', 'partner_id', 'partner_link_url', 'partner_image_url', diff --git a/crontab b/crontab index b1e03dbb..3589b3e9 100644 --- a/crontab +++ b/crontab @@ -1,2 +1,3 @@ * * * * * /usr/local/bin/php /castopod/public/index.php scheduled-activities * * * * * /usr/local/bin/php /castopod/public/index.php scheduled-video-clips +* * * * * /usr/local/bin/php /castopod/public/index.php scheduled-websub-publish diff --git a/docs/src/getting-started/install.md b/docs/src/getting-started/install.md index 5a621098..2a72ada3 100644 --- a/docs/src/getting-started/install.md +++ b/docs/src/getting-started/install.md @@ -88,6 +88,13 @@ want to generate Video Clips. The following extensions must be installed: * * * * * /path/to/php /path/to/castopod/public/index.php scheduled-activities ``` + - For having your episodes be broadcasted on open hubs upon publication using + [WebSub](https://en.wikipedia.org/wiki/WebSub): + + ```bash + * * * * * /usr/local/bin/php /castopod/public/index.php scheduled-websub-publish + ``` + - For Video Clips to be created (see [FFmpeg requirements](#ffmpeg-v418-or-higher-for-video-clips)): diff --git a/modules/Admin/Controllers/EpisodeController.php b/modules/Admin/Controllers/EpisodeController.php index 58beff97..84869bfb 100644 --- a/modules/Admin/Controllers/EpisodeController.php +++ b/modules/Admin/Controllers/EpisodeController.php @@ -280,6 +280,9 @@ class EpisodeController extends BaseController $this->episode->setAudio($this->request->getFile('audio_file')); $this->episode->setCover($this->request->getFile('cover')); + // republish on websub hubs upon edit + $this->episode->is_published_on_hubs = false; + $transcriptChoice = $this->request->getPost('transcript-choice'); if ($transcriptChoice === 'upload-file') { $transcriptFile = $this->request->getFile('transcript_file'); @@ -725,6 +728,11 @@ class EpisodeController extends BaseController (new PostModel())->removePost($post); } + // set podcast is_published_on_hubs to false to trigger websub push + (new PodcastModel())->update($this->episode->podcast->id, [ + 'is_published_on_hubs' => false, + ]); + $episodeModel = new EpisodeModel(); if ($this->episode->published_at !== null) { // if episode is published, set episode published_at to null to unpublish before deletion diff --git a/modules/Admin/Controllers/PodcastController.php b/modules/Admin/Controllers/PodcastController.php index ce649a9b..8fbbee4d 100644 --- a/modules/Admin/Controllers/PodcastController.php +++ b/modules/Admin/Controllers/PodcastController.php @@ -340,6 +340,9 @@ class PodcastController extends BaseController $this->podcast->is_locked = $this->request->getPost('lock') === 'yes'; $this->podcast->updated_by = (int) user_id(); + // republish on websub hubs upon edit + $this->podcast->is_published_on_hubs = false; + $db = db_connect(); $db->transStart(); diff --git a/modules/Auth/Database/Migrations/2020-07-03-191500_add_podcasts_users.php b/modules/Auth/Database/Migrations/2020-07-03-191500_add_podcasts_users.php index 222eead7..b6535c4a 100644 --- a/modules/Auth/Database/Migrations/2020-07-03-191500_add_podcasts_users.php +++ b/modules/Auth/Database/Migrations/2020-07-03-191500_add_podcasts_users.php @@ -10,7 +10,7 @@ declare(strict_types=1); * @link https://castopod.org/ */ -namespace App\Database\Migrations; +namespace Modules\Auth\Database\Migrations; use CodeIgniter\Database\Migration; diff --git a/modules/Install/Controllers/InstallController.php b/modules/Install/Controllers/InstallController.php index 49ca7183..34209a34 100644 --- a/modules/Install/Controllers/InstallController.php +++ b/modules/Install/Controllers/InstallController.php @@ -251,6 +251,8 @@ class InstallController extends Controller ->latest(); $migrations->setNamespace(APP_NAMESPACE) ->latest(); + $migrations->setNamespace('Modules\WebSub') + ->latest(); $migrations->setNamespace('Modules\Auth') ->latest(); $migrations->setNamespace('Modules\Analytics') diff --git a/modules/WebSub/Config/Routes.php b/modules/WebSub/Config/Routes.php new file mode 100644 index 00000000..28ad2cc6 --- /dev/null +++ b/modules/WebSub/Config/Routes.php @@ -0,0 +1,21 @@ +group('', [ + 'namespace' => 'Modules\WebSub\Controllers', +], function ($routes): void { + $routes->cli('scheduled-websub-publish', 'WebSubController::publish'); +}); diff --git a/modules/WebSub/Config/WebSub.php b/modules/WebSub/Config/WebSub.php new file mode 100644 index 00000000..745a0eb1 --- /dev/null +++ b/modules/WebSub/Config/WebSub.php @@ -0,0 +1,23 @@ +distinct() + ->select('podcasts.*') + ->join('episodes', 'podcasts.id = episodes.podcast_id', 'left outer') + ->where('podcasts.is_published_on_hubs', false) + ->orGroupStart() + ->where('episodes.is_published_on_hubs', false) + ->where('`' . $podcastModel->db->getPrefix() . 'episodes`.`published_at` <= NOW()', null, false) + ->groupEnd() + ->findAll(); + + if ($podcasts === []) { + return; + } + + $request = Services::curlrequest(); + + $requestOptions = [ + 'headers' => [ + 'User-Agent' => 'Castopod/' . CP_VERSION . '; +' . base_url('', 'https'), + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + ]; + + $hubUrls = config('WebSub') + ->hubs; + + foreach ($podcasts as $podcast) { + $requestOptions['form_params'] = [ + 'hub.mode' => 'publish', + 'hub.url' => $podcast->feed_url, + ]; + + foreach ($hubUrls as $hub) { + try { + $request->post($hub, $requestOptions); + } catch (Exception $exception) { + log_message( + 'critical', + "COULD NOT PUBLISH @{$podcast->handle} ON {$hub}" . PHP_EOL . $exception->getMessage() + ); + } + } + + // set podcast feed as having been pushed onto hubs + (new PodcastModel())->update($podcast->id, [ + 'is_published_on_hubs' => true, + ]); + + // set newly published episodes as pushed onto hubs + (new EpisodeModel())->set('is_published_on_hubs', true) + ->where([ + 'podcast_id' => $podcast->id, + 'is_published_on_hubs' => false, + ]) + ->where('`published_at` <= NOW()', null, false) + ->update(); + } + } +} diff --git a/modules/WebSub/Database/Migrations/2022-03-07-180000_add_is_published_on_hubs_to_podcasts.php b/modules/WebSub/Database/Migrations/2022-03-07-180000_add_is_published_on_hubs_to_podcasts.php new file mode 100644 index 00000000..523ffdfd --- /dev/null +++ b/modules/WebSub/Database/Migrations/2022-03-07-180000_add_is_published_on_hubs_to_podcasts.php @@ -0,0 +1,35 @@ +db->getPrefix(); + + $createQuery = <<db->query($createQuery); + } + + public function down(): void + { + $prefix = $this->db->getPrefix(); + + $this->forge->dropColumn($prefix . 'podcasts', 'is_published_on_hubs'); + } +} diff --git a/modules/WebSub/Database/Migrations/2022-03-07-181500_add_is_published_on_hubs_to_episodes.php b/modules/WebSub/Database/Migrations/2022-03-07-181500_add_is_published_on_hubs_to_episodes.php new file mode 100644 index 00000000..3bb71966 --- /dev/null +++ b/modules/WebSub/Database/Migrations/2022-03-07-181500_add_is_published_on_hubs_to_episodes.php @@ -0,0 +1,35 @@ +db->getPrefix(); + + $createQuery = <<db->query($createQuery); + } + + public function down(): void + { + $prefix = $this->db->getPrefix(); + + $this->forge->dropColumn($prefix . 'episodes', 'is_published_on_hubs'); + } +}