diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 23c5e95152c5ce5e87ace05e444b0df2fc0d4cd4..cfbc02c9b88be296bc3cabd726a0300e7ced386a 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -253,6 +253,29 @@ $routes->group(
                                 'filter' => 'permission:podcast_episodes-edit',
                             ]
                         );
+                        $routes->get(
+                            'soundbites',
+                            'Episode::soundbitesEdit/$1/$2',
+                            [
+                                'as' => 'soundbites-edit',
+                                'filter' => 'permission:podcast_episodes-edit',
+                            ]
+                        );
+                        $routes->post(
+                            'soundbites',
+                            'Episode::soundbitesAttemptEdit/$1/$2',
+                            [
+                                'filter' => 'permission:podcast_episodes-edit',
+                            ]
+                        );
+                        $routes->add(
+                            'soundbites/(:num)/delete',
+                            'Episode::soundbiteDelete/$1/$2/$3',
+                            [
+                                'as' => 'soundbite-delete',
+                                'filter' => 'permission:podcast_episodes-edit',
+                            ]
+                        );
                     });
                 });
 
diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php
index 4f2531c8cdb02fb00602fd86e4502ffc53869449..f88c49b277802b6e7ab2ca4de3a4c9bbe307686a 100644
--- a/app/Controllers/Admin/Episode.php
+++ b/app/Controllers/Admin/Episode.php
@@ -10,6 +10,7 @@ namespace App\Controllers\Admin;
 
 use App\Models\EpisodeModel;
 use App\Models\PodcastModel;
+use App\Models\SoundbiteModel;
 use CodeIgniter\I18n\Time;
 
 class Episode extends BaseController
@@ -24,6 +25,11 @@ class Episode extends BaseController
      */
     protected $episode;
 
+    /**
+     * @var \App\Entities\Soundbite|null
+     */
+    protected $soundbites;
+
     public function _remap($method, ...$params)
     {
         $this->podcast = (new PodcastModel())->getPodcastById($params[0]);
@@ -39,9 +45,12 @@ class Episode extends BaseController
             ) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
             }
+
+            unset($params[1]);
+            unset($params[0]);
         }
 
-        return $this->$method();
+        return $this->$method(...$params);
     }
 
     public function list()
@@ -316,4 +325,89 @@ class Episode extends BaseController
 
         return redirect()->route('episode-list', [$this->podcast->id]);
     }
+
+    public function soundbitesEdit()
+    {
+        helper(['form']);
+
+        $data = [
+            'podcast' => $this->podcast,
+            'episode' => $this->episode,
+        ];
+
+        replace_breadcrumb_params([
+            0 => $this->podcast->title,
+            1 => $this->episode->title,
+        ]);
+        return view('admin/episode/soundbites', $data);
+    }
+
+    public function soundbitesAttemptEdit()
+    {
+        $soundbites_array = $this->request->getPost('soundbites_array');
+        $rules = [
+            'soundbites_array.0.start_time' =>
+                'permit_empty|required_with[soundbites_array.0.duration]|decimal|greater_than_equal_to[0]',
+            'soundbites_array.0.duration' =>
+                'permit_empty|required_with[soundbites_array.0.start_time]|decimal|greater_than_equal_to[0]',
+        ];
+        foreach ($soundbites_array as $soundbite_id => $soundbite) {
+            $rules += [
+                "soundbites_array.{$soundbite_id}.start_time" => 'required|decimal|greater_than_equal_to[0]',
+                "soundbites_array.{$soundbite_id}.duration" => 'required|decimal|greater_than_equal_to[0]',
+            ];
+        }
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        foreach ($soundbites_array as $soundbite_id => $soundbite) {
+            if (
+                !empty($soundbite['start_time']) &&
+                !empty($soundbite['duration'])
+            ) {
+                $data = [
+                    'podcast_id' => $this->podcast->id,
+                    'episode_id' => $this->episode->id,
+                    'start_time' => $soundbite['start_time'],
+                    'duration' => $soundbite['duration'],
+                    'label' => $soundbite['label'],
+                    'updated_by' => user()->id,
+                ];
+                if ($soundbite_id == 0) {
+                    $data += ['created_by' => user()->id];
+                } else {
+                    $data += ['id' => $soundbite_id];
+                }
+                $soundbiteModel = new SoundbiteModel();
+                if (!$soundbiteModel->save($data)) {
+                    return redirect()
+                        ->back()
+                        ->withInput()
+                        ->with('errors', $soundbiteModel->errors());
+                }
+            }
+        }
+        return redirect()->route('soundbites-edit', [
+            $this->podcast->id,
+            $this->episode->id,
+        ]);
+    }
+
+    public function soundbiteDelete($soundbiteId)
+    {
+        (new SoundbiteModel())->deleteSoundbite(
+            $this->podcast->id,
+            $this->episode->id,
+            $soundbiteId
+        );
+
+        return redirect()->route('soundbites-edit', [
+            $this->podcast->id,
+            $this->episode->id,
+        ]);
+    }
 }
diff --git a/app/Database/Migrations/2020-06-05-180000_add_soundbites.php b/app/Database/Migrations/2020-06-05-180000_add_soundbites.php
new file mode 100644
index 0000000000000000000000000000000000000000..57af8f31769ebf0511dd34a48c9b964780848182
--- /dev/null
+++ b/app/Database/Migrations/2020-06-05-180000_add_soundbites.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * Class AddSoundbites
+ * Creates soundbites table in database
+ *
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Migrations;
+
+use CodeIgniter\Database\Migration;
+
+class AddSoundbites extends Migration
+{
+    public function up()
+    {
+        $this->forge->addField([
+            'id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+                'auto_increment' => true,
+            ],
+            'podcast_id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'episode_id' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'start_time' => [
+                'type' => 'FLOAT',
+            ],
+            'duration' => [
+                'type' => 'FLOAT',
+            ],
+            'label' => [
+                'type' => 'VARCHAR',
+                'constraint' => 128,
+                'null' => true,
+            ],
+            'created_by' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'updated_by' => [
+                'type' => 'INT',
+                'unsigned' => true,
+            ],
+            'created_at' => [
+                'type' => 'DATETIME',
+            ],
+            'updated_at' => [
+                'type' => 'DATETIME',
+            ],
+            'deleted_at' => [
+                'type' => 'DATETIME',
+                'null' => true,
+            ],
+        ]);
+        $this->forge->addKey('id', true);
+        $this->forge->addUniqueKey(['episode_id', 'start_time', 'duration']);
+        $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
+        $this->forge->addForeignKey('episode_id', 'episodes', 'id');
+        $this->forge->addForeignKey('created_by', 'users', 'id');
+        $this->forge->addForeignKey('updated_by', 'users', 'id');
+        $this->forge->createTable('soundbites');
+    }
+
+    public function down()
+    {
+        $this->forge->dropTable('soundbites');
+    }
+}
diff --git a/app/Entities/Category.php b/app/Entities/Category.php
index aa1e32d9a0561d197f1be69aca073ef3c993621b..e233f5a5e07687fc5da742a8921c388f8b3df08c 100644
--- a/app/Entities/Category.php
+++ b/app/Entities/Category.php
@@ -19,6 +19,7 @@ class Category extends Entity
     protected $parent;
 
     protected $casts = [
+        'id' => 'integer',
         'parent_id' => 'integer',
         'code' => 'string',
         'apple_category' => 'string',
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index b316e97e292d75f09fbe075e7cb1e3ea96c1a0a6..5d240f9c2d08a52ad19b172c22fb12ddb69dc447 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -9,6 +9,7 @@
 namespace App\Entities;
 
 use App\Models\PodcastModel;
+use App\Models\SoundbiteModel;
 use CodeIgniter\Entity;
 use CodeIgniter\I18n\Time;
 use League\CommonMark\CommonMarkConverter;
@@ -75,6 +76,11 @@ class Episode extends Entity
      */
     protected $chapters_url;
 
+    /**
+     * @var \App\Entities\Soundbite[]
+     */
+    protected $soundbites;
+
     /**
      * Holds text only description, striped of any markdown or html special characters
      *
@@ -95,6 +101,7 @@ class Episode extends Entity
     ];
 
     protected $casts = [
+        'id' => 'integer',
         'guid' => 'string',
         'slug' => 'string',
         'title' => 'string',
@@ -348,6 +355,29 @@ class Episode extends Entity
             : null;
     }
 
+    /**
+     * Returns the episode’s soundbites
+     *
+     * @return \App\Entities\Episode[]
+     */
+    public function getSoundbites()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Episode must be created before getting soundbites.'
+            );
+        }
+
+        if (empty($this->soundbites)) {
+            $this->soundbites = (new SoundbiteModel())->getEpisodeSoundbites(
+                $this->getPodcast()->id,
+                $this->id
+            );
+        }
+
+        return $this->soundbites;
+    }
+
     public function getLink()
     {
         return base_url(
diff --git a/app/Entities/Soundbite.php b/app/Entities/Soundbite.php
new file mode 100644
index 0000000000000000000000000000000000000000..33373c8bde834c34f0af390d4590ff948207ece8
--- /dev/null
+++ b/app/Entities/Soundbite.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Entities;
+
+use CodeIgniter\Entity;
+
+class Soundbite extends Entity
+{
+    protected $casts = [
+        'id' => 'integer',
+        'podcast_id' => 'integer',
+        'episode_id' => 'integer',
+        'start_time' => 'float',
+        'duration' => 'float',
+        'label' => '?string',
+        'created_by' => 'integer',
+        'updated_by' => 'integer',
+    ];
+
+    public function setCreatedBy(\App\Entities\User $user)
+    {
+        $this->attributes['created_by'] = $user->id;
+
+        return $this;
+    }
+
+    public function setUpdatedBy(\App\Entities\User $user)
+    {
+        $this->attributes['updated_by'] = $user->id;
+
+        return $this;
+    }
+}
diff --git a/app/Entities/User.php b/app/Entities/User.php
index 6a3e7a1fcfcb48f5b731b5705609aefe0f761f9b..2c94df540a20d4c467f03749bcc34ef022243941 100644
--- a/app/Entities/User.php
+++ b/app/Entities/User.php
@@ -29,6 +29,7 @@ class User extends \Myth\Auth\Entities\User
      * when they are accessed.
      */
     protected $casts = [
+        'id' => 'integer',
         'active' => 'boolean',
         'force_pass_reset' => 'boolean',
         'podcast_role' => '?string',
diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php
index 88baff7196466fab4a31c290677d93f5054279e8..a00a2b5e2f5561978e3cb01d1d9a36c22ad65ec1 100644
--- a/app/Helpers/rss_helper.php
+++ b/app/Helpers/rss_helper.php
@@ -255,6 +255,19 @@ function get_rss_feed($podcast, $serviceSlug = '')
             $chaptersElement->addAttribute('type', 'application/json+chapters');
         }
 
+        foreach ($episode->soundbites as $soundbite) {
+            $soundbiteElement = $item->addChild(
+                'soundbite',
+                empty($soundbite->label) ? null : $soundbite->label,
+                $podcast_namespace
+            );
+            $soundbiteElement->addAttribute(
+                'start_time',
+                $soundbite->start_time
+            );
+            $soundbiteElement->addAttribute('duration', $soundbite->duration);
+        }
+
         $episode->is_blocked &&
             $item->addChild('block', 'Yes', $itunes_namespace);
     }
diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php
index 8fcfcbebd616232a42417eb52c6b6d2783c4a4c9..27301ab73a22642f900756c65809c9290700520b 100644
--- a/app/Language/en/Breadcrumb.php
+++ b/app/Language/en/Breadcrumb.php
@@ -30,4 +30,5 @@ return [
     'players' => 'players',
     'listening-time' => 'listening time',
     'time-periods' => 'time periods',
+    'soundbites' => 'soundbites',
 ];
diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php
index ea045e49d44b773e38932b8a1c2e169be1afc212..94d175c9ab255208dc4f9fb12c3275bc3ede86c0 100644
--- a/app/Language/en/Episode.php
+++ b/app/Language/en/Episode.php
@@ -82,4 +82,23 @@ return [
         'submit_create' => 'Create episode',
         'submit_edit' => 'Save episode',
     ],
+    'soundbites' => 'Soundbites',
+    'soundbites_form' => [
+        'title' => 'Edit soundbites',
+        'info_section_title' => 'Episode soundbites',
+        'info_section_subtitle' => 'Add, edit or delete soundbites',
+        'start_time' => 'Start',
+        'start_time_hint' =>
+            'The first second of the soundbite, it can be a decimal number.',
+        'duration' => 'Duration',
+        'duration_hint' =>
+            'The duration of the soundbite (in seconds), it can be a decimal number.',
+        'label' => 'Label',
+        'label_hint' => 'Text that will be displayed.',
+        'play' => 'Play soundbite',
+        'delete' => 'Delete soundbite',
+        'bookmark' =>
+            'Click while playing to get current position, click again to get duration.',
+        'submit_edit' => 'Save all soundbites',
+    ],
 ];
diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php
index de8d5f8084b7ca29f050d5eb0b29eb509b97df47..71d8c331d9303395334e10d84c1d0a94f89ba487 100644
--- a/app/Language/fr/Breadcrumb.php
+++ b/app/Language/fr/Breadcrumb.php
@@ -30,4 +30,5 @@ return [
     'players' => 'lecteurs',
     'listening-time' => 'drée d’écoute',
     'time-periods' => 'périodes',
+    'soundbites' => 'extraits sonores',
 ];
diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php
index 25911e2a5d87f793689ba66ae272f9fa86d49183..36d1daac64962d5756b243ead42d58a7ddfbb762 100644
--- a/app/Language/fr/Episode.php
+++ b/app/Language/fr/Episode.php
@@ -83,4 +83,24 @@ return [
         'submit_create' => 'Créer l’épisode',
         'submit_edit' => 'Enregistrer l’épisode',
     ],
+    'soundbites' => 'Extraits sonores',
+    'soundbites_form' => [
+        'title' => 'Modifier les extraits sonores',
+        'info_section_title' => 'Extraits sonores de l’épisode',
+        'info_section_subtitle' =>
+            'Ajouter, modifier ou supprimer des extraits sonores',
+        'start_time' => 'Début',
+        'start_time_hint' =>
+            'La première seconde de l’extrait sonore, cela peut être un nombre décimal.',
+        'duration' => 'Durée',
+        'duration_hint' =>
+            'La durée de l’extrait sonore (en secondes), cela peut être un nombre décimal.',
+        'label' => 'Libellé',
+        'label_hint' => 'Texte qui sera affiché.',
+        'play' => 'Écouter l’extrait sonore',
+        'delete' => 'Supprimer l’extrait sonore',
+        'bookmark' =>
+            'Cliquez pour récupérer la position actuelle, cliquez à nouveau pour récupérer la durée.',
+        'submit_edit' => 'Enregistrer tous les extraits sonores',
+    ],
 ];
