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 @@ + + + 'transcript', + 'name' => 'transcript', + 'class' => 'form-input mb-4', + 'type' => 'file', + 'accept' => '.txt,.html,.srt,.json', +]) ?> + + 'chapters', + 'name' => 'chapters', + 'class' => 'form-input mb-4', + 'type' => 'file', + 'accept' => '.json', +]) ?> + + '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) ) ?> + + +
+ +transcript): ?> +
+ transcriptUrl, + icon('file', 'mr-2') . $episode->transcript, + [ + 'class' => 'inline-flex items-center text-xs', + 'target' => '_blank', + 'rel' => 'noreferrer noopener', + ] + ) . + anchor( + route_to('transcript-delete', $podcast->id, $episode->id), + icon('delete-bin', 'mx-auto'), + [ + 'class' => + 'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900', + 'data-toggle' => 'tooltip', + 'data-placement' => 'bottom', + 'title' => lang('Episode.form.transcript_delete'), + ] + ) ?> +
+ + 'transcript', + 'name' => 'transcript', + 'class' => 'form-input mb-4', + 'type' => 'file', + 'accept' => '.txt,.html,.srt,.json', +]) ?> +
+
+ +chapters): ?> +
+ chaptersUrl, + icon('file', 'mr-2') . $episode->chapters, + [ + 'class' => 'inline-flex items-center text-xs', + 'target' => '_blank', + 'rel' => 'noreferrer noopener', + ] + ) . + anchor( + route_to('chapters-delete', $podcast->id, $episode->id), + icon('delete-bin', 'mx-auto'), + [ + 'class' => + 'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900', + 'data-toggle' => 'tooltip', + 'data-placement' => 'bottom', + 'title' => lang('Episode.form.chapters_delete'), + ] + ) ?> +
+ + 'chapters', + 'name' => 'chapters', + 'class' => 'form-input mb-4', + 'type' => 'file', + 'accept' => '.json', +]) ?> +