From 87cc437e1ead5486ed46ca37e2055aaf5c9445c1 Mon Sep 17 00:00:00 2001 From: Guy Martin Date: Sat, 17 Feb 2024 12:02:38 +0000 Subject: [PATCH] feat: display chapters in episode's public page closes #423 --- app/Config/Routes.php | 7 ++- app/Controllers/EpisodeController.php | 62 +++++++++++++++++++ app/Controllers/EpisodePreviewController.php | 8 +++ app/Entities/Episode.php | 1 - app/Language/en/Episode.php | 2 + modules/Media/Entities/Chapters.php | 42 +++++++++++++ tailwind.config.cjs | 1 + themes/cp_app/episode/_partials/chapter.php | 11 ++++ .../cp_app/episode/_partials/navigation.php | 12 ++++ themes/cp_app/episode/chapters.php | 25 ++++++++ themes/cp_app/episode/preview-chapters.php | 25 ++++++++ 11 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 themes/cp_app/episode/_partials/chapter.php create mode 100644 themes/cp_app/episode/chapters.php create mode 100644 themes/cp_app/episode/preview-chapters.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index f9e06943..66644b83 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -125,6 +125,9 @@ $routes->group('@(:podcastHandle)', static function ($routes): void { $routes->get('activity', 'EpisodeController::activity/$1/$2', [ 'as' => 'episode-activity', ]); + $routes->get('chapters', 'EpisodeController::chapters/$1/$2', [ + 'as' => 'episode-chapters', + ]); $routes->options('comments', 'ActivityPubController::preflight'); $routes->get('comments', 'EpisodeController::comments/$1/$2', [ 'as' => 'episode-comments', @@ -196,10 +199,12 @@ $routes->get('/audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioContro $routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [ 'as' => 'episode-preview', ]); - $routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [ 'as' => 'episode-preview-activity', ]); +$routes->get('/p/(:uuid)/chapters', 'EpisodePreviewController::chapters/$1', [ + 'as' => 'episode-preview-chapters', +]); // Other pages $routes->get('/credits', 'CreditsController', [ diff --git a/app/Controllers/EpisodeController.php b/app/Controllers/EpisodeController.php index cda684aa..9c99a01d 100644 --- a/app/Controllers/EpisodeController.php +++ b/app/Controllers/EpisodeController.php @@ -27,6 +27,7 @@ use Config\Services; 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 @@ -166,6 +167,67 @@ class EpisodeController extends BaseController return $cachedView; } + public function chapters(): 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}", + '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 + ? $secondsToNextUnpublishedEpisode + : DECADE, + 'cache_name' => $cacheName, + ]); + } + + return $cachedView; + } + public function embed(string $theme = 'light-transparent'): string { header('Content-Security-Policy: frame-ancestors http://*:* https://*:*'); diff --git a/app/Controllers/EpisodePreviewController.php b/app/Controllers/EpisodePreviewController.php index 6c03eef2..f100653a 100644 --- a/app/Controllers/EpisodePreviewController.php +++ b/app/Controllers/EpisodePreviewController.php @@ -63,4 +63,12 @@ class EpisodePreviewController extends BaseController 'episode' => $this->episode, ]); } + + public function chapters(): RedirectResponse | string + { + return view('episode/preview-chapters', [ + 'podcast' => $this->episode->podcast, + 'episode' => $this->episode, + ]); + } } diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 357b4748..5b8ed613 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -21,7 +21,6 @@ use App\Models\PostModel; use CodeIgniter\Entity\Entity; use CodeIgniter\Files\File; use CodeIgniter\HTTP\Files\UploadedFile; -use CodeIgniter\HTTP\URI; use CodeIgniter\I18n\Time; use Config\Images; use Exception; diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index 44be8e38..7536120d 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -23,6 +23,7 @@ return [ 'back_to_episodes' => 'Back to episodes of {podcast}', 'comments' => 'Comments', 'activity' => 'Activity', + 'chapters' => 'Chapters', 'description' => 'Episode description', 'number_of_comments' => '{numberOfComments, plural, one {# comment} @@ -42,4 +43,5 @@ return [ 'publish' => 'Publish', 'publish_edit' => 'Edit publication', ], + 'no_chapters' => 'No chapters are available for this episode.', ]; diff --git a/modules/Media/Entities/Chapters.php b/modules/Media/Entities/Chapters.php index fe66c59e..0d20ab67 100644 --- a/modules/Media/Entities/Chapters.php +++ b/modules/Media/Entities/Chapters.php @@ -10,7 +10,49 @@ declare(strict_types=1); namespace Modules\Media\Entities; +use CodeIgniter\Files\File; +use Exception; + class Chapters extends BaseMedia { protected string $type = 'chapters'; + + public function initFileProperties(): void + { + parent::initFileProperties(); + + if ($this->file_metadata !== null && array_key_exists('chapter_count', $this->file_metadata)) { + helper('media'); + + $this->chapter_count = $this->file_metadata['chapter_count']; + } + } + + public function setFile(File $file): self + { + parent::setFile($file); + + $metadata = lstat((string) $file) ?? []; + + helper('filesystem'); + + $metadata['chapter_count'] = $this->countChaptersInJson($file); + + $this->attributes['file_metadata'] = json_encode($metadata, JSON_INVALID_UTF8_IGNORE); + + $this->file = $file; + + return $this; + } + + private function countChaptersInJson(File $file): Int + { + $chapterContent = file_get_contents($file->getRealPath()); + + if ($chapterContent === false) { + throw new Exception('Could not read chapter file at ' . $this->file->getRealPath()); + } + + return substr_count($chapterContent, 'startTime'); + } } diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 2d7690d8..526574ad 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -41,6 +41,7 @@ module.exports = { backgroundColor: { base: "hsl(var(--color-background-base) / )", elevated: "hsl(var(--color-background-elevated) / )", + subtle: "hsl(var(--color-border-subtle) / )", navigation: "hsl(var(--color-background-navigation) / )", "navigation-active": "hsl(var(--color-background-navigation-active) / )", diff --git a/themes/cp_app/episode/_partials/chapter.php b/themes/cp_app/episode/_partials/chapter.php new file mode 100644 index 00000000..f362b6cd --- /dev/null +++ b/themes/cp_app/episode/_partials/chapter.php @@ -0,0 +1,11 @@ +
+ +
+
+ +
+ + + +
+
diff --git a/themes/cp_app/episode/_partials/navigation.php b/themes/cp_app/episode/_partials/navigation.php index 73fdcf5b..cc9a313d 100644 --- a/themes/cp_app/episode/_partials/navigation.php +++ b/themes/cp_app/episode/_partials/navigation.php @@ -12,6 +12,11 @@ if ($episode->publication_status === 'published') { 'label' => lang('Episode.activity'), 'labelInfo' => $episode->posts_count, ], + [ + 'uri' => route_to('episode-chapters', esc($podcast->handle), esc($episode->slug)), + 'label' => lang('Episode.chapters'), + 'labelInfo' => $episode->chapters === null ? 0 : $episode->chapters->chapter_count, + ], ]; } else { $navigationItems = [ @@ -25,8 +30,15 @@ if ($episode->publication_status === 'published') { 'label' => lang('Episode.activity'), 'labelInfo' => $episode->posts_count, ], + [ + 'uri' => route_to('episode-preview-chapters', $episode->preview_id), + 'label' => lang('Episode.chapters'), + 'labelInfo' => $episode->chapters === null ? 0 : $episode->chapters->chapter_count, + ], ]; } + + ?>