diff --git a/app/Models/MediaModel.php b/app/Models/MediaModel.php index 85695c410b2942de9c3e8508c38822dc87c9d388..bc61689d23e6b749587d88381194907f04bc0a4f 100644 --- a/app/Models/MediaModel.php +++ b/app/Models/MediaModel.php @@ -145,6 +145,21 @@ class MediaModel extends Model return $this->update($media->id, $media); } + /** + * @return array<mixed> + */ + public function getAllOfType(): array + { + $result = $this->where('type', $this->fileType) + ->findAll(); + $mediaClass = $this->returnType; + foreach ($result as $key => $media) { + $result[$key] = new $mediaClass($media->toArray(false, true)); + } + + return $result; + } + public function deleteMedia(object $media): bool { $media->deleteFile(); diff --git a/app/Resources/icons/home-gear.svg b/app/Resources/icons/home-gear.svg new file mode 100644 index 0000000000000000000000000000000000000000..2504da6dd9507c25e06d4abcffb2f92f404912f4 --- /dev/null +++ b/app/Resources/icons/home-gear.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zM8.592 13.808l-.991.572 1 1.733.993-.573a3.5 3.5 0 0 0 1.405.811v1.145h2.002V16.35a3.5 3.5 0 0 0 1.405-.81l.992.572L16.4 14.38l-.991-.572a3.504 3.504 0 0 0 0-1.62l.991-.573-1-1.733-.993.573A3.5 3.5 0 0 0 13 9.645V8.5h-2.002v1.144a3.5 3.5 0 0 0-1.405.811l-.992-.573L7.6 11.616l.991.572a3.504 3.504 0 0 0 0 1.62zm3.408.69a1.5 1.5 0 1 1-.002-3.001 1.5 1.5 0 0 1 .002 3z"/> + </g> +</svg> diff --git a/app/Views/Components/Button.php b/app/Views/Components/Button.php index 651dcd4882d40c66e4d7b81d858eb1a24e579fb5..79c96ac5fdc7931b4f1bf08d7ad409f21dcc9a2b 100644 --- a/app/Views/Components/Button.php +++ b/app/Views/Components/Button.php @@ -46,6 +46,12 @@ class Button extends Component 'large' => 'text-base leading-6', ]; + $iconSize = [ + 'small' => 'text-sm', + 'base' => 'text-lg', + 'large' => 'text-2xl', + ]; + $basePaddings = [ 'small' => 'px-3 py-1', 'base' => 'px-3 py-2', @@ -77,14 +83,14 @@ class Button extends Component if ($this->iconLeft !== '') { $this->slot = (new Icon([ 'glyph' => $this->iconLeft, - 'class' => 'mr-2 opacity-75', + 'class' => 'mr-2 opacity-75' . ' ' . $iconSize[$this->size], ]))->render() . $this->slot; } if ($this->iconRight !== '') { $this->slot .= (new Icon([ 'glyph' => $this->iconRight, - 'class' => 'ml-2 opacity-75', + 'class' => 'ml-2 opacity-75' . ' ' . $iconSize[$this->size], ]))->render(); } diff --git a/modules/Admin/Config/Routes.php b/modules/Admin/Config/Routes.php index c22fba69984d9aa49523b919a1e587b36bd5b30c..b263686ee7aaac2ddde083f7427ffa2ed5e2403f 100644 --- a/modules/Admin/Config/Routes.php +++ b/modules/Admin/Config/Routes.php @@ -40,6 +40,10 @@ $routes->group( 'as' => 'settings-images-regenerate', 'filter' => 'permission:settings-manage', ]); + $routes->post('instance-housekeeping-run', 'SettingsController::runHousekeeping', [ + 'as' => 'settings-housekeeping-run', + 'filter' => 'permission:settings-manage', + ]); $routes->get('theme', 'SettingsController::theme', [ 'as' => 'settings-theme', 'filter' => 'permission:settings-manage', diff --git a/modules/Admin/Controllers/SettingsController.php b/modules/Admin/Controllers/SettingsController.php index 7f91371bbf2fa0cb38f38ba3c79bd77fe88bafd6..879d5d4376d19e78c577997b68c14330605fad9b 100644 --- a/modules/Admin/Controllers/SettingsController.php +++ b/modules/Admin/Controllers/SettingsController.php @@ -10,8 +10,12 @@ declare(strict_types=1); namespace Modules\Admin\Controllers; +use App\Entities\Media\Image; +use App\Models\ActorModel; +use App\Models\MediaModel; use App\Models\PersonModel; use App\Models\PodcastModel; +use CodeIgniter\Files\File; use CodeIgniter\HTTP\RedirectResponse; use PHP_ICO; @@ -156,6 +160,108 @@ class SettingsController extends BaseController return redirect('settings-general')->with('message', lang('Settings.images.regenerationSuccess')); } + public function runHousekeeping(): RedirectResponse + { + helper('media'); + + // Delete all podcast image sizes to recreate them + $allPodcasts = (new PodcastModel())->findAll(); + foreach ($allPodcasts as $podcast) { + $podcastImages = glob( + ROOTPATH . 'public/' . config('App')->mediaRoot . "/podcasts/{$podcast->handle}/*_*{jpg,png,webp}", + GLOB_BRACE + ); + + if ($podcastImages) { + foreach ($podcastImages as $podcastImage) { + if (is_file($podcastImage)) { + unlink($podcastImage); + } + } + } + } + + // Delete all person image sizes to recreate them + $personsImages = glob( + ROOTPATH . 'public/' . config('App')->mediaRoot . '/persons/*_*{jpg,png,webp}', + GLOB_BRACE + ); + if ($personsImages) { + foreach ($personsImages as $personsImage) { + if (is_file($personsImage)) { + unlink($personsImage); + } + } + } + + $allImages = (new MediaModel('image'))->getAllOfType(); + foreach ($allImages as $image) { + if (str_starts_with($image->file_path, 'podcasts')) { + if (str_ends_with($image->file_path, 'banner.jpg') || str_ends_with($image->file_path, 'banner.png')) { + $image->sizes = config('Images') + ->podcastBannerSizes; + } else { + $image->sizes = config('Images') + ->podcastCoverSizes; + } + } elseif (str_starts_with($image->file_path, 'persons')) { + $image->sizes = config('Images') + ->personAvatarSizes; + } + $image->setFile(new File(media_path($image->file_path))); + + (new MediaModel('image'))->updateMedia($image); + } + + $allAudio = (new MediaModel('audio'))->getAllOfType(); + foreach ($allAudio as $audio) { + $audio->setFile(new File(media_path($audio->file_path))); + + (new MediaModel('audio'))->updateMedia($audio); + } + + $allTranscripts = (new MediaModel('transcript'))->getAllOfType(); + foreach ($allTranscripts as $transcript) { + $transcript->setFile(new File(media_path($transcript->file_path))); + + (new MediaModel('transcript'))->updateMedia($transcript); + } + + $allChapters = (new MediaModel('chapters'))->getAllOfType(); + foreach ($allChapters as $chapters) { + $chapters->setFile(new File(media_path($chapters->file_path))); + + (new MediaModel('chapters'))->updateMedia($chapters); + } + + $allVideos = (new MediaModel('video'))->getAllOfType(); + foreach ($allVideos as $video) { + $video->setFile(new File(media_path($video->file_path))); + + (new MediaModel('video'))->updateMedia($video); + } + + // reset avatar and banner image urls for each podcast actor + foreach ($allPodcasts as $podcast) { + $actorModel = new ActorModel(); + $actor = $actorModel->getActorById($podcast->actor_id); + + if ($actor !== null) { + // update values + $actor->avatar_image_url = $podcast->cover->federation_url; + $actor->avatar_image_mimetype = $podcast->cover->file_mimetype; + $actor->cover_image_url = $podcast->banner->federation_url; + $actor->cover_image_mimetype = $podcast->banner->file_mimetype; + + if ($actor->hasChanged()) { + $actorModel->update($actor->id, $actor); + } + } + } + + return redirect('settings-general')->with('message', lang('Settings.housekeeping.runSuccess')); + } + public function theme(): string { helper('form'); diff --git a/modules/Admin/Language/en/Settings.php b/modules/Admin/Language/en/Settings.php index 48580a5e8813d5eac4c964f10a1a1dc9f088b6f7..8d0ece529ea35075c3b58dc1a0953d15d984b387 100644 --- a/modules/Admin/Language/en/Settings.php +++ b/modules/Admin/Language/en/Settings.php @@ -28,6 +28,12 @@ return [ 'regenerate' => 'Regenerate images', 'regenerationSuccess' => 'All images have been regenerated successfully!', ], + 'housekeeping' => [ + 'title' => 'Housekeeping', + 'subtitle' => 'Runs various housekeeping tasks, such as rewriting media files metadata (images, audio files, transcripts, chapters, …).', + 'run' => 'Run housekeeping', + 'runSuccess' => 'Housekeeping has been run successfully!', + ], 'theme' => [ 'title' => 'Theme', 'accent_section_title' => 'Accent color', diff --git a/modules/Admin/Language/fr/Settings.php b/modules/Admin/Language/fr/Settings.php index 6c7dece2d544b1e302c2b5c4ba1f07c08a8397eb..0d0fe8df0fe3f5e296a1d9a015b6891594ef0960 100644 --- a/modules/Admin/Language/fr/Settings.php +++ b/modules/Admin/Language/fr/Settings.php @@ -28,6 +28,12 @@ return [ 'regenerate' => 'Regénérer les images', 'regenerationSuccess' => 'Toutes les images ont été regénérés avec succès !', ], + 'housekeeping' => [ + 'title' => 'Ménage', + 'subtitle' => 'Exécute un nombre de tâches de nettoyage, comme la réécriture de métadonnées des fichiers media (images, fichiers audio, transcript, chapitres, …).', + 'run' => 'Faire le ménage', + 'runSuccess' => 'Le ménage a été effectué avec succès !', + ], 'theme' => [ 'title' => 'Thème', 'accent_section_title' => 'Couleur d’accentuation', diff --git a/themes/cp_admin/settings/general.php b/themes/cp_admin/settings/general.php index 26c8e86d9bf7df3480d179679a8e4ba1ec67cdfe..bd806aa240221269f23d291febe95b4e65d4e11d 100644 --- a/themes/cp_admin/settings/general.php +++ b/themes/cp_admin/settings/general.php @@ -69,6 +69,20 @@ </Forms.Section> </form> + +<form action="<?= route_to('settings-housekeeping-run') ?>" method="POST" class="flex flex-col max-w-xl gap-y-4"> +<?= csrf_field() ?> + +<Forms.Section + title="<?= lang('Settings.housekeeping.title') ?>" + subtitle="<?= lang('Settings.housekeeping.subtitle') ?>" > + + <Button variant="primary" type="submit" iconLeft="home-gear"><?= lang('Settings.housekeeping.run') ?></Button> + +</Forms.Section> + +</form> + </div> <?= $this->endSection() ?>