Newer
Older

Yassine Doghri
committed

Yassine Doghri
committed
declare(strict_types=1);
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Episode;
use App\Entities\Podcast;

Yassine Doghri
committed
use App\Libraries\NoteObject;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;

Yassine Doghri
committed
use CodeIgniter\Database\BaseBuilder;

Yassine Doghri
committed
use CodeIgniter\Exceptions\PageNotFoundException;

Yassine Doghri
committed
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;

Yassine Doghri
committed
use Config\Embed;
use Config\Services;

Yassine Doghri
committed
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
use Modules\Media\FileManagers\FileManagerInterface;
use SimpleXMLElement;
class EpisodeController extends BaseController
use AnalyticsTrait;
protected Podcast $podcast;
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed

Yassine Doghri
committed
if (count($params) < 2) {
throw PageNotFoundException::forPageNotFound();
}
if (
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast

Yassine Doghri
committed
) {
throw PageNotFoundException::forPageNotFound();
}

Yassine Doghri
committed
$this->podcast = $podcast;

Yassine Doghri
committed
if (
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
throw PageNotFoundException::forPageNotFound();

Yassine Doghri
committed
$this->episode = $episode;
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
public function index(): string
// Prevent analytics hit when authenticated

Yassine Doghri
committed
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}

Yassine Doghri
committed

Yassine Doghri
committed
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,

Yassine Doghri
committed
auth()
->loggedIn() ? 'authenticated' : null,

Yassine Doghri
committed
]),
);

Yassine Doghri
committed
if (! ($cachedView = cache($cacheName))) {
$data = [

Yassine Doghri
committed
'metatags' => get_episode_metatags($this->episode),
'podcast' => $this->podcast,
'episode' => $this->episode,
];

Yassine Doghri
committed
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,

Yassine Doghri
committed
);

