diff --git a/app/Config/Images.php b/app/Config/Images.php
index cb8c4e59fc6e6e5fdc10fdc363f6f1065267351b..e48805adf64d6bde68ff3d7bcc54f9d7c1acf8b0 100644
--- a/app/Config/Images.php
+++ b/app/Config/Images.php
@@ -126,6 +126,10 @@ class Images extends BaseConfig
         ],
     ];
 
+    public string $avatarDefaultPath = 'castopod-avatar-default.jpg';
+
+    public string $avatarDefaultMimeType = 'image/jpg';
+
     public string $podcastBannerDefaultPath = 'castopod-banner-default.jpg';
 
     public string $podcastBannerDefaultMimeType = 'image/jpeg';
diff --git a/app/Database/Migrations/2020-05-29-120000_add_media.php b/app/Database/Migrations/2020-05-29-120000_add_media.php
index 6f80aad11463276eb67441428f6e4909e58e367e..f807837014d1dbc2c1cfc023cb24f270829b974f 100644
--- a/app/Database/Migrations/2020-05-29-120000_add_media.php
+++ b/app/Database/Migrations/2020-05-29-120000_add_media.php
@@ -37,7 +37,7 @@ class AddMedia extends Migration
             ],
             'file_metadata' => [
                 'type' => 'JSON',
-                'nullable' => true,
+                'null' => true,
             ],
             'type' => [
                 'type' => 'ENUM',
@@ -46,6 +46,7 @@ class AddMedia extends Migration
             ],
             'description' => [
                 'type' => 'TEXT',
+                'null' => true,
             ],
             'language_code' => [
                 'type' => 'VARCHAR',
diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
index 436c741b5fd872bf4f7791aa487f0b7df5140380..65913aee8e5479b212c3812bfeef32a4640f46de 100644
--- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
+++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
@@ -54,7 +54,6 @@ class AddPodcasts extends Migration
                 'type' => 'INT',
                 'unsigned' => true,
                 'null' => true,
-                'default' => null,
             ],
             'language_code' => [
                 'type' => 'VARCHAR',
@@ -69,7 +68,6 @@ class AddPodcasts extends Migration
                 'type' => 'ENUM',
                 'constraint' => ['clean', 'explicit'],
                 'null' => true,
-                'default' => null,
             ],
             'owner_name' => [
                 'type' => 'VARCHAR',
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 9e0efaa25078539b49e870f16f09f500500b41dc..bbbad3f8dade4fc861aea76ebc8d28a0591d31d5 100644
--- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php
+++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
@@ -79,7 +79,6 @@ class AddEpisodes extends Migration
                 'type' => 'ENUM',
                 'constraint' => ['clean', 'explicit'],
                 'null' => true,
-                'default' => null,
             ],
             'number' => [
                 'type' => 'INT',
diff --git a/app/Database/Migrations/2020-06-05-190000_add_platforms.php b/app/Database/Migrations/2020-06-05-190000_add_platforms.php
index 24dec9434ab8b39a7d6a2da2fbbe77fa06e03ded..7cc3231e360c312dc4ade70cfbfc918318a3a3a6 100644
--- a/app/Database/Migrations/2020-06-05-190000_add_platforms.php
+++ b/app/Database/Migrations/2020-06-05-190000_add_platforms.php
@@ -39,7 +39,6 @@ class AddPlatforms extends Migration
                 'type' => 'VARCHAR',
                 'constraint' => 512,
                 'null' => true,
-                'default' => null,
             ],
         ]);
         $this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()');
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index dbdb47ccde4b6115d0e80380d7ed1c26b4b34fa8..feeede4d35d3051c015960b4c03a7413276192df 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -15,14 +15,13 @@ use App\Entities\Media\Chapters;
 use App\Entities\Media\Image;
 use App\Entities\Media\Transcript;
 use App\Libraries\SimpleRSSElement;
-use App\Models\ClipsModel;
+use App\Models\ClipModel;
 use App\Models\EpisodeCommentModel;
 use App\Models\MediaModel;
 use App\Models\PersonModel;
 use App\Models\PodcastModel;
 use App\Models\PostModel;
 use CodeIgniter\Entity\Entity;
-use CodeIgniter\Files\File;
 use CodeIgniter\HTTP\Files\UploadedFile;
 use CodeIgniter\I18n\Time;
 use League\CommonMark\CommonMarkConverter;
