feat: add WebSub module for pushing feed updates to open hubs
This commit is contained in:
parent
1253096197
commit
10d3f73786
|
@ -49,6 +49,7 @@ class Autoload extends AutoloadConfig
|
||||||
'Modules\Analytics' => ROOTPATH . 'modules/Analytics/',
|
'Modules\Analytics' => ROOTPATH . 'modules/Analytics/',
|
||||||
'Modules\Install' => ROOTPATH . 'modules/Install/',
|
'Modules\Install' => ROOTPATH . 'modules/Install/',
|
||||||
'Modules\Fediverse' => ROOTPATH . 'modules/Fediverse/',
|
'Modules\Fediverse' => ROOTPATH . 'modules/Fediverse/',
|
||||||
|
'Modules\WebSub' => ROOTPATH . 'modules/WebSub/',
|
||||||
'Config' => APPPATH . 'Config/',
|
'Config' => APPPATH . 'Config/',
|
||||||
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
|
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
|
||||||
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',
|
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',
|
||||||
|
|
|
@ -69,6 +69,7 @@ use RuntimeException;
|
||||||
* @property string|null $location_osm
|
* @property string|null $location_osm
|
||||||
* @property array|null $custom_rss
|
* @property array|null $custom_rss
|
||||||
* @property string $custom_rss_string
|
* @property string $custom_rss_string
|
||||||
|
* @property bool $is_published_on_hubs
|
||||||
* @property int $posts_count
|
* @property int $posts_count
|
||||||
* @property int $comments_count
|
* @property int $comments_count
|
||||||
* @property int $created_by
|
* @property int $created_by
|
||||||
|
@ -164,6 +165,7 @@ class Episode extends Entity
|
||||||
'location_geo' => '?string',
|
'location_geo' => '?string',
|
||||||
'location_osm' => '?string',
|
'location_osm' => '?string',
|
||||||
'custom_rss' => '?json-array',
|
'custom_rss' => '?json-array',
|
||||||
|
'is_published_on_hubs' => 'boolean',
|
||||||
'posts_count' => 'integer',
|
'posts_count' => 'integer',
|
||||||
'comments_count' => 'integer',
|
'comments_count' => 'integer',
|
||||||
'created_by' => 'integer',
|
'created_by' => 'integer',
|
||||||
|
|
|
@ -73,6 +73,7 @@ use RuntimeException;
|
||||||
* @property string|null $payment_pointer
|
* @property string|null $payment_pointer
|
||||||
* @property array|null $custom_rss
|
* @property array|null $custom_rss
|
||||||
* @property string $custom_rss_string
|
* @property string $custom_rss_string
|
||||||
|
* @property bool $is_published_on_hubs
|
||||||
* @property string|null $partner_id
|
* @property string|null $partner_id
|
||||||
* @property string|null $partner_link_url
|
* @property string|null $partner_link_url
|
||||||
* @property string|null $partner_image_url
|
* @property string|null $partner_image_url
|
||||||
|
@ -180,6 +181,7 @@ class Podcast extends Entity
|
||||||
'location_osm' => '?string',
|
'location_osm' => '?string',
|
||||||
'payment_pointer' => '?string',
|
'payment_pointer' => '?string',
|
||||||
'custom_rss' => '?json-array',
|
'custom_rss' => '?json-array',
|
||||||
|
'is_published_on_hubs' => 'boolean',
|
||||||
'partner_id' => '?string',
|
'partner_id' => '?string',
|
||||||
'partner_link_url' => '?string',
|
'partner_link_url' => '?string',
|
||||||
'partner_image_url' => '?string',
|
'partner_image_url' => '?string',
|
||||||
|
|
|
@ -41,6 +41,16 @@ if (! function_exists('get_rss_feed')) {
|
||||||
$atomLink->addAttribute('rel', 'self');
|
$atomLink->addAttribute('rel', 'self');
|
||||||
$atomLink->addAttribute('type', 'application/rss+xml');
|
$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) {
|
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, $itunesNamespace);
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,7 @@ class EpisodeModel extends Model
|
||||||
'location_geo',
|
'location_geo',
|
||||||
'location_osm',
|
'location_osm',
|
||||||
'custom_rss',
|
'custom_rss',
|
||||||
|
'is_published_on_hubs',
|
||||||
'posts_count',
|
'posts_count',
|
||||||
'comments_count',
|
'comments_count',
|
||||||
'published_at',
|
'published_at',
|
||||||
|
@ -378,7 +379,7 @@ class EpisodeModel extends Model
|
||||||
/**
|
/**
|
||||||
* @param mixed[] $data
|
* @param mixed[] $data
|
||||||
*
|
*
|
||||||
* @return array<string, array<string|int, mixed>>
|
* @return mixed[]
|
||||||
*/
|
*/
|
||||||
public function clearCache(array $data): array
|
public function clearCache(array $data): array
|
||||||
{
|
{
|
||||||
|
@ -404,7 +405,7 @@ class EpisodeModel extends Model
|
||||||
/**
|
/**
|
||||||
* @param mixed[] $data
|
* @param mixed[] $data
|
||||||
*
|
*
|
||||||
* @return array<string, array<string|int, mixed>>
|
* @return mixed[]
|
||||||
*/
|
*/
|
||||||
protected function writeEnclosureMetadata(array $data): array
|
protected function writeEnclosureMetadata(array $data): array
|
||||||
{
|
{
|
||||||
|
|
|
@ -60,6 +60,7 @@ class PodcastModel extends Model
|
||||||
'location_osm',
|
'location_osm',
|
||||||
'payment_pointer',
|
'payment_pointer',
|
||||||
'custom_rss',
|
'custom_rss',
|
||||||
|
'is_published_on_hubs',
|
||||||
'partner_id',
|
'partner_id',
|
||||||
'partner_link_url',
|
'partner_link_url',
|
||||||
'partner_image_url',
|
'partner_image_url',
|
||||||
|
|
1
crontab
1
crontab
|
@ -1,2 +1,3 @@
|
||||||
* * * * * /usr/local/bin/php /castopod/public/index.php scheduled-activities
|
* * * * * /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-video-clips
|
||||||
|
* * * * * /usr/local/bin/php /castopod/public/index.php scheduled-websub-publish
|
||||||
|
|
|
@ -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
|
* * * * * /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
|
- For Video Clips to be created (see
|
||||||
[FFmpeg requirements](#ffmpeg-v418-or-higher-for-video-clips)):
|
[FFmpeg requirements](#ffmpeg-v418-or-higher-for-video-clips)):
|
||||||
|
|
||||||
|
|
|
@ -280,6 +280,9 @@ class EpisodeController extends BaseController
|
||||||
$this->episode->setAudio($this->request->getFile('audio_file'));
|
$this->episode->setAudio($this->request->getFile('audio_file'));
|
||||||
$this->episode->setCover($this->request->getFile('cover'));
|
$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');
|
$transcriptChoice = $this->request->getPost('transcript-choice');
|
||||||
if ($transcriptChoice === 'upload-file') {
|
if ($transcriptChoice === 'upload-file') {
|
||||||
$transcriptFile = $this->request->getFile('transcript_file');
|
$transcriptFile = $this->request->getFile('transcript_file');
|
||||||
|
@ -725,6 +728,11 @@ class EpisodeController extends BaseController
|
||||||
(new PostModel())->removePost($post);
|
(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();
|
$episodeModel = new EpisodeModel();
|
||||||
if ($this->episode->published_at !== null) {
|
if ($this->episode->published_at !== null) {
|
||||||
// if episode is published, set episode published_at to null to unpublish before deletion
|
// if episode is published, set episode published_at to null to unpublish before deletion
|
||||||
|
|
|
@ -340,6 +340,9 @@ class PodcastController extends BaseController
|
||||||
$this->podcast->is_locked = $this->request->getPost('lock') === 'yes';
|
$this->podcast->is_locked = $this->request->getPost('lock') === 'yes';
|
||||||
$this->podcast->updated_by = (int) user_id();
|
$this->podcast->updated_by = (int) user_id();
|
||||||
|
|
||||||
|
// republish on websub hubs upon edit
|
||||||
|
$this->podcast->is_published_on_hubs = false;
|
||||||
|
|
||||||
$db = db_connect();
|
$db = db_connect();
|
||||||
|
|
||||||
$db->transStart();
|
$db->transStart();
|
||||||
|
|
|
@ -10,7 +10,7 @@ declare(strict_types=1);
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace App\Database\Migrations;
|
namespace Modules\Auth\Database\Migrations;
|
||||||
|
|
||||||
use CodeIgniter\Database\Migration;
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
|
|
@ -251,6 +251,8 @@ class InstallController extends Controller
|
||||||
->latest();
|
->latest();
|
||||||
$migrations->setNamespace(APP_NAMESPACE)
|
$migrations->setNamespace(APP_NAMESPACE)
|
||||||
->latest();
|
->latest();
|
||||||
|
$migrations->setNamespace('Modules\WebSub')
|
||||||
|
->latest();
|
||||||
$migrations->setNamespace('Modules\Auth')
|
$migrations->setNamespace('Modules\Auth')
|
||||||
->latest();
|
->latest();
|
||||||
$migrations->setNamespace('Modules\Analytics')
|
$migrations->setNamespace('Modules\Analytics')
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2022 Ad Aures
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
$routes = service('routes');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSub routes file
|
||||||
|
*/
|
||||||
|
|
||||||
|
$routes->group('', [
|
||||||
|
'namespace' => 'Modules\WebSub\Controllers',
|
||||||
|
], function ($routes): void {
|
||||||
|
$routes->cli('scheduled-websub-publish', 'WebSubController::publish');
|
||||||
|
});
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\WebSub\Config;
|
||||||
|
|
||||||
|
use CodeIgniter\Config\BaseConfig;
|
||||||
|
|
||||||
|
class WebSub extends BaseConfig
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Hubs to ping
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
public array $hubs = [
|
||||||
|
'https://pubsubhubbub.appspot.com/',
|
||||||
|
'https://pubsubhubbub.superfeedr.com/',
|
||||||
|
'https://websubhub.com/hub',
|
||||||
|
'https://switchboard.p3k.io/',
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2022 Ad Aures
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Modules\WebSub\Controllers;
|
||||||
|
|
||||||
|
use App\Models\EpisodeModel;
|
||||||
|
use App\Models\PodcastModel;
|
||||||
|
use CodeIgniter\Controller;
|
||||||
|
use Config\Services;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class WebSubController extends Controller
|
||||||
|
{
|
||||||
|
public function publish(): void
|
||||||
|
{
|
||||||
|
if (ENVIRONMENT !== 'production') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all podcasts that haven't been published yet
|
||||||
|
// or having a published episode that hasn't been pushed yet
|
||||||
|
$podcastModel = new PodcastModel();
|
||||||
|
$podcasts = $podcastModel
|
||||||
|
->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2022 Ad Aures
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Modules\WebSub\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class AddIsPublishedOnHubsToPodcasts extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
||||||
|
$createQuery = <<<CODE_SAMPLE
|
||||||
|
ALTER TABLE {$prefix}podcasts
|
||||||
|
ADD COLUMN `is_published_on_hubs` BOOLEAN NOT NULL DEFAULT 0 AFTER `custom_rss`;
|
||||||
|
CODE_SAMPLE;
|
||||||
|
|
||||||
|
$this->db->query($createQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
||||||
|
$this->forge->dropColumn($prefix . 'podcasts', 'is_published_on_hubs');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2022 Ad Aures
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Modules\WebSub\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class AddIsPublishedOnHubsToEpisodes extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
||||||
|
$createQuery = <<<CODE_SAMPLE
|
||||||
|
ALTER TABLE {$prefix}episodes
|
||||||
|
ADD COLUMN `is_published_on_hubs` BOOLEAN NOT NULL DEFAULT 0 AFTER `custom_rss`;
|
||||||
|
CODE_SAMPLE;
|
||||||
|
|
||||||
|
$this->db->query($createQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$prefix = $this->db->getPrefix();
|
||||||
|
|
||||||
|
$this->forge->dropColumn($prefix . 'episodes', 'is_published_on_hubs');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue