diff --git a/app/Config/Mimes.php b/app/Config/Mimes.php
index ac5c7f46..da0cd335 100644
--- a/app/Config/Mimes.php
+++ b/app/Config/Mimes.php
@@ -307,7 +307,7 @@ class Mimes
],
'svg' => ['image/svg+xml', 'application/xml', 'text/xml'],
'vcf' => 'text/x-vcard',
- 'srt' => ['text/srt', 'text/plain'],
+ 'srt' => ['text/srt', 'text/plain', 'application/octet-stream'],
'vtt' => ['text/vtt', 'text/plain'],
'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'],
];
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index ada6c2c1..23c5e951 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -237,6 +237,22 @@ $routes->group(
'as' => 'episode-delete',
'filter' => 'permission:podcast_episodes-delete',
]);
+ $routes->get(
+ 'transcript-delete',
+ 'Episode::transcriptDelete/$1/$2',
+ [
+ 'as' => 'transcript-delete',
+ 'filter' => 'permission:podcast_episodes-edit',
+ ]
+ );
+ $routes->get(
+ 'chapters-delete',
+ 'Episode::chaptersDelete/$1/$2',
+ [
+ 'as' => 'chapters-delete',
+ 'filter' => 'permission:podcast_episodes-edit',
+ ]
+ );
});
});
diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php
index 0e1d348f..4f2531c8 100644
--- a/app/Controllers/Admin/Episode.php
+++ b/app/Controllers/Admin/Episode.php
@@ -96,6 +96,8 @@ class Episode extends BaseController
'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
+ 'transcript' => 'ext_in[transcript,txt,html,srt,json]',
+ 'chapters' => 'ext_in[chapters,json]',
'publication_date' => 'valid_date[Y-m-d H:i]|permit_empty',
];
@@ -114,6 +116,8 @@ class Episode extends BaseController
'enclosure' => $this->request->getFile('enclosure'),
'description_markdown' => $this->request->getPost('description'),
'image' => $this->request->getFile('image'),
+ 'transcript' => $this->request->getFile('transcript'),
+ 'chapters' => $this->request->getFile('chapters'),
'parental_advisory' =>
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
@@ -189,6 +193,8 @@ class Episode extends BaseController
'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
+ 'transcript' => 'ext_in[transcript,txt,html,srt,json]',
+ 'chapters' => 'ext_in[chapters,json]',
'publication_date' => 'valid_date[Y-m-d H:i]|permit_empty',
];
@@ -231,6 +237,14 @@ class Episode extends BaseController
if ($image) {
$this->episode->image = $image;
}
+ $transcript = $this->request->getFile('transcript');
+ if ($transcript->isValid()) {
+ $this->episode->transcript = $transcript;
+ }
+ $chapters = $this->request->getFile('chapters');
+ if ($chapters->isValid()) {
+ $this->episode->chapters = $chapters;
+ }
$episodeModel = new EpisodeModel();
@@ -262,6 +276,40 @@ class Episode extends BaseController
]);
}
+ public function transcriptDelete()
+ {
+ unlink($this->episode->transcript);
+ $this->episode->transcript_uri = null;
+
+ $episodeModel = new EpisodeModel();
+
+ if (!$episodeModel->update($this->episode->id, $this->episode)) {
+ return redirect()
+ ->back()
+ ->withInput()
+ ->with('errors', $episodeModel->errors());
+ }
+
+ return redirect()->back();
+ }
+
+ public function chaptersDelete()
+ {
+ unlink($this->episode->chapters);
+ $this->episode->chapters_uri = null;
+
+ $episodeModel = new EpisodeModel();
+
+ if (!$episodeModel->update($this->episode->id, $this->episode)) {
+ return redirect()
+ ->back()
+ ->withInput()
+ ->with('errors', $episodeModel->errors());
+ }
+
+ return redirect()->back();
+ }
+
public function delete()
{
(new EpisodeModel())->delete($this->episode->id);
diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
index 8449dbc7..8147df8f 100644
--- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php
+++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
@@ -73,6 +73,16 @@ class AddEpisodes extends Migration
'constraint' => 255,
'null' => true,
],
+ 'transcript_uri' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 255,
+ 'null' => true,
+ ],
+ 'chapters_uri' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 255,
+ 'null' => true,
+ ],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index 6b4939a5..b316e97e 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -35,6 +35,16 @@ class Episode extends Entity
*/
protected $enclosure;
+ /**
+ * @var \CodeIgniter\Files\File
+ */
+ protected $transcript;
+
+ /**
+ * @var \CodeIgniter\Files\File
+ */
+ protected $chapters;
+
/**
* @var string
*/
@@ -55,6 +65,16 @@ class Episode extends Entity
*/
protected $enclosure_opengraph_url;
+ /**
+ * @var string
+ */
+ protected $transcript_url;
+
+ /**
+ * @var string
+ */
+ protected $chapters_url;
+
/**
* Holds text only description, striped of any markdown or html special characters
*
@@ -86,6 +106,8 @@ class Episode extends Entity
'description_markdown' => 'string',
'description_html' => 'string',
'image_uri' => '?string',
+ 'transcript_uri' => '?string',
+ 'chapters_uri' => '?string',
'parental_advisory' => '?string',
'number' => '?integer',
'season_number' => '?integer',
@@ -170,11 +192,75 @@ class Episode extends Entity
}
}
+ /**
+ * Saves an episode transcript
+ *
+ * @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $transcript
+ *
+ */
+ public function setTranscript($transcript)
+ {
+ if (
+ !empty($transcript) &&
+ (!($transcript instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
+ $transcript->isValid())
+ ) {
+ helper('media');
+
+ $this->attributes['transcript_uri'] = save_podcast_media(
+ $transcript,
+ $this->getPodcast()->name,
+ $this->attributes['slug'] . '-transcript'
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Saves an episode chapters
+ *
+ * @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $chapters
+ *
+ */
+ public function setChapters($chapters)
+ {
+ if (
+ !empty($chapters) &&
+ (!($chapters instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
+ $chapters->isValid())
+ ) {
+ helper('media');
+
+ $this->attributes['chapters_uri'] = save_podcast_media(
+ $chapters,
+ $this->getPodcast()->name,
+ $this->attributes['slug'] . '-chapters'
+ );
+ }
+
+ return $this;
+ }
+
public function getEnclosure()
{
return new \CodeIgniter\Files\File($this->getEnclosureMediaPath());
}
+ public function getTranscript()
+ {
+ return $this->attributes['transcript_uri']
+ ? new \CodeIgniter\Files\File($this->getTranscriptMediaPath())
+ : null;
+ }
+
+ public function getChapters()
+ {
+ return $this->attributes['chapters_uri']
+ ? new \CodeIgniter\Files\File($this->getChaptersMediaPath())
+ : null;
+ }
+
public function getEnclosureMediaPath()
{
helper('media');
@@ -182,6 +268,24 @@ class Episode extends Entity
return media_path($this->attributes['enclosure_uri']);
}
+ public function getTranscriptMediaPath()
+ {
+ helper('media');
+
+ return $this->attributes['transcript_uri']
+ ? media_path($this->attributes['transcript_uri'])
+ : null;
+ }
+
+ public function getChaptersMediaPath()
+ {
+ helper('media');
+
+ return $this->attributes['chapters_uri']
+ ? media_path($this->attributes['chapters_uri'])
+ : null;
+ }
+
public function getEnclosureUrl()
{
helper('analytics');
@@ -230,6 +334,20 @@ class Episode extends Entity
return $this->getEnclosureUrl() . '?_from=-+Open+Graph+-';
}
+ public function getTranscriptUrl()
+ {
+ return $this->attributes['transcript_uri']
+ ? base_url($this->getTranscriptMediaPath())
+ : null;
+ }
+
+ public function getChaptersUrl()
+ {
+ return $this->attributes['chapters_uri']
+ ? base_url($this->getChaptersMediaPath())
+ : null;
+ }
+
public function getLink()
{
return base_url(
diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php
index db7dad44..e7bc4ffb 100644
--- a/app/Helpers/rss_helper.php
+++ b/app/Helpers/rss_helper.php
@@ -8,6 +8,7 @@
use App\Libraries\SimpleRSSElement;
use CodeIgniter\I18n\Time;
+use Config\Mimes;
/**
* Generates the rss feed for a given podcast entity
@@ -217,6 +218,35 @@ function get_rss_feed($podcast, $serviceName = '')
);
$item->addChild('episodeType', $episode->type, $itunes_namespace);
+ if ($episode->transcript) {
+ $transcriptElement = $item->addChild(
+ 'transcript',
+ null,
+ $podcast_namespace
+ );
+ $transcriptElement->addAttribute('url', $episode->transcriptUrl);
+ $transcriptElement->addAttribute(
+ 'type',
+ Mimes::guessTypeFromExtension(
+ pathinfo($episode->transcript_uri, PATHINFO_EXTENSION)
+ )
+ );
+ $transcriptElement->addAttribute(
+ 'language',
+ $podcast->language_code
+ );
+ }
+
+ if ($episode->chapters) {
+ $chaptersElement = $item->addChild(
+ 'chapters',
+ null,
+ $podcast_namespace
+ );
+ $chaptersElement->addAttribute('url', $episode->chaptersUrl);
+ $chaptersElement->addAttribute('type', 'application/json+chapters');
+ }
+
$episode->is_blocked &&
$item->addChild('block', 'Yes', $itunes_namespace);
}
diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php
index 652bad3a..ea045e49 100644
--- a/app/Language/en/Episode.php
+++ b/app/Language/en/Episode.php
@@ -70,6 +70,15 @@ return [
'block' => 'Episode should be hidden from all platforms',
'block_hint' =>
'The episode show or hide status. If you want this episode removed from the Apple directory, toggle this on.',
+ 'additional_files_section_title' => 'Additional files',
+ 'additional_files_section_subtitle' =>
+ 'These files may be used by other platforms to provide better experience to your audience.
See the {podcastNamespaceLink} for more information.',
+ 'transcript' => 'Transcript or closed captions',
+ 'transcript_hint' => 'Allowed formats are txt, html, srt or json.',
+ 'transcript_delete' => 'Delete transcript',
+ 'chapters' => 'Chapters',
+ 'chapters_hint' => 'File should be in JSON Chapters Format.',
+ 'chapters_delete' => 'Delete chapters',
'submit_create' => 'Create episode',
'submit_edit' => 'Save episode',
],
diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php
index a98e63b9..25911e2a 100644
--- a/app/Language/fr/Episode.php
+++ b/app/Language/fr/Episode.php
@@ -70,6 +70,16 @@ return [
'block' => 'L’épisode doit être masqué de toutes les plateformes',
'block_hint' =>
'La visibilité de l’épisode. Si vous souhaitez retirer cet épisode de l’index Apple, activez ce champ.',
+ 'additional_files_section_title' => 'Fichiers additionels',
+ 'additional_files_section_subtitle' =>
+ 'Ces fichiers pourront être utilisées par d’autres plate-formes pour procurer une meilleure expérience à vos auditeurs.
Consulter le {podcastNamespaceLink} pour plus d’informations.',
+ 'transcript' => 'Transcription ou sous-titrage',
+ 'transcript_hint' =>
+ 'Les formats autorisés sont txt, html, srt ou json.',
+ 'transcript_delete' => 'Supprimer la transcription',
+ 'chapters' => 'Chapitrage',
+ 'chapters_hint' => 'Le fichier doit être en "JSON Chapters Format".',
+ 'chapters_delete' => 'Supprimer le chaptrage',
'submit_create' => 'Créer l’épisode',
'submit_edit' => 'Enregistrer l’épisode',
],
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index 9ce7ce0c..c60803a2 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -28,6 +28,8 @@ class EpisodeModel extends Model
'description_markdown',
'description_html',
'image_uri',
+ 'transcript_uri',
+ 'chapters_uri',
'parental_advisory',
'number',
'season_number',
diff --git a/app/Views/_assets/icons/file.svg b/app/Views/_assets/icons/file.svg
new file mode 100644
index 00000000..dcddb396
--- /dev/null
+++ b/app/Views/_assets/icons/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php
index 6d1e50c9..d5e39664 100644
--- a/app/Views/admin/episode/create.php
+++ b/app/Views/admin/episode/create.php
@@ -264,6 +264,40 @@
= form_section_close() ?>
+= form_section(
+ lang('Episode.form.additional_files_section_title'),
+ lang('Episode.form.additional_files_section_subtitle')
+) ?>
+= form_label(
+ lang('Episode.form.transcript'),
+ 'transcript',
+ [],
+ lang('Episode.form.transcript_hint'),
+ true
+) ?>
+= form_input([
+ 'id' => 'transcript',
+ 'name' => 'transcript',
+ 'class' => 'form-input mb-4',
+ 'type' => 'file',
+ 'accept' => '.txt,.html,.srt,.json',
+]) ?>
+= form_label(
+ lang('Episode.form.chapters'),
+ 'chapters',
+ [],
+ lang('Episode.form.chapters_hint'),
+ true
+) ?>
+= form_input([
+ 'id' => 'chapters',
+ 'name' => 'chapters',
+ 'class' => 'form-input mb-4',
+ 'type' => 'file',
+ 'accept' => '.json',
+]) ?>
+= form_section_close() ?>
+
= button(
lang('Episode.form.submit_create'),
null,
diff --git a/app/Views/admin/episode/edit.php b/app/Views/admin/episode/edit.php
index 2b5db05f..541a3789 100644
--- a/app/Views/admin/episode/edit.php
+++ b/app/Views/admin/episode/edit.php
@@ -136,7 +136,6 @@
= form_radio(
['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio-btn'],
-
'bonus',
old('type') ? old('type') === 'bonus' : $episode->type === 'bonus'
) ?>
@@ -273,6 +272,91 @@
old('block', $episode->is_blocked)
) ?>
+= form_section_close() ?>
+= form_section(
+ lang('Episode.form.additional_files_section_title'),
+ lang('Episode.form.additional_files_section_subtitle')
+) ?>
+