diff --git a/app/Models/SoundbiteModel.php b/app/Models/SoundbiteModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa55d5927c967fc85bae34bc92ab32a965e50637
--- /dev/null
+++ b/app/Models/SoundbiteModel.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * Class SoundbiteModel
+ * Model for podcasts_soundbites table in database
+ *
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class SoundbiteModel extends Model
+{
+    protected $table = 'soundbites';
+    protected $primaryKey = 'id';
+
+    protected $allowedFields = [
+        'podcast_id',
+        'episode_id',
+        'label',
+        'start_time',
+        'duration',
+        'created_by',
+        'updated_by',
+    ];
+
+    protected $returnType = \App\Entities\Soundbite::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = true;
+
+    protected $afterInsert = ['clearCache'];
+    protected $afterUpdate = ['clearCache'];
+    protected $beforeDelete = ['clearCache'];
+
+    public function deleteSoundbite($podcastId, $episodeId, $soundbiteId)
+    {
+        return $this->delete([
+            'podcast_id' => $podcastId,
+            'episode_id' => $episodeId,
+            'id' => $soundbiteId,
+        ]);
+    }
+
+    /**
+     * Gets all soundbites for an episode
+     *
+     * @param int $podcastId
+     * @param int $episodeId
+     *
+     * @return \App\Entities\Soundbite[]
+     */
+    public function getEpisodeSoundbites(int $podcastId, int $episodeId): array
+    {
+        if (!($found = cache("episode{$episodeId}_soundbites"))) {
+            $found = $this->where([
+                'episode_id' => $episodeId,
+                'podcast_id' => $podcastId,
+            ])
+                ->orderBy('start_time')
+                ->findAll();
+            cache()->save("episode{$episodeId}_soundbites", $found, DECADE);
+        }
+        return $found;
+    }
+
+    public function clearCache(array $data)
+    {
+        $episode = (new EpisodeModel())->find(
+            isset($data['data'])
+                ? $data['data']['episode_id']
+                : $data['id']['episode_id']
+        );
+
+        cache()->delete("episode{$episode->id}_soundbites");
+
+        // delete cache for rss feed
+        cache()->delete("podcast{$episode->id}_feed");
+        foreach (\Opawg\UserAgentsPhp\UserAgentsRSS::$db as $service) {
+            cache()->delete(
+                "podcast{$episode->podcast->id}_feed_{$service['slug']}"
+            );
+        }
+
+        $supportedLocales = config('App')->supportedLocales;
+        foreach ($supportedLocales as $locale) {
+            cache()->delete(
+                "page_podcast{$episode->podcast->id}_episode{$episode->id}_{$locale}"
+            );
+        }
+        return $data;
+    }
+}
diff --git a/app/Views/_assets/icons/bookmark.svg b/app/Views/_assets/icons/bookmark.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f340d6ed246d92df66f1d1be3101a0d0c489624c
--- /dev/null
+++ b/app/Views/_assets/icons/bookmark.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 2h14a1 1 0 0 1 1 1v19.143a.5.5 0 0 1-.766.424L12 18.03l-7.234 4.536A.5.5 0 0 1 4 22.143V3a1 1 0 0 1 1-1z"/></svg>
\ No newline at end of file
diff --git a/app/Views/_assets/icons/play.svg b/app/Views/_assets/icons/play.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4978d3d5b5d56da9ac0cd84dd565d21aeb1ec076
--- /dev/null
+++ b/app/Views/_assets/icons/play.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7.752 5.439l10.508 6.13a.5.5 0 0 1 0 .863l-10.508 6.13A.5.5 0 0 1 7 18.128V5.871a.5.5 0 0 1 .752-.432z"/></svg>
\ No newline at end of file
diff --git a/app/Views/_assets/icons/timer.svg b/app/Views/_assets/icons/timer.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4f2136e6e9ef1654f4f3e4d151ef7db7921785ec
--- /dev/null
+++ b/app/Views/_assets/icons/timer.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M17.618 5.968l1.453-1.453 1.414 1.414-1.453 1.453a9 9 0 1 1-1.414-1.414zM11 8v6h2V8h-2zM8 1h8v2H8V1z"/></svg>
\ No newline at end of file
diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts
index 777a0d8e1a63e8b2208e6e2cf90917e84d2b2f9e..08014db121679caa4b6a9ab8896112af61d4748f 100644
--- a/app/Views/_assets/modules/Charts.ts
+++ b/app/Views/_assets/modules/Charts.ts
@@ -3,7 +3,7 @@ import am4geodata_worldLow from "@amcharts/amcharts4-geodata/worldLow";
 import * as am4charts from "@amcharts/amcharts4/charts";
 import * as am4core from "@amcharts/amcharts4/core";
 import * as am4maps from "@amcharts/amcharts4/maps";