Yassine Doghri
committed
if (auth()->loggedIn()) {
helper('form');
return view('episode/comments', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update

Yassine Doghri
committed
return view('episode/comments', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,

Yassine Doghri
committed
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function activity(): string
{
// Prevent analytics hit when authenticated

Yassine Doghri
committed
if (! auth()->loggedIn()) {

Yassine Doghri
committed
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}

Yassine Doghri
committed
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'activity',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,

Yassine Doghri
committed
auth()
->loggedIn() ? 'authenticated' : null,

Yassine Doghri
committed
]),
);

Yassine Doghri
committed
if (! ($cachedView = cache($cacheName))) {
$data = [

Yassine Doghri
committed
'metatags' => get_episode_metatags($this->episode),
'podcast' => $this->podcast,
'episode' => $this->episode,

Yassine Doghri
committed
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);

Yassine Doghri
committed
if (auth()->loggedIn()) {

Yassine Doghri
committed
helper('form');
return view('episode/activity', $data);

Yassine Doghri
committed
}

Yassine Doghri
committed

Yassine Doghri
committed
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
public function chapters(): string
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
{
// Prevent analytics hit when authenticated
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'chapters',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
// get chapters from json file
$data = [
'metatags' => get_episode_metatags($this->episode),
'podcast' => $this->podcast,
'episode' => $this->episode,
];
if (isset($this->episode->chapters->file_key)) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$episodeChaptersJsonString = (string) $fileManager->getFileContents($this->episode->chapters->file_key);
$chapters = json_decode($episodeChaptersJsonString, true);
$data['chapters'] = $chapters;
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
if (auth()->loggedIn()) {
helper('form');
return view('episode/chapters', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/chapters', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
public function transcript(): string
{
// Prevent analytics hit when authenticated
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'transcript',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
// get transcript from json file
$data = [
'metatags' => get_episode_metatags($this->episode),
'podcast' => $this->podcast,
'episode' => $this->episode,
];
if ($this->episode->transcript !== null) {
$data['transcript'] = $this->episode->transcript;
if ($this->episode->transcript->json_key !== null) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$transcriptJsonString = (string) $fileManager->getFileContents(
$this->episode->transcript->json_key
);
$data['captions'] = json_decode($transcriptJsonString, true);
}
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
if (auth()->loggedIn()) {
helper('form');
return view('episode/transcript', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/transcript', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function embed(string $theme = 'light-transparent'): string
{

Yassine Doghri
committed
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');

Benjamin Bellamy
committed
// Prevent analytics hit when authenticated

Yassine Doghri
committed
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$session = Services::session();

Yassine Doghri
committed

Yassine Doghri
committed
if (service('superglobals')->server('HTTP_REFERER') !== null) {
$session->set('embed_domain', parse_url(service('superglobals')->server('HTTP_REFERER'), PHP_URL_HOST));
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'embed',
$theme,
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {

Yassine Doghri
committed
$themeData = EpisodeModel::$themes[$theme];
'podcast' => $this->podcast,
'episode' => $this->episode,
'theme' => $theme,

Yassine Doghri
committed
'themeData' => $themeData,

Yassine Doghri
committed
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function oembedJSON(): ResponseInterface
{
return $this->response->setJSON([
'type' => 'rich',
'version' => '1.0',
'title' => $this->episode->title,
'provider_name' => $this->podcast->title,
'provider_url' => $this->podcast->link,
'author_name' => $this->podcast->title,
'author_url' => $this->podcast->link,
'html' => '<iframe src="' .
$this->episode->embed_url .
'" width="100%" height="' . config('Embed')->height . '" frameborder="0" scrolling="no"></iframe>',
'width' => config('Embed')

Yassine Doghri
committed
->width,
'height' => config('Embed')

Yassine Doghri
committed
->height,
'thumbnail_url' => $this->episode->cover->og_url,
'thumbnail_width' => config('Images')

Yassine Doghri
committed
->podcastCoverSizes['og']['width'],
'thumbnail_height' => config('Images')

Yassine Doghri
committed
->podcastCoverSizes['og']['height'],
]);
}
public function oembedXML(): ResponseInterface
{

Yassine Doghri
committed
$oembed = new SimpleXMLElement("<?xml version='1.0' encoding='utf-8' standalone='yes'?><oembed></oembed>");
$oembed->addChild('type', 'rich');
$oembed->addChild('version', '1.0');
$oembed->addChild('title', $this->episode->title);
$oembed->addChild('provider_name', $this->podcast->title);
$oembed->addChild('provider_url', $this->podcast->link);
$oembed->addChild('author_name', $this->podcast->title);
$oembed->addChild('author_url', $this->podcast->link);

Yassine Doghri
committed
$oembed->addChild('thumbnail', $this->episode->cover->og_url);
$oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['og']['width']);
$oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['og']['height']);
$oembed->addChild(
'html',
htmlspecialchars(
'<iframe src="' .
$this->episode->embed_url .

Yassine Doghri
committed
'" width="100%" height="' . config(
Embed::class
)->height . '" frameborder="0" scrolling="no"></iframe>',
),
);
$oembed->addChild('width', (string) config('Embed')->width);
$oembed->addChild('height', (string) config('Embed')->height);

Yassine Doghri
committed
// @phpstan-ignore-next-line
return $this->response->setXML($oembed);
}

Yassine Doghri
committed
public function episodeObject(): Response
{
$podcastObject = new PodcastEpisode($this->episode);
return $this->response
->setContentType('application/json')
->setBody($podcastObject->toJSON());
}
public function comments(): Response
{
/**
* get comments: aggregated replies from posts referring to the episode
*/
$episodeComments = model('PostModel')
->whereIn('in_reply_to_id', fn (BaseBuilder $builder): BaseBuilder => $builder->select('id')
->from('fediverse_posts')
->where('episode_id', $this->episode->id))

Yassine Doghri
committed
->where('`published_at` <= UTC_TIMESTAMP()', null, false)

Yassine Doghri
committed
->orderBy('published_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) {
$episodeComments->paginate(12);
$pager = $episodeComments->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedComments = $episodeComments->paginate(12, 'default', $pageNumber);
$pager = $episodeComments->pager;
$orderedItems = [];
if ($paginatedComments !== null) {
foreach ($paginatedComments as $comment) {
$orderedItems[] = (new NoteObject($comment))->toArray();
}
}
// @phpstan-ignore-next-line
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setHeader('Access-Control-Allow-Origin', '*')

Yassine Doghri
committed
->setBody($collection->toJSON());
}