+ + = $speaker ?> + + = $text ?> +
+diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 66644b83..9635b8b6 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -128,6 +128,9 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
$routes->get('chapters', 'EpisodeController::chapters/$1/$2', [
'as' => 'episode-chapters',
]);
+ $routes->get('transcript', 'EpisodeController::transcript/$1/$2', [
+ 'as' => 'episode-transcript',
+ ]);
$routes->options('comments', 'ActivityPubController::preflight');
$routes->get('comments', 'EpisodeController::comments/$1/$2', [
'as' => 'episode-comments',
@@ -205,6 +208,9 @@ $routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
$routes->get('/p/(:uuid)/chapters', 'EpisodePreviewController::chapters/$1', [
'as' => 'episode-preview-chapters',
]);
+$routes->get('/p/(:uuid)/transcript', 'EpisodePreviewController::transcript/$1', [
+ 'as' => 'episode-preview-transcript',
+]);
// Other pages
$routes->get('/credits', 'CreditsController', [
diff --git a/app/Controllers/EpisodeController.php b/app/Controllers/EpisodeController.php
index 9c99a01d..ae76e96b 100644
--- a/app/Controllers/EpisodeController.php
+++ b/app/Controllers/EpisodeController.php
@@ -167,7 +167,7 @@ class EpisodeController extends BaseController
return $cachedView;
}
- public function chapters(): String
+ public function chapters(): string
{
// Prevent analytics hit when authenticated
if (! auth()->loggedIn()) {
@@ -228,6 +228,72 @@ class EpisodeController extends BaseController
return $cachedView;
}
+ 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
+ ? $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 583fae9a..2640a538 100644
--- a/app/Controllers/EpisodePreviewController.php
+++ b/app/Controllers/EpisodePreviewController.php
@@ -13,7 +13,6 @@ namespace App\Controllers;
use App\Entities\Episode;
use App\Models\EpisodeModel;
use CodeIgniter\Exceptions\PageNotFoundException;
-use CodeIgniter\HTTP\RedirectResponse;
use Modules\Media\FileManagers\FileManagerInterface;
class EpisodePreviewController extends BaseController
@@ -45,7 +44,7 @@ class EpisodePreviewController extends BaseController
return $this->{$method}(...$params);
}
- public function index(): RedirectResponse | string
+ public function index(): string
{
helper('form');
@@ -55,7 +54,7 @@ class EpisodePreviewController extends BaseController
]);
}
- public function activity(): RedirectResponse | string
+ public function activity(): string
{
helper('form');
@@ -65,7 +64,7 @@ class EpisodePreviewController extends BaseController
]);
}
- public function chapters(): RedirectResponse | string
+ public function chapters(): string
{
$data = [
'podcast' => $this->episode->podcast,
@@ -84,4 +83,30 @@ class EpisodePreviewController extends BaseController
helper('form');
return view('episode/preview-chapters', $data);
}
+
+ public function transcript(): string
+ {
+ // get transcript from json file
+ $data = [
+ 'podcast' => $this->episode->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);
+ }
+ }
+
+ helper('form');
+ return view('episode/preview-transcript', $data);
+ }
}
diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php
index 7536120d..c2ed2b80 100644
--- a/app/Language/en/Episode.php
+++ b/app/Language/en/Episode.php
@@ -24,6 +24,7 @@ return [
'comments' => 'Comments',
'activity' => 'Activity',
'chapters' => 'Chapters',
+ 'transcript' => 'Transcript',
'description' => 'Episode description',
'number_of_comments' => '{numberOfComments, plural,
one {# comment}
@@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication',
],
'no_chapters' => 'No chapters are available for this episode.',
+ 'download_transcript' => 'Download transcript ({extension})',
+ 'no_transcript' => 'No transcript available for this episode.',
];
diff --git a/modules/Media/TranscriptParser.php b/modules/Media/TranscriptParser.php
index 90167d0a..06b11465 100644
--- a/modules/Media/TranscriptParser.php
+++ b/modules/Media/TranscriptParser.php
@@ -144,6 +144,7 @@ class TranscriptParser
break;
case VTT_STATE_BLANK:
+ $speakercount = 0;
$state = VTT_STATE_TIME;
break;
@@ -154,20 +155,8 @@ class TranscriptParser
case VTT_STATE_TEXT:
if (trim($line) === '') {
- $sub = new stdClass();
- $sub->number = $subNum;
- [$startTime, $endTime] = explode(' --> ', $subTime);
- $sub->startTime = $this->getSecondsFromVTTTimeString($startTime);
- $sub->endTime = $this->getSecondsFromVTTTimeString($endTime);
- $sub->text = trim($subText);
- if ($subSpeaker !== '') {
- $sub->speaker = trim((string) $subSpeaker);
- }
-
- $subText = '';
$state = VTT_STATE_TIME;
- $subs[] = $sub;
- ++$subNum;
+ //$subs[] = $sub;
} elseif ($subText !== '') {
$subText .= PHP_EOL . $line;
} else {
@@ -179,18 +168,36 @@ class TranscriptParser
* 2. Who is speaking
* 3. Any styling cues encoded in the VTT (which we dump)
* More information: https://www.w3.org/TR/webvtt1/
- */
- $vtt_speaker_pattern = '/^<.*>/';
- $removethese = ['<', '>'];
+ *
+ * If there is more than one speaker in a cue, we also need
+ * to handle this, to repeat the start and end times for
+ * the second cue.
+ * */
+
+ $vtt_speaker_pattern = '/^<.*>/U';
+ $removethese = ['', '<', '>'];
preg_match($vtt_speaker_pattern, $line, $matches);
if (isset($matches[0])) {
- $subVoiceCue = explode(' ', str_replace($removethese, '', $matches[0]));
- $subSpeaker = $subVoiceCue[1];
+ $subVoiceCue = str_replace($removethese, '', $matches[0]);
+ $subSpeaker = substr($subVoiceCue, strpos($subVoiceCue, ' '));
} else {
$subSpeaker = '';
}
$subText .= preg_replace($vtt_speaker_pattern, '', $line);
+ $sub = new stdClass();
+ $sub->number = $subNum;
+ [$startTime, $endTime] = explode(' --> ', $subTime);
+ $sub->startTime = $this->getSecondsFromVTTTimeString($startTime);
+ $sub->endTime = $this->getSecondsFromVTTTimeString($endTime);
+ $sub->text = trim($subText);
+ if ($subSpeaker !== '') {
+ $sub->speaker = trim($subSpeaker);
+ }
+
+ $subText = '';
+ $subs[] = $sub;
+ ++$subNum;
}
break;
@@ -215,6 +222,11 @@ class TranscriptParser
private function getSecondsFromVTTTimeString(string $timeString): float
{
$timeString = explode('.', $timeString);
+ if (substr_count($timeString[0], ':') === 1) {
+ // add hours if only MM:SS.mmm format
+ $timeString[0] = '00:' . $timeString[0];
+ }
+
return (strtotime($timeString[0]) - strtotime('TODAY')) + (float) "0.{$timeString[1]}";
}
}
diff --git a/themes/cp_app/episode/_partials/navigation.php b/themes/cp_app/episode/_partials/navigation.php
index beabf102..790dcaa2 100644
--- a/themes/cp_app/episode/_partials/navigation.php
+++ b/themes/cp_app/episode/_partials/navigation.php
@@ -17,6 +17,11 @@ if ($episode->publication_status === 'published') {
'label' => lang('Episode.chapters'),
'labelInfo' => $episode->chapters === null ? 0 : $episode->chapters->chapter_count,
],
+ [
+ 'uri' => route_to('episode-transcript', esc($podcast->handle), esc($episode->slug)),
+ 'label' => lang('Episode.transcript'),
+ 'labelInfo' => $episode->transcript === null ? '–' : '✓',
+ ],
];
} else {
$navigationItems = [
@@ -35,6 +40,11 @@ if ($episode->publication_status === 'published') {
'label' => lang('Episode.chapters'),
'labelInfo' => $episode->chapters === null ? 0 : $episode->chapters->chapter_count,
],
+ [
+ 'uri' => route_to('episode-preview-transcript', $episode->preview_id),
+ 'label' => lang('Episode.transcript'),
+ 'labelInfo' => $episode->transcript === null ? '–' : '✓',
+ ],
];
}
diff --git a/themes/cp_app/episode/_partials/transcript.php b/themes/cp_app/episode/_partials/transcript.php
new file mode 100644
index 00000000..af6826ab
--- /dev/null
+++ b/themes/cp_app/episode/_partials/transcript.php
@@ -0,0 +1,9 @@
+
+
+ = $speaker ?>
+
+ = $text ?>
+