-import * as am4plugins_sliceGrouper from "@amcharts/amcharts4/plugins/sliceGrouper"; 
+import * as am4plugins_sliceGrouper from "@amcharts/amcharts4/plugins/sliceGrouper";
 import am4themes_material from "@amcharts/amcharts4/themes/material";
 
 const drawPieChart = (chartDivId: string, dataUrl: string | null): void => {
@@ -21,7 +21,9 @@ const drawPieChart = (chartDivId: string, dataUrl: string | null): void => {
   chart.dataSource.parser.options.emptyAs = 0;
   // Add and configure Series
   const pieSeries = chart.series.push(new am4charts.PieSeries());
-  const grouper = pieSeries.plugins.push(new am4plugins_sliceGrouper.SliceGrouper());
+  const grouper = pieSeries.plugins.push(
+    new am4plugins_sliceGrouper.SliceGrouper()
+  );
   grouper.limit = 9;
   grouper.groupName = "- Other -";
   grouper.clickBehavior = "break";
@@ -95,13 +97,12 @@ const drawBarChart = (chartDivId: string, dataUrl: string | null): void => {
   series.dataFields.categoryX = "labels";
   series.name = "Hits";
   series.columns.template.tooltipText = "{valueY} hits";
-  series.columns.template.fillOpacity = .8;
+  series.columns.template.fillOpacity = 0.8;
   const columnTemplate = series.columns.template;
   columnTemplate.strokeWidth = 2;
   columnTemplate.strokeOpacity = 1;
 };
 
-
 const drawXYDurationChart = (
   chartDivId: string,
   dataUrl: string | null
diff --git a/app/Views/_assets/modules/Soundbites.ts b/app/Views/_assets/modules/Soundbites.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b8f7ffbf0c33bd6efd27d370f4008d88f0080875
--- /dev/null
+++ b/app/Views/_assets/modules/Soundbites.ts
@@ -0,0 +1,95 @@
+let timeout: number | null = null;
+
+const playSoundbite = (
+  audioPlayer: HTMLAudioElement,
+  startTime: number,
+  duration: number
+): void => {
+  audioPlayer.currentTime = startTime;
+  if (duration > 0) {
+    audioPlayer.play();
+    if (timeout) {
+      clearTimeout(timeout);
+      timeout = null;
+    }
+    timeout = window.setTimeout(() => {
+      audioPlayer.pause();
+      timeout = null;
+    }, duration * 1000);
+  }
+};
+
+const Soundbites = (): void => {
+  const audioPlayer: HTMLAudioElement | null = document.querySelector("audio");
+
+  if (audioPlayer) {
+    const soundbiteButton: HTMLButtonElement | null = document.querySelector(
+      "button[data-type='get-soundbite']"
+    );
+    if (soundbiteButton) {
+      const startTimeField: HTMLInputElement | null = document.querySelector(
+        `input[name="${soundbiteButton.dataset.startTimeFieldName}"]`
+      );
+      const durationField: HTMLInputElement | null = document.querySelector(
+        `input[name="${soundbiteButton.dataset.durationFieldName}"]`
+      );
+
+      if (startTimeField && durationField) {
+        soundbiteButton.addEventListener("click", () => {
+          if (startTimeField.value === "") {
+            startTimeField.value = (
+              Math.round(audioPlayer.currentTime * 100) / 100
+            ).toString();
+          } else {
+            durationField.value = (
+              Math.round(
+                (audioPlayer.currentTime - Number(startTimeField.value)) * 100
+              ) / 100
+            ).toString();
+          }
+        });
+      }
+    }
+
+    const soundbitePlayButtons: NodeListOf<
+      HTMLButtonElement
+    > | null = document.querySelectorAll("button[data-type='play-soundbite']");
+    if (soundbitePlayButtons) {
+      for (let i = 0; i < soundbitePlayButtons.length; i++) {
+        const soundbitePlayButton: HTMLButtonElement = soundbitePlayButtons[i];
+        soundbitePlayButton.addEventListener("click", () => {
+          playSoundbite(
+            audioPlayer,
+            Number(soundbitePlayButton.dataset.soundbiteStartTime),
+            Number(soundbitePlayButton.dataset.soundbiteDuration)
+          );
+        });
+      }
+    }
+
+    const inputFields: NodeListOf<
+      HTMLInputElement
+    > | null = document.querySelectorAll("input[data-type='soundbite-field']");
+    if (inputFields) {
+      for (let i = 0; i < inputFields.length; i++) {
+        const inputField: HTMLInputElement = inputFields[i];
+        const soundbitePlayButton: HTMLButtonElement | null = document.querySelector(
+          `button[data-type="play-soundbite"][data-soundbite-id="${inputField.dataset.soundbiteId}"]`
+        );
+        if (soundbitePlayButton) {
+          if (inputField.dataset.fieldType == "start-time") {
+            inputField.addEventListener("input", () => {
+              soundbitePlayButton.dataset.soundbiteStartTime = inputField.value;
+            });
+          } else if (inputField.dataset.fieldType == "duration") {
+            inputField.addEventListener("input", () => {
+              soundbitePlayButton.dataset.soundbiteDuration = inputField.value;
+            });
+          }
+        }
+      }
+    }
+  }
+};
+
+export default Soundbites;
diff --git a/app/Views/_assets/soundbites.ts b/app/Views/_assets/soundbites.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fa8b6be11633e94f747419f15274cdbfd2371e21
--- /dev/null
+++ b/app/Views/_assets/soundbites.ts
@@ -0,0 +1,3 @@
+import Soundbites from "./modules/Soundbites";
+
+Soundbites();
diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php
index 6a8d1706679c353851225363792b033cecdfd09b..3f1289cc29c55cdbee539e237bb3d42e460e1224 100644
--- a/app/Views/admin/_layout.php
+++ b/app/Views/admin/_layout.php
@@ -10,6 +10,7 @@
     <link rel="stylesheet" href="/assets/admin.css"/>
     <link rel="stylesheet" href="/assets/index.css"/>
     <script src="/assets/admin.js" type="module" defer></script>
+    <script src="/assets/soundbites.js" type="module" defer></script>
 </head>
 
 <body class="relative bg-gray-100 holy-grail-grid">
diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php
index dff7ceffb1e889f612f4786975f64b8631ab3e59..da3170e9f07998d46a931156ee3a3ecaf957482a 100644
--- a/app/Views/admin/episode/list.php
+++ b/app/Views/admin/episode/list.php
@@ -11,10 +11,12 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('headerRight') ?>
-<?= button(lang('Episode.create'), route_to('episode-create', $podcast->id), [
-    'variant' => 'primary',
-    'iconLeft' => 'add',
-]) ?>
+<?= button(
+    lang('Episode.create'),
+    route_to('episode-create', $podcast->id),
+
+    ['variant' => 'primary', 'iconLeft' => 'add']
+) ?>
 <?= $this->endSection() ?>
 
 
@@ -59,6 +61,13 @@
                                         $podcast->id,
                                         $episode->id
                                     ) ?>"><?= lang('Episode.edit') ?></a>
+                                    <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
+                                        'soundbites-edit',
+                                        $podcast->id,
+                                        $episode->id
+                                    ) ?>"><?= lang(
+    'Episode.soundbites'
+) ?></a>
                                     <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
                                         'episode',
                                         $podcast->name,