@@ -181,7 +180,7 @@ class Episode extends Entity
         } else {
             $cover = new Image([
                 'file_name' => $this->attributes['slug'],
-                'file_directory' => 'podcasts/' . $this->attributes['handle'],
+                'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
                 'sizes' => config('Images')
                     ->podcastCoverSizes,
                 'uploaded_by' => user_id(),
@@ -197,10 +196,19 @@ class Episode extends Entity
 
     public function getCover(): Image
     {
-        if (! $this->cover instanceof Image) {
-            $this->cover = (new MediaModel('image'))->getMediaById($this->cover_id);
+        if ($this->cover instanceof Image) {
+            return $this->cover;
         }
 
+        if ($this->cover_id === null) {
+            $this->cover = $this->getPodcast()
+                ->getCover();
+
+            return $this->cover;
+        }
+
+        $this->cover = (new MediaModel('image'))->getMediaById($this->cover_id);
+
         return $this->cover;
     }
 
@@ -210,22 +218,22 @@ class Episode extends Entity
             return $this;
         }
 
-        if ($this->audio_id !== null) {
+        if ($this->audio_id !== 0) {
             $this->getAudio()
                 ->setFile($file);
             $this->getAudio()
                 ->updated_by = (int) user_id();
             (new MediaModel('audio'))->updateMedia($this->getAudio());
         } else {
-            $transcript = new Audio([
+            $audio = new Audio([
                 'file_name' => $this->attributes['slug'],
-                'file_directory' => 'podcasts/' . $this->attributes['handle'],
+                'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
                 'uploaded_by' => user_id(),
                 'updated_by' => user_id(),
             ]);
-            $transcript->setFile($file);
+            $audio->setFile($file);
 
-            $this->attributes['transcript_id'] = (new MediaModel())->saveMedia($transcript);
+            $this->attributes['audio_id'] = (new MediaModel())->saveMedia($audio);
         }
 
         return $this;
@@ -255,7 +263,7 @@ class Episode extends Entity
         } else {
             $transcript = new Transcript([
                 'file_name' => $this->attributes['slug'] . '-transcript',
-                'file_directory' => 'podcasts/' . $this->attributes['handle'],
+                'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
                 'uploaded_by' => user_id(),
                 'updated_by' => user_id(),
             ]);
@@ -291,7 +299,7 @@ class Episode extends Entity
         } else {
             $chapters = new Chapters([
                 'file_name' => $this->attributes['slug'] . '-chapters',
-                'file_directory' => 'podcasts/' . $this->attributes['handle'],
+                'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
                 'uploaded_by' => user_id(),
                 'updated_by' => user_id(),
             ]);
@@ -306,7 +314,7 @@ class Episode extends Entity
     public function getChapters(): ?Chapters
     {
         if ($this->chapters_id !== null && $this->chapters === null) {
-            $this->chapters = (new MediaModel('document'))->getMediaById($this->chapters_id);
+            $this->chapters = (new MediaModel('chapters'))->getMediaById($this->chapters_id);
         }
 
         return $this->chapters;
@@ -324,7 +332,7 @@ class Episode extends Entity
         helper('analytics');
 
         // remove 'podcasts/' from audio file path
-        $strippedAudioFilePath = substr($this->audio->file_path, 9);
+        $strippedAudioFilePath = substr($this->getAudio()->file_path, 9);
 
         return generate_episode_analytics_url(
             $this->podcast_id,
@@ -400,7 +408,7 @@ class Episode extends Entity
         }
 
         if ($this->clips === null) {
-            $this->clips = (new ClipsModel())->getEpisodeClips($this->getPodcast() ->id, $this->id);
+            $this->clips = (new ClipModel())->getEpisodeClips($this->getPodcast() ->id, $this->id);
         }
 
         return $this->clips;
diff --git a/app/Entities/Media/Audio.php b/app/Entities/Media/Audio.php
index 288f95506ee66df297bbd333cab5c6b6ed645920..de71409fb841519f622360ad1d22feafe4ddc952 100644
--- a/app/Entities/Media/Audio.php
+++ b/app/Entities/Media/Audio.php
@@ -39,12 +39,13 @@ class Audio extends BaseMedia
         parent::setFile($file);
 
         $getID3 = new GetID3();
-        $audioMetadata = $getID3->analyze((string) $file);
+        $audioMetadata = $getID3->analyze(media_path($this->file_path));
 
-        $this->attributes['file_mimetype'] = $audioMetadata['mimetype'];
+        $this->attributes['file_mimetype'] = $audioMetadata['mime_type'];
         $this->attributes['file_size'] = $audioMetadata['filesize'];
-        $this->attributes['description'] = $audioMetadata['comments']['comment'];
-        $this->attributes['file_metadata'] = $audioMetadata;
+        // @phpstan-ignore-next-line
+        $this->attributes['description'] = @$audioMetadata['id3v2']['comments']['comment'];
+        $this->attributes['file_metadata'] = json_encode($audioMetadata);
 
         return $this;
     }
diff --git a/app/Entities/Media/BaseMedia.php b/app/Entities/Media/BaseMedia.php
index 36b672b99788b5e402b406507b8ce99ff4f7c913..f71781f80c88a6d5218a104f2f86be689b55ac5a 100644
--- a/app/Entities/Media/BaseMedia.php
+++ b/app/Entities/Media/BaseMedia.php
@@ -20,11 +20,12 @@ use CodeIgniter\Files\File;
  * @property string $file_directory
  * @property string $file_extension
  * @property string $file_name
+ * @property string $file_name_with_extension
  * @property int $file_size
  * @property string $file_mimetype
- * @property array $file_metadata
+ * @property array|null $file_metadata
  * @property 'image'|'audio'|'video'|'document' $type
- * @property string $description
+ * @property string|null $description
  * @property string|null $language_code
  * @property int $uploaded_by
  * @property int $updated_by
@@ -33,8 +34,6 @@ class BaseMedia extends Entity
 {
     protected File $file;
 
-    protected string $type = 'document';
-
     /**
      * @var string[]
      */
@@ -49,9 +48,9 @@ class BaseMedia extends Entity
         'file_path' => 'string',
         'file_size' => 'int',
         'file_mimetype' => 'string',
-        'file_metadata' => 'json-array',
+        'file_metadata' => '?json-array',
         'type' => 'string',
-        'description' => 'string',
+        'description' => '?string',
         'language_code' => '?string',
         'uploaded_by' => 'integer',
         'updated_by' => 'integer',
@@ -81,6 +80,7 @@ class BaseMedia extends Entity
             $this->attributes['file_name'] = $filename;
             $this->attributes['file_directory'] = $dirname;
             $this->attributes['file_extension'] = $extension;
+            $this->attributes['file_name_with_extension'] = "{$filename}.{$extension}";
         }
     }
 
@@ -101,4 +101,10 @@ class BaseMedia extends Entity
 
         return $this;
     }
+
+    public function deleteFile(): void
+    {
+        helper('media');
+        unlink(media_path($this->file_path));
+    }
 }
diff --git a/app/Entities/Media/Image.php b/app/Entities/Media/Image.php
index fff51e00a6f730da9b5a3d5f3729084187c5c91f..7d48b0f4d05e4472de4eacde9daa088f3885805c 100644
--- a/app/Entities/Media/Image.php
+++ b/app/Entities/Media/Image.php
@@ -70,9 +70,7 @@ class Image extends BaseMedia
 
     public function deleteFile(): void
     {
-        helper('media');
-
-        unlink(media_path($this->file_path));
+        parent::deleteFile();
 
         $this->deleteSizes();
     }
diff --git a/app/Entities/Person.php b/app/Entities/Person.php
index c8b8478913f6037ae81e35f7ba5e9e22f123e3c3..d13e614e304e703093171a89573da5c5bcbe896f 100644
--- a/app/Entities/Person.php
+++ b/app/Entities/Person.php
@@ -14,6 +14,7 @@ use App\Entities\Media\Image;
 use App\Models\MediaModel;
 use App\Models\PersonModel;
 use CodeIgniter\Entity\Entity;
+use CodeIgniter\Files\File;
 use CodeIgniter\HTTP\Files\UploadedFile;
 use RuntimeException;
 
@@ -55,20 +56,20 @@ class Person extends Entity
     /**
      * Saves the person avatar in `public/media/persons/`
      */
-    public function setAvatar(?UploadedFile $file = null): static
+    public function setAvatar(UploadedFile | File $file = null): static
     {
-        if ($file === null || ! $file->isValid()) {
+        if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) {
             return $this;
         }
 
-        if (array_key_exists('cover_id', $this->attributes) && $this->attributes['cover_id'] !== null) {
+        if (array_key_exists('avatar_id', $this->attributes) && $this->attributes['avatar_id'] !== null) {
             $this->getAvatar()
                 ->setFile($file);
             $this->getAvatar()
                 ->updated_by = (int) user_id();
             (new MediaModel('image'))->updateMedia($this->getAvatar());
         } else {
-            $cover = new Image([
+            $avatar = new Image([
                 'file_name' => $this->attributes['unique_name'],
                 'file_directory' => 'persons',
                 'sizes' => config('Images')
@@ -76,9 +77,9 @@ class Person extends Entity
                 'uploaded_by' => user_id(),
                 'updated_by' => user_id(),
             ]);
-            $cover->setFile($file);
+            $avatar->setFile($file);
 
-            $this->attributes['cover_id'] = (new MediaModel('image'))->saveMedia($cover);
+            $this->attributes['avatar_id'] = (new MediaModel('image'))->saveMedia($avatar);
         }
 
         return $this;
@@ -89,10 +90,15 @@ class Person extends Entity
         if ($this->attributes['avatar_id'] === null) {
             helper('media');
             return new Image([
-                'file_path' => media_path('castopod-avatar-default.jpg'),
-                'file_mimetype' => 'image/jpeg',
-                'sizes' => config('Images')
-                    ->personAvatarSizes,
+                'file_path' => config('Images')
+                    ->avatarDefaultPath,
+                'file_mimetype' => config('Images')
+                    ->avatarDefaultMimeType,
+                'file_size' => 0,
+                'file_metadata' => [
+                    'sizes' => config('Images')
+                        ->personAvatarSizes,
+                ],
             ]);
         }
 
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index d2e5a04bb7996848be9f97d7e1236ce7f38735c2..67534ddb57bd9c66ba3f207ff0521cdf3d7c85d6 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -19,6 +19,7 @@ use App\Models\PersonModel;
 use App\Models\PlatformModel;
 use App\Models\UserModel;
 use CodeIgniter\Entity\Entity;
+use CodeIgniter\Files\File;
 use CodeIgniter\HTTP\Files\UploadedFile;
 use CodeIgniter\I18n\Time;
 use League\CommonMark\CommonMarkConverter;
@@ -194,9 +195,9 @@ class Podcast extends Entity
         return $this->actor;
     }
 
-    public function setCover(?UploadedFile $file = null): self
+    public function setCover(UploadedFile | File $file = null): self
     {
-        if ($file === null || ! $file->isValid()) {
+        if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) {
             return $this;
         }
 
@@ -232,9 +233,9 @@ class Podcast extends Entity
         return $this->cover;
     }
 
-    public function setBanner(?UploadedFile $file): self
+    public function setBanner(UploadedFile | File $file = null): self
     {
-        if ($file === null || ! $file->isValid()) {
+        if ($file === null || ($file instanceof UploadedFile && ! $file->isValid())) {
             return $this;
         }
 
diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php
index 15ad1c088f72c9f898afc9674f8f5a97616d86d7..a608e03ba5fc6d5d050f574aa55d6694e9e2771f 100644
--- a/app/Helpers/components_helper.php
+++ b/app/Helpers/components_helper.php
@@ -144,7 +144,7 @@ if (! function_exists('publication_button')) {
             case 'scheduled':
                 $label = lang('Episode.publish_edit');
                 $route = route_to('episode-publish_edit', $podcastId, $episodeId);
-                $variant = 'accent';
+                $variant = 'warning';
                 $iconLeft = 'upload-cloud';
                 break;
             case 'published':
diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php
index 93e40cf2dc41f61bf769746bc6efbbd9672892a6..076296b505e0b8c6b3c83bf8fe7bd36998fdcc88 100644
--- a/app/Helpers/rss_helper.php
+++ b/app/Helpers/rss_helper.php
@@ -267,7 +267,7 @@ if (! function_exists('get_rss_feed')) {
                 $transcriptElement->addAttribute('language', $podcast->language_code);
             }
 
-            if ($episode->chapters->file_url !== '') {
+            if ($episode->getChapters() !== null) {
                 $chaptersElement = $item->addChild('chapters', null, $podcastNamespace);
                 $chaptersElement->addAttribute('url', $episode->chapters->file_url);
                 $chaptersElement->addAttribute('type', 'application/json+chapters');
diff --git a/app/Models/ClipModel.php b/app/Models/ClipModel.php
index 6a7b2d8cc5f9588f4560e1f2c97b63325bc5ac80..3a1b8b25264746a7395ef75ffdecc891fbf40142 100644
--- a/app/Models/ClipModel.php
+++ b/app/Models/ClipModel.php
@@ -16,7 +16,7 @@ use App\Entities\Clip;
 use CodeIgniter\Database\BaseResult;
 use CodeIgniter\Model;
 
-class ClipsModel extends Model
+class ClipModel extends Model
 {
     /**
      * @var string
diff --git a/app/Models/MediaModel.php b/app/Models/MediaModel.php
index ceea6a957fee43bc3e8a3905d712f6727138dbdf..f0fd5b8389b1a2b340da971430b2630742877acc 100644
--- a/app/Models/MediaModel.php
+++ b/app/Models/MediaModel.php
@@ -128,8 +128,10 @@ class MediaModel extends Model
         return $this->update($media->id, $media);
     }
 
-    public function deleteMedia(int $mediaId): bool
+    public function deleteMedia(object $media): bool
     {
-        return $this->delete($mediaId, true);
+        $media->deleteFile();
+
+        return $this->delete($media->id, true);
     }
 }
diff --git a/modules/Admin/Controllers/EpisodeController.php b/modules/Admin/Controllers/EpisodeController.php
index 637f84da1032ff020c71e2bbbe54d49c124bd6a8..7cdb144a8f2d5fb46a134bd7357db0f7d1eea4eb 100644
--- a/modules/Admin/Controllers/EpisodeController.php
+++ b/modules/Admin/Controllers/EpisodeController.php
@@ -15,9 +15,10 @@ use App\Entities\EpisodeComment;
 use App\Entities\Location;
 use App\Entities\Podcast;
 use App\Entities\Post;
-use App\Models\ClipsModel;
+use App\Models\ClipModel;
 use App\Models\EpisodeCommentModel;
 use App\Models\EpisodeModel;
+use App\Models\MediaModel;
 use App\Models\PodcastModel;
 use App\Models\PostModel;
 use CodeIgniter\Exceptions\PageNotFoundException;
@@ -125,6 +126,9 @@ class EpisodeController extends BaseController
                 ->with('errors', $this->validator->getErrors());
         }
 
+        $db = db_connect();
+        $db->transStart();
+
         $newEpisode = new Episode([
             'podcast_id' => $this->podcast->id,
             'title' => $this->request->getPost('title'),
@@ -143,10 +147,10 @@ class EpisodeController extends BaseController
                     ? $this->request->getPost('parental_advisory')
                     : null,
             'number' => $this->request->getPost('episode_number')
-                ? $this->request->getPost('episode_number')
+                ? (int) $this->request->getPost('episode_number')
                 : null,
             'season_number' => $this->request->getPost('season_number')
-                ? $this->request->getPost('season_number')
+                ? (int) $this->request->getPost('season_number')
                 : null,
             'type' => $this->request->getPost('type'),
             'is_blocked' => $this->request->getPost('block') === 'yes',
@@ -156,9 +160,6 @@ class EpisodeController extends BaseController
             'published_at' => null,
         ]);
 
-        $db = db_connect();
-        $db->transStart();
-
         $transcriptChoice = $this->request->getPost('transcript-choice');
         if ($transcriptChoice === 'upload-file') {
             $newEpisode->setTranscript($this->request->getFile('transcript_file'));
@@ -178,8 +179,8 @@ class EpisodeController extends BaseController
         }
 
         $episodeModel = new EpisodeModel();
-
         if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
+            $db->transRollback();
             return redirect()
                 ->back()
                 ->withInput()
@@ -195,6 +196,7 @@ class EpisodeController extends BaseController
             $podcastModel = new PodcastModel();
 
             if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
+                $db->transRollback();
                 return redirect()
                     ->back()
                     ->withInput()
@@ -202,6 +204,8 @@ class EpisodeController extends BaseController
             }
         }
 
+        $db->transComplete();
+
         return redirect()->route('episode-view', [$this->podcast->id, $newEpisodeId]);
     }
 
@@ -268,36 +272,34 @@ class EpisodeController extends BaseController
         if ($transcriptChoice === 'upload-file') {
             $transcriptFile = $this->request->getFile('transcript_file');
             if ($transcriptFile !== null && $transcriptFile->isValid()) {
-                $this->episode->transcript_file = $transcriptFile;
+                $this->episode->setTranscript($transcriptFile);
                 $this->episode->transcript_remote_url = null;
             }
         } elseif ($transcriptChoice === 'remote-url') {
             if (
-                ($transcriptFileRemoteUrl = $this->request->getPost('transcript_remote_url')) &&
-                (($transcriptFile = $this->episode->transcript_file) !== null)
+                ($transcriptRemoteUrl = $this->request->getPost('transcript_remote_url')) &&
+                (($transcriptFile = $this->episode->transcript_id) !== null)
             ) {
-                unlink((string) $transcriptFile);
-                $this->episode->transcript->file_path = null;
+                (new MediaModel())->deleteMedia($this->episode->transcript);
             }
-            $this->episode->transcript_remote_url = $transcriptFileRemoteUrl === '' ? null : $transcriptFileRemoteUrl;
+            $this->episode->transcript_remote_url = $transcriptRemoteUrl === '' ? null : $transcriptRemoteUrl;
         }
 
         $chaptersChoice = $this->request->getPost('chapters-choice');
         if ($chaptersChoice === 'upload-file') {
             $chaptersFile = $this->request->getFile('chapters_file');
             if ($chaptersFile !== null && $chaptersFile->isValid()) {
-                $this->episode->chapters = $chaptersFile;
+                $this->episode->setChapters($chaptersFile);
                 $this->episode->chapters_remote_url = null;
             }
         } elseif ($chaptersChoice === 'remote-url') {
             if (
-                ($chaptersFileRemoteUrl = $this->request->getPost('chapters_remote_url')) &&
-                (($chaptersFile = $this->episode->chapters_file) !== null)
+                ($chaptersRemoteUrl = $this->request->getPost('chapters_remote_url')) &&
+                (($chaptersFile = $this->episode->chapters) !== null)
             ) {
-                unlink((string) $chaptersFile);
-                $this->episode->chapters->file_path = null;
+                (new MediaModel())->deleteMedia($this->episode->chapters);
             }
-            $this->episode->chapters_remote_url = $chaptersFileRemoteUrl === '' ? null : $chaptersFileRemoteUrl;
+            $this->episode->chapters_remote_url = $chaptersRemoteUrl === '' ? null : $chaptersRemoteUrl;
         }
 
         $db = db_connect();
@@ -338,16 +340,12 @@ class EpisodeController extends BaseController
 
     public function transcriptDelete(): RedirectResponse
     {
-        unlink((string) $this->episode->transcript_file);
-        $this->episode->transcript->file_path = null;
-
-        $episodeModel = new EpisodeModel();
-
-        if (! $episodeModel->update($this->episode->id, $this->episode)) {
+        $mediaModel = new MediaModel();
+        if (! $mediaModel->deleteMedia($this->episode->transcript)) {
             return redirect()
                 ->back()
                 ->withInput()
-                ->with('errors', $episodeModel->errors());
+                ->with('errors', $mediaModel->errors());
         }
 
         return redirect()->back();
@@ -355,16 +353,12 @@ class EpisodeController extends BaseController
 
     public function chaptersDelete(): RedirectResponse
     {
-        unlink((string) $this->episode->chapters_file);
-        $this->episode->chapters->file_path = null;
-
-        $episodeModel = new EpisodeModel();
-
-        if (! $episodeModel->update($this->episode->id, $this->episode)) {
+        $mediaModel = new MediaModel();
+        if (! $mediaModel->deleteMedia($this->episode->chapters)) {
             return redirect()
                 ->back()
                 ->withInput()
-                ->with('errors', $episodeModel->errors());
+                ->with('errors', $mediaModel->errors());
         }
 
         return redirect()->back();
@@ -797,7 +791,7 @@ class EpisodeController extends BaseController
 
     public function soundbiteDelete(string $clipId): RedirectResponse
     {
-        (new ClipsModel())->deleteClip($this->podcast->id, $this->episode->id, (int) $clipId);
+        (new ClipModel())->deleteClip($this->podcast->id, $this->episode->id, (int) $clipId);
 
         return redirect()->route('clips-edit', [$this->podcast->id, $this->episode->id]);
     }
diff --git a/modules/Admin/Controllers/PersonController.php b/modules/Admin/Controllers/PersonController.php
index a2020c022857827468c35efd0b11cad45ec1c2df..da410de48d645930cbdd12920edf411fddbe1a79 100644
--- a/modules/Admin/Controllers/PersonController.php
+++ b/modules/Admin/Controllers/PersonController.php
@@ -76,24 +76,29 @@ class PersonController extends BaseController
                 ->with('errors', $this->validator->getErrors());
         }
 
+        $db = db_connect();
+        $db->transStart();
+
         $person = new Person([
-            'avatar' => $this->request->getFile('avatar'),
             'full_name' => $this->request->getPost('full_name'),
             'unique_name' => $this->request->getPost('unique_name'),
             'information_url' => $this->request->getPost('information_url'),
+            'avatar' => $this->request->getFile('avatar'),
             'created_by' => user_id(),
             'updated_by' => user_id(),
         ]);
 
         $personModel = new PersonModel();
-
         if (! $personModel->insert($person)) {
+            $db->transRollback();
             return redirect()
                 ->back()
                 ->withInput()
                 ->with('errors', $personModel->errors());
         }
 
+        $db->transComplete();
+
         return redirect()->route('person-list');
     }
 
@@ -128,11 +133,7 @@ class PersonController extends BaseController
         $this->person->full_name = $this->request->getPost('full_name');
         $this->person->unique_name = $this->request->getPost('unique_name');
         $this->person->information_url = $this->request->getPost('information_url');
-
-        $avatarFile = $this->request->getFile('avatar');
-        if ($avatarFile !== null && $avatarFile->isValid()) {
-            $this->person->avatar = new Image($avatarFile);
-        }
+        $this->person->setAvatar($this->request->getFile('avatar'));
 
         $this->person->updated_by = user_id();
 
diff --git a/modules/Admin/Controllers/PodcastController.php b/modules/Admin/Controllers/PodcastController.php
index 09cf6693ab41bde26225701bc43ce372626fa44f..22e9a68ea978d241f5b939aba1f7d7b2fe6463e8 100644
--- a/modules/Admin/Controllers/PodcastController.php
+++ b/modules/Admin/Controllers/PodcastController.php
@@ -192,6 +192,9 @@ class PodcastController extends BaseController
             $partnerImageUrl = null;
         }
 
+        $db = db_connect();
+        $db->transStart();
+
         $newPodcast = new Podcast([
             'title' => $this->request->getPost('title'),
             'handle' => $this->request->getPost('handle'),
@@ -226,9 +229,6 @@ class PodcastController extends BaseController
             'updated_by' => user_id(),
         ]);
 
-        $db = db_connect();
-        $db->transStart();
-
         $podcastModel = new PodcastModel();
         if (! ($newPodcastId = $podcastModel->insert($newPodcast, true))) {
             $db->transRollback();
@@ -363,10 +363,8 @@ class PodcastController extends BaseController
             return redirect()->back();
         }
 
-        $this->podcast->banner->deleteFile();
-
         $mediaModel = new MediaModel();
-        if (! $mediaModel->deleteMedia((int) $this->podcast->banner_id)) {
+        if (! $mediaModel->deleteMedia($this->podcast->banner)) {
             return redirect()
                 ->back()
                 ->withInput()
diff --git a/modules/Admin/Controllers/PodcastImportController.php b/modules/Admin/Controllers/PodcastImportController.php
index a990cc0a1d77c2fd8f80b1b6f7e6aeeedc2a33ba..9af473052f2b2cfa4fa0afa2362a2e368421735d 100644
--- a/modules/Admin/Controllers/PodcastImportController.php
+++ b/modules/Admin/Controllers/PodcastImportController.php
@@ -131,6 +131,10 @@ class PodcastImportController extends BaseController
             if (property_exists($nsPodcast, 'guid') && $nsPodcast->guid !== null) {
                 $guid = (string) $nsPodcast->guid;
             }
+
+            $db = db_connect();
+            $db->transStart();
+
             $podcast = new Podcast([
                 'guid' => $guid,
                 'handle' => $this->request->getPost('handle'),
@@ -139,7 +143,7 @@ class PodcastImportController extends BaseController
                 'title' => (string) $feed->channel[0]->title,
                 'description_markdown' => $converter->convert($channelDescriptionHtml),
                 'description_html' => $channelDescriptionHtml,
-                'cover' => new Image($coverFile),
+                'cover' => $coverFile,
                 'banner' => null,
                 'language_code' => $this->request->getPost('language'),
                 'category_id' => $this->request->getPost('category'),
@@ -185,10 +189,6 @@ class PodcastImportController extends BaseController
         }
 
         $podcastModel = new PodcastModel();
-        $db = db_connect();
-
-        $db->transStart();
-
         if (! ($newPodcastId = $podcastModel->insert($podcast, true))) {
             $db->transRollback();
             return redirect()
@@ -249,7 +249,7 @@ class PodcastImportController extends BaseController
                     'full_name' => $fullName,
                     'unique_name' => slugify($fullName),
                     'information_url' => $podcastPerson->attributes()['href'],
-                    'avatar' => new Image(download_file((string) $podcastPerson->attributes()['img'])),
+                    'avatar' => download_file((string) $podcastPerson->attributes()['img']),
                     'created_by' => user_id(),
                     'updated_by' => user_id(),
                 ]);
@@ -326,7 +326,7 @@ class PodcastImportController extends BaseController
                 property_exists($nsItunes, 'image') && $nsItunes->image !== null &&
                 $nsItunes->image->attributes()['href'] !== null
             ) {
-                $episodeCover = new Image(download_file((string) $nsItunes->image->attributes()['href']));
+                $episodeCover = download_file((string) $nsItunes->image->attributes()['href']);
             } else {
                 $episodeCover = null;
             }
@@ -402,7 +402,7 @@ class PodcastImportController extends BaseController
                         'full_name' => $fullName,
                         'unique_name' => slugify($fullName),
                         'information_url' => $episodePerson->attributes()['href'],
-                        'avatar' => new Image(download_file((string) $episodePerson->attributes()['img'])),
+                        'avatar' => download_file((string) $episodePerson->attributes()['img']),
                         'created_by' => user_id(),
                         'updated_by' => user_id(),
                     ]);
diff --git a/modules/Fediverse/Database/Migrations/2018-01-01-100000_add_activities.php b/modules/Fediverse/Database/Migrations/2018-01-01-100000_add_activities.php
index c50c46a741b3c635e034a25ad36652361dc3c3be..417e809d355b908d0a9714d7a82044f8dc395445 100644
--- a/modules/Fediverse/Database/Migrations/2018-01-01-100000_add_activities.php
+++ b/modules/Fediverse/Database/Migrations/2018-01-01-100000_add_activities.php
@@ -48,12 +48,10 @@ class AddActivities extends Migration
                 'type' => 'ENUM',
                 'constraint' => ['queued', 'delivered'],
                 'null' => true,
-                'default' => null,
             ],
             'scheduled_at' => [
                 'type' => 'DATETIME',
                 'null' => true,
-                'default' => null,
             ],
             'created_at' => [
                 'type' => 'DATETIME',
diff --git a/themes/cp_admin/episode/edit.php b/themes/cp_admin/episode/edit.php
index 035b8d6352b539916ad3fb80be063ad112a8f5c5..6e62156348ad678834e3778028393679577a4bb0 100644
--- a/themes/cp_admin/episode/edit.php
+++ b/themes/cp_admin/episode/edit.php
@@ -161,12 +161,12 @@
 
     <div class="py-2 tab-panels">
         <section id="transcript-file-upload" class="flex items-center tab-panel">
-            <?php if ($episode->transcript_file) : ?>
+            <?php if ($episode->transcript) : ?>
                 <div class="flex mb-1 gap-x-2">
                     <?= anchor(
                 $episode->transcript->file_url,
                 icon('file', 'mr-2 text-skin-muted') .
-                            $episode->transcript_file,
+                            $episode->transcript->file_name_with_extension,
                 [
                     'class' => 'inline-flex items-center text-xs',
                     'target' => '_blank',
@@ -218,33 +218,33 @@
 
     <div class="py-2 tab-panels">
         <section id="chapters-file-upload" class="flex items-center tab-panel">
-            <?php if ($episode->chapters_file) : ?>
+            <?php if ($episode->chapters) : ?>
                 <div class="flex mb-1 gap-x-2">
                     <?= anchor(
                 $episode->chapters->file_url,
-                icon('file', 'mr-2') . $episode->chapters_file,
+                icon('file', 'mr-2') . $episode->chapters->file_name_with_extension,
                 [
                     'class' => 'inline-flex items-center text-xs',
                     'target' => '_blank',
                     'rel' => 'noreferrer noopener',
                 ],
             ) .
-                        anchor(
-                            route_to(
-                                'chapters-delete',
-                                $podcast->id,
-                                $episode->id,
+                    anchor(
+                        route_to(
+                            'chapters-delete',
+                            $podcast->id,
+                            $episode->id,
+                        ),
+                        icon('delete-bin', 'mx-auto'),
+                        [
+                            'class' =>
+                            'text-sm p-1 bg-red-100 rounded-full text-red-700 hover:text-red-900 focus:ring-accent',
+                            'data-tooltip' => 'bottom',
+                            'title' => lang(
+                                'Episode.form.chapters_file_delete',
                             ),
-                            icon('delete-bin', 'mx-auto'),
-                            [
-                                'class' =>
-                                'text-sm p-1 bg-red-100 rounded-full text-red-700 hover:text-red-900 focus:ring-accent',
-                                'data-tooltip' => 'bottom',
-                                'title' => lang(
-                                    'Episode.form.chapters_file_delete',
-                                ),
-                            ],
-                        ) ?>
+                        ],
+                    ) ?>
                 </div>
             <?php endif; ?>
             <Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></Forms.Label>
diff --git a/themes/cp_admin/person/create.php b/themes/cp_admin/person/create.php
index a7e2ec01931f56a793a46446623a40dfc1feeed0..09f15dc6f6956905c366ef2cd45691cdc64ef460 100644
--- a/themes/cp_admin/person/create.php
+++ b/themes/cp_admin/person/create.php
@@ -32,7 +32,8 @@
     name="unique_name"
     label="<?= lang('Person.form.unique_name') ?>"
     hint="<?= lang('Person.form.unique_name_hint') ?>"
-    required="true" />
+    required="true"
+    data-slugify="slug" />
 <Forms.Field
     name="information_url"
     label="<?= lang('Person.form.information_url') ?>"