diff --git a/app/Views/admin/episode/soundbites.php b/app/Views/admin/episode/soundbites.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a5ff48404b8e9cad1eb95caaa83e3916120492f
--- /dev/null
+++ b/app/Views/admin/episode/soundbites.php
@@ -0,0 +1,198 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Episode.soundbites_form.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Episode.soundbites_form.title') ?>
+<?= $this->endSection() ?>
+
+
+<?= $this->section('content') ?>
+
+<?= form_open_multipart(
+    route_to('episode-soundbites-edit', $podcast->id, $episode->id),
+    ['method' => 'post', 'class' => 'flex flex-col']
+) ?>
+<?= csrf_field() ?>
+
+<?= form_section(
+    lang('Episode.soundbites_form.info_section_title'),
+    lang('Episode.soundbites_form.info_section_subtitle')
+) ?>
+
+    <table class="w-full table-fixed">
+        <thead>
+        <tr>
+            <th class="w-3/12 px-1 py-2">       
+            <?= form_label(
+                lang('Episode.soundbites_form.start_time'),
+                'start_time',
+                [],
+                lang('Episode.soundbites_form.start_time_hint')
+            ) ?></th>
+            <th class="w-3/12 px-1 py-2"><?= form_label(
+                lang('Episode.soundbites_form.duration'),
+                'duration',
+                [],
+                lang('Episode.soundbites_form.duration_hint')
+            ) ?></th>
+            <th class="w-7/12 px-1 py-2"><?= form_label(
+                lang('Episode.soundbites_form.label'),
+                'label',
+                [],
+                lang('Episode.soundbites_form.label_hint'),
+                true
+            ) ?></th>
+            <th class="w-1/12 px-1 py-2"></th>
+            </tr>
+        </thead>
+        <tbody>
+        <?php foreach ($episode->soundbites as $soundbite): ?>
+        <tr>
+            <td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input(
+                [
+                    'id' => "soundbites_array[{$soundbite->id}][start_time]",
+                    'name' => "soundbites_array[{$soundbite->id}][start_time]",
+                    'class' => 'form-input w-full border-none text-center',
+                    'value' => $soundbite->start_time,
+                    'data-type' => 'soundbite-field',
+                    'data-field-type' => 'start-time',
+                    'data-soundbite-id' => $soundbite->id,
+                    'required' => 'required',
+                    'min' => '0',
+                ]
+            ) ?></td>
+            <td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input(
+                [
+                    'id' => "soundbites_array[{$soundbite->id}][duration]",
+                    'name' => "soundbites_array[{$soundbite->id}][duration]",
+                    'class' => 'form-input w-full border-none text-center',
+                    'value' => $soundbite->duration,
+                    'data-type' => 'soundbite-field',
+                    'data-field-type' => 'duration',
+                    'data-soundbite-id' => $soundbite->id,
+                    'required' => 'required',
+                    'min' => '0',
+                ]
+            ) ?></td>
+            <td class="px-1 py-2 font-medium bg-white border border-light-blue-500"><?= form_input(
+                [
+                    'id' => "soundbites_array[{$soundbite->id}][label]",
+                    'name' => "soundbites_array[{$soundbite->id}][label]",
+                    'class' => 'form-input w-full border-none',
+                    'value' => $soundbite->label,
+                ]
+            ) ?></td>
+            <td class="px-4 py-2"><?= icon_button(
+                'play',
+                lang('Episode.soundbites_form.play'),
+                null,
+                ['variant' => 'primary'],
+                [
+                    'class' => 'mb-1 mr-1',
+                    'data-type' => 'play-soundbite',
+                    'data-soundbite-id' => $soundbite->id,
+                    'data-soundbite-start-time' => $soundbite->start_time,
+                    'data-soundbite-duration' => $soundbite->duration,
+                ]
+            ) ?>
+            <?= icon_button(
+                'delete-bin',
+                lang('Episode.soundbites_form.delete'),
+                route_to(
+                    'soundbite-delete',
+                    $podcast->id,
+                    $episode->id,
+                    $soundbite->id
+                ),
+                ['variant' => 'danger'],
+                []
+            ) ?>    
+            </td>
+        </tr>
+        <?php endforeach; ?>
+        <tr>
+        <td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input(
+            [
+                'id' => 'soundbites_array[0][start_time]',
+                'name' => 'soundbites_array[0][start_time]',
+                'class' => 'form-input w-full border-none text-center',
+                'value' => old('start_time'),
+                'data-soundbite-id' => '0',
+                'data-type' => 'soundbite-field',
+                'data-field-type' => 'start-time',
+                'min' => '0',
+            ]
+        ) ?></td>
+        <td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input(
+            [
+                'id' => 'soundbites_array[0][duration]',
+                'name' => 'soundbites_array[0][duration]',
+                'class' => 'form-input w-full border-none text-center',
+                'value' => old('duration'),
+                'data-soundbite-id' => '0',
+                'data-type' => 'soundbite-field',
+                'data-field-type' => 'duration',
+                'min' => '0',
+            ]
+        ) ?></td>
+        <td class="px-1 py-4 font-medium bg-white border border-light-blue-500"><?= form_input(
+            [
+                'id' => 'soundbites_array[0][label]',
+                'name' => 'soundbites_array[0][label]',
+                'class' => 'form-input w-full border-none',
+                'value' => old('label'),
+            ]
+        ) ?></td>
+        <td class="px-4 py-2"><?= icon_button(
+            'play',
+            lang('Episode.soundbites_form.play'),
+            null,
+            ['variant' => 'primary'],
+            [
+                'data-type' => 'play-soundbite',
+                'data-soundbite-id' => 0,
+                'data-soundbite-start-time' => 0,
+                'data-soundbite-duration' => 0,
+            ]
+        ) ?>
+            
+                    
+        </td>
+        </tr>
+        <tr><td colspan="3">
+            <audio controls preload="auto" class="w-full">
+                <source src="/<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>">
+        Your browser does not support the audio tag.
+            </audio>
+        </td><td class="px-4 py-2"><?= icon_button(
+            'timer',
+            lang('Episode.soundbites_form.bookmark'),
+            null,
+            ['variant' => 'info'],
+            [
+                'data-type' => 'get-soundbite',
+                'data-start-time-field-name' =>
+                    'soundbites_array[0][start_time]',
+                'data-duration-field-name' => 'soundbites_array[0][duration]',
+            ]
+        ) ?></td></tr>
+    </tbody>
+    </table>
+
+    
+<?= form_section_close() ?>
+
+<?= button(
+    lang('Episode.soundbites_form.submit_edit'),
+    null,
+    ['variant' => 'primary'],
+    ['type' => 'submit', 'class' => 'self-end']
+) ?>
+
+<?= form_close() ?>
+
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php
index d36151f19e277e6dd523a46344c210ad5cc4a0e3..74744e47366bdfba9474d720d673343d58111f8b 100644
--- a/app/Views/admin/episode/view.php
+++ b/app/Views/admin/episode/view.php
@@ -22,7 +22,7 @@
             alt="Episode cover"
             class="object-cover w-full"
         />
-        <audio controls preload="none" class="w-full mb-6">
+        <audio controls preload="auto" class="w-full mb-6">
         <source src="/<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>">
         Your browser does not support the audio tag.
         </audio>
@@ -51,6 +51,57 @@
     </section>
 </div>
 
+    <div class="mb-12">
+    <?= button(
+        lang('Episode.soundbites_form.title'),
+        route_to('soundbites-edit', $podcast->id, $episode->id),
+        ['variant' => 'info', 'iconLeft' => 'edit'],
+        ['class' => 'mb-4']
+    ) ?>
+    <?php if (count($episode->soundbites) > 0): ?>
+    <?= data_table(
+        [
+            [
+                'header' => 'Play',
+                'cell' => function ($soundbite) {
+                    return icon_button(
+                        'play',
+                        lang('Episode.soundbites_form.play'),
+                        null,
+                        ['variant' => 'primary'],
+                        [
+                            'class' => 'mb-1 mr-1',
+                            'data-type' => 'play-soundbite',
+                            'data-soundbite-start-time' =>
+                                $soundbite->start_time,
+                            'data-soundbite-duration' => $soundbite->duration,
+                        ]
+                    );
+                },
+            ],
+            [
+                'header' => lang('Episode.soundbites_form.start_time'),
+                'cell' => function ($soundbite) {
+                    return format_duration($soundbite->start_time);
+                },
+            ],
+            [
+                'header' => lang('Episode.soundbites_form.duration'),
+                'cell' => function ($soundbite) {
+                    return format_duration($soundbite->duration);
+                },
+            ],
+            [
+                'header' => lang('Episode.soundbites_form.label'),
+                'cell' => function ($soundbite) {
+                    return $soundbite->label;
+                },
+            ],
+        ],
+        $episode->soundbites
+    ) ?>
+    <?php endif; ?>
+    </div>
 
     <div class="mb-12 text-center">
     <h2><?= lang('Charts.episode_by_day') ?></h2>
diff --git a/app/Views/episode.php b/app/Views/episode.php
index 4b04a7d3b78e792b67cc7e7421cc64fe977bc1cd..08a6ae1191b439ef0705ce649cf8299ee97c0c29 100644
--- a/app/Views/episode.php
+++ b/app/Views/episode.php
@@ -15,6 +15,7 @@
     <link rel="stylesheet" href="/assets/index.css"/>
     <link rel="canonical" href="<?= current_url() ?>" />
     <script src="/assets/podcast.js" type="module" defer></script>
+    <script src="/assets/soundbites.js" type="module" defer></script>
     <meta property="og:title" content="<?= $episode->title ?>" />
     <meta property="og:locale" content="<?= $podcast->language_code ?>" />
     <meta property="og:site_name" content="<?= $podcast->title ?>" />
@@ -107,6 +108,52 @@
         </div>
       </header>
 
+    <?php if (count($episode->soundbites) > 0): ?>
+        <div class="w-full max-w-3xl px-2 py-6 mx-auto md:px-6">
+    <?= data_table(
+        [
+            [
+                'header' => lang('Episode.soundbites'),
+                'cell' => function ($soundbite) {
+                    return icon_button(
+                        'play',
+                        lang('Episode.soundbites_form.play'),
+                        null,
+                        ['variant' => 'primary'],
+                        [
+                            'class' => 'mb-1 mr-1',
+                            'data-type' => 'play-soundbite',
+                            'data-soundbite-start-time' =>
+                                $soundbite->start_time,
+                            'data-soundbite-duration' => $soundbite->duration,
+                        ]
+                    );
+                },
+            ],
+            [
+                'header' => lang('Episode.soundbites_form.start_time'),
+                'cell' => function ($soundbite) {
+                    return format_duration($soundbite->start_time);
+                },
+            ],
+            [
+                'header' => lang('Episode.soundbites_form.duration'),
+                'cell' => function ($soundbite) {
+                    return format_duration($soundbite->duration);
+                },
+            ],
+            [
+                'header' => lang('Episode.soundbites_form.label'),
+                'cell' => function ($soundbite) {
+                    return $soundbite->label;
+                },
+            ],
+        ],
+        $episode->soundbites
+    ) ?>
+    </div>
+<?php endif; ?>
+      
       <section class="w-full max-w-3xl px-2 py-6 mx-auto prose md:px-6">
       <?= $episode->description_html ?>
       </section>