feat(media): add s3 to manage media files

Users may choose between filesystem (FS) or S3 to store and manage their media files
This commit is contained in:
Yassine Doghri 2023-03-16 13:00:05 +00:00
parent 9fc49a7430
commit d93fc98469
85 changed files with 2168 additions and 976 deletions

1
.gitignore vendored
View File

@ -177,6 +177,7 @@ modules/Admin/Language/*/PersonsTaxonomy.php
mariadb
phpmyadmin
sessions
data
# Castopod bundle & packages
castopod/

View File

@ -26,18 +26,6 @@ class App extends BaseConfig
*/
public string $baseURL = 'http://localhost:8080/';
/**
* --------------------------------------------------------------------------
* Media Base URL
* --------------------------------------------------------------------------
*
* URL to your media root. Typically this will be your base URL,
* WITH a trailing slash:
*
* http://cdn.example.com/
*/
public string $mediaBaseURL = 'http://localhost:8080/';
/**
* --------------------------------------------------------------------------
* Index File
@ -420,14 +408,6 @@ class App extends BaseConfig
*/
public bool $CSPEnabled = false;
/**
* --------------------------------------------------------------------------
* Media root folder
* --------------------------------------------------------------------------
* Defines the root folder for media files storage
*/
public string $mediaRoot = 'media';
/**
* --------------------------------------------------------------------------
* Instance / Site Config

View File

@ -50,6 +50,7 @@ class Autoload extends AutoloadConfig
'Modules\Install' => ROOTPATH . 'modules/Install/',
'Modules\Update' => ROOTPATH . 'modules/Update/',
'Modules\Fediverse' => ROOTPATH . 'modules/Fediverse/',
'Modules\Media' => ROOTPATH . 'modules/Media/',
'Modules\WebSub' => ROOTPATH . 'modules/WebSub/',
'Modules\Api\Rest\V1' => ROOTPATH . 'modules/Api/Rest/V1',
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',

View File

@ -211,7 +211,7 @@ class Mimes
'word' => ['application/msword', 'application/octet-stream'],
'xl' => 'application/excel',
'eml' => 'message/rfc822',
'json' => ['application/json', 'text/json'],
'json' => ['application/json', 'text/json', 'text/plain'],
'pem' => ['application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'],
'p10' => ['application/x-pkcs10', 'application/pkcs10'],
'p12' => 'application/x-pkcs12',

View File

@ -9,13 +9,13 @@ use CodeIgniter\Database\Seeder;
class FakeSinglePodcastApiSeeder extends Seeder
{
/**
* @return array{id: int, file_path: string, file_size: int, file_mimetype: string, file_metadata: string, type: string, description: null, language_code: null, uploaded_by: int, updated_by: int, uploaded_at: string, updated_at: string}
* @return array{id: int, file_key: string, file_size: int, file_mimetype: string, file_metadata: string, type: string, description: null, language_code: null, uploaded_by: int, updated_by: int, uploaded_at: string, updated_at: string}
*/
public static function cover(): array
{
return [
'id' => 1,
'file_path' => 'podcasts/Handle/cover.jpg',
'file_key' => 'podcasts/Handle/cover.jpg',
'file_size' => 400000,
'file_mimetype' => 'image/jpeg',
'file_metadata' => '{"FILE":{"FileName":"cover.jpg","FileDateTime":1654861723,"FileSize":468541,"FileType":2,"MimeType":"image\/jpeg","SectionsFound":"COMMENT"},"COMPUTED":{"html":"width=\"1400\" height=\"1400\"","Height":1400,"Width":1400,"IsColor":1},"COMMENT":["CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90\n"],"sizes":{"tiny":{"width":40,"height":40,"mimetype":"image\/webp","extension":"webp"},"thumbnail":{"width":150,"height":150,"mimetype":"image\/webp","extension":"webp"},"medium":{"width":320,"height":320,"mimetype":"image\/webp","extension":"webp"},"large":{"width":1024,"height":1024,"mimetype":"image\/webp","extension":"webp"},"feed":{"width":1400,"height":1400},"id3":{"width":500,"height":500},"og":{"width":1200,"height":1200},"federation":{"width":400,"height":400},"webmanifest192":{"width":192,"height":192,"mimetype":"image\/png","extension":"png"},"webmanifest512":{"width":512,"height":512,"mimetype":"image\/png","extension":"png"}}}',
@ -30,13 +30,13 @@ class FakeSinglePodcastApiSeeder extends Seeder
}
/**
* @return array{id: int, file_path: string, file_size: int, file_mimetype: string, file_metadata: string, type: string, description: null, language_code: null, uploaded_by: int, updated_by: int, uploaded_at: string, updated_at: string}
* @return array{id: int, file_key: string, file_size: int, file_mimetype: string, file_metadata: string, type: string, description: null, language_code: null, uploaded_by: int, updated_by: int, uploaded_at: string, updated_at: string}
*/
public static function banner(): array
{
return [
'id' => 2,
'file_path' => 'podcasts/Handle/banner.jpg',
'file_key' => 'podcasts/Handle/banner.jpg',
'file_size' => 400000,
'file_mimetype' => 'image/jpeg',
'file_metadata' => '{"FILE":{"FileName":"banner.jpg","FileDateTime":1654861724,"FileSize":98209,"FileType":2,"MimeType":"image\/jpeg","SectionsFound":""},"COMPUTED":{"html":"width=\"1500\" height=\"500\"","Height":500,"Width":1500,"IsColor":1},"sizes":{"small":{"width":320,"height":128,"mimetype":"image\/webp","extension":"webp"},"medium":{"width":960,"height":320,"mimetype":"image\/webp","extension":"webp"},"federation":{"width":1500,"height":500}}}',

View File

@ -11,17 +11,17 @@ declare(strict_types=1);
namespace App\Entities\Clip;
use App\Entities\Episode;
use App\Entities\Media\Audio;
use App\Entities\Media\Video;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PodcastModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Video;
use Modules\Media\Models\MediaModel;
/**
* @property int $id
@ -122,14 +122,8 @@ class BaseClip extends Entity
return (new UserModel())->find($this->created_by);
}
public function setMedia(string $filePath = null): static
public function setMedia(File $file, string $fileKey): static
{
if ($filePath === null) {
return $this;
}
$file = new File($filePath);
if ($this->media_id !== null) {
$this->getMedia()
->setFile($file);
@ -138,9 +132,9 @@ class BaseClip extends Entity
(new MediaModel('audio'))->updateMedia($this->getMedia());
} else {
$media = new Audio([
'file_path' => $filePath,
'file_key' => $fileKey,
'language_code' => $this->getPodcast()
->language_code,
->language_code,
'uploaded_by' => $this->attributes['created_by'],
'updated_by' => $this->attributes['created_by'],
]);

View File

@ -10,9 +10,9 @@ declare(strict_types=1);
namespace App\Entities\Clip;
use App\Entities\Media\Video;
use App\Models\MediaModel;
use CodeIgniter\Files\File;
use Modules\Media\Entities\Video;
use Modules\Media\Models\MediaModel;
/**
* @property array $theme
@ -63,30 +63,23 @@ class VideoClip extends BaseClip
return $this;
}
public function setMedia(string $filePath = null): static
public function setMedia(File $file, string $fileKey): static
{
if ($filePath === null) {
return $this;
}
if ($this->attributes['media_id'] !== null) {
// media is already set, do nothing
return $this;
}
helper('media');
$file = new File(media_path($filePath));
$video = new Video([
'file_path' => $filePath,
'file_key' => $fileKey,
'language_code' => $this->getPodcast()
->language_code,
->language_code,
'uploaded_by' => $this->attributes['created_by'],
'updated_by' => $this->attributes['created_by'],
]);
$video->setFile($file);
$this->attributes['media_id'] = (new MediaModel())->saveMedia($video);
$this->attributes['media_id'] = (new MediaModel('video'))->saveMedia($video);
return $this;
}

View File

@ -11,14 +11,9 @@ declare(strict_types=1);
namespace App\Entities;
use App\Entities\Clip\Soundbite;
use App\Entities\Media\Audio;
use App\Entities\Media\Chapters;
use App\Entities\Media\Image;
use App\Entities\Media\Transcript;
use App\Libraries\SimpleRSSElement;
use App\Models\ClipModel;
use App\Models\EpisodeCommentModel;
use App\Models\MediaModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
@ -32,6 +27,11 @@ use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Image;
use Modules\Media\Entities\Transcript;
use Modules\Media\Models\MediaModel;
use RuntimeException;
/**
@ -191,10 +191,9 @@ class Episode extends Entity
(new MediaModel('image'))->updateMedia($this->getCover());
} else {
$cover = new Image([
'file_name' => $this->attributes['slug'],
'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
'sizes' => config('Images')
->podcastCoverSizes,
->podcastCoverSizes,
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
@ -238,8 +237,10 @@ class Episode extends Entity
(new MediaModel('audio'))->updateMedia($this->getAudio());
} else {
$audio = new Audio([
'file_name' => pathinfo($file->getRandomName(), PATHINFO_FILENAME),
'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . pathinfo(
$file->getRandomName(),
PATHINFO_FILENAME
) . '.' . $file->getExtension(),
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => user_id(),
@ -276,10 +277,9 @@ class Episode extends Entity
(new MediaModel('transcript'))->updateMedia($this->getTranscript());
} else {
$transcript = new Transcript([
'file_name' => $this->attributes['slug'] . '-transcript',
'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
'language_code' => $this->getPodcast()
->language_code,
->language_code,
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
@ -314,10 +314,9 @@ class Episode extends Entity
(new MediaModel('chapters'))->updateMedia($this->getChapters());
} else {
$chapters = new Chapters([
'file_name' => $this->attributes['slug'] . '-chapters',
'file_directory' => 'podcasts/' . $this->getPodcast()->handle,
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
'language_code' => $this->getPodcast()
->language_code,
->language_code,
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);

View File

@ -1,106 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Media;
use CodeIgniter\Files\File;
/**
* @property array $sizes
*/
class Image extends BaseMedia
{
protected string $type = 'image';
public function initFileProperties(): void
{
parent::initFileProperties();
if ($this->file_path && $this->file_metadata) {
$this->sizes = $this->file_metadata['sizes'];
$this->initSizeProperties();
}
}
public function initSizeProperties(): bool
{
helper('media');
foreach ($this->sizes as $name => $size) {
$extension = array_key_exists('extension', $size) ? $size['extension'] : $this->file_extension;
$mimetype = array_key_exists('mimetype', $size) ? $size['mimetype'] : $this->file_mimetype;
$this->{$name . '_path'} = $this->file_directory . '/' . $this->file_name . '_' . $name . '.' . $extension;
$this->{$name . '_url'} = media_base_url($this->{$name . '_path'});
$this->{$name . '_mimetype'} = $mimetype;
}
return true;
}
public function setFile(File $file): self
{
parent::setFile($file);
if ($this->file_mimetype === 'image/jpeg' && $metadata = @exif_read_data(
media_path($this->file_path),
null,
true
)) {
$metadata['sizes'] = $this->sizes;
$this->attributes['file_size'] = $metadata['FILE']['FileSize'];
} else {
$metadata = [
'sizes' => $this->sizes,
];
}
$this->attributes['file_metadata'] = json_encode($metadata, JSON_INVALID_UTF8_IGNORE);
$this->initFileProperties();
$this->saveSizes();
return $this;
}
public function deleteFile(): bool
{
if (parent::deleteFile()) {
return $this->deleteSizes();
}
return false;
}
public function saveSizes(): void
{
// save derived sizes
$imageService = service('image');
foreach ($this->sizes as $name => $size) {
$pathProperty = $name . '_path';
$imageService
->withFile(media_path($this->file_path))
->resize($size['width'], $size['height']);
$imageService->save(media_path($this->{$pathProperty}));
}
}
private function deleteSizes(): bool
{
// delete all derived sizes
foreach (array_keys($this->sizes) as $name) {
$pathProperty = $name . '_path';
if (! unlink(media_path($this->{$pathProperty}))) {
return false;
}
}
return true;
}
}

View File

@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Media;
use App\Libraries\TranscriptParser;
use CodeIgniter\Files\File;
class Transcript extends BaseMedia
{
public ?string $json_path = null;
public ?string $json_url = null;
protected string $type = 'transcript';
public function initFileProperties(): void
{
parent::initFileProperties();
if ($this->file_path && $this->file_metadata && array_key_exists('json_path', $this->file_metadata)) {
helper('media');
$this->json_path = media_path($this->file_metadata['json_path']);
$this->json_url = media_base_url($this->file_metadata['json_path']);
}
}
public function setFile(File $file): self
{
parent::setFile($file);
$content = file_get_contents(media_path($this->attributes['file_path']));
if ($content === false) {
return $this;
}
$metadata = [];
if ($fileMetadata = lstat((string) $file)) {
$metadata = $fileMetadata;
}
$transcriptParser = new TranscriptParser();
$jsonFilePath = $this->attributes['file_directory'] . '/' . $this->attributes['file_name'] . '.json';
if (($transcriptJson = $transcriptParser->loadString($content)->parseSrt()) && file_put_contents(
media_path($jsonFilePath),
$transcriptJson
)) {
// set metadata (generated json file path)
$metadata['json_path'] = $jsonFilePath;
}
$this->attributes['file_metadata'] = json_encode($metadata, JSON_INVALID_UTF8_IGNORE);
return $this;
}
public function deleteFile(): bool
{
if (! parent::deleteFile()) {
return false;
}
if ($this->json_path) {
return unlink($this->json_path);
}
return true;
}
}

View File

@ -10,12 +10,12 @@ declare(strict_types=1);
namespace App\Entities;
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 Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use RuntimeException;
/**
@ -70,10 +70,9 @@ class Person extends Entity
(new MediaModel('image'))->updateMedia($this->getAvatar());
} else {
$avatar = new Image([
'file_name' => $this->attributes['unique_name'],
'file_directory' => 'persons',
'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
'sizes' => config('Images')
->personAvatarSizes,
->personAvatarSizes,
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
@ -90,7 +89,7 @@ class Person extends Entity
if ($this->attributes['avatar_id'] === null) {
helper('media');
return new Image([
'file_path' => config('Images')
'file_key' => config('Images')
->avatarDefaultPath,
'file_mimetype' => config('Images')
->avatarDefaultMimeType,
@ -99,7 +98,7 @@ class Person extends Entity
'sizes' => config('Images')
->personAvatarSizes,
],
]);
], 'fs');
}
if ($this->avatar === null) {

View File

@ -10,12 +10,10 @@ declare(strict_types=1);
namespace App\Entities;
use App\Entities\Media\Image;
use App\Libraries\SimpleRSSElement;
use App\Models\ActorModel;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PersonModel;
use App\Models\PlatformModel;
use CodeIgniter\Entity\Entity;
@ -30,6 +28,8 @@ use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException;
@ -49,7 +49,7 @@ use RuntimeException;
* @property int $cover_id
* @property Image $cover
* @property int|null $banner_id
* @property Image|null $banner
* @property Image $banner
* @property string $language_code
* @property int $category_id
* @property Category|null $category
@ -243,10 +243,9 @@ class Podcast extends Entity
(new MediaModel('image'))->updateMedia($this->getCover());
} else {
$cover = new Image([
'file_name' => 'cover',
'file_directory' => 'podcasts/' . $this->attributes['handle'],
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
'sizes' => config('Images')
->podcastCoverSizes,
->podcastCoverSizes,
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
@ -281,10 +280,9 @@ class Podcast extends Entity
(new MediaModel('image'))->updateMedia($this->getBanner());
} else {
$banner = new Image([
'file_name' => 'banner',
'file_directory' => 'podcasts/' . $this->attributes['handle'],
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
'sizes' => config('Images')
->podcastBannerSizes,
->podcastBannerSizes,
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
@ -304,14 +302,14 @@ class Podcast extends Entity
'Images'
)->podcastBannerDefaultPaths['default'];
return new Image([
'file_path' => $defaultBanner['path'],
'file_key' => $defaultBanner['path'],
'file_mimetype' => $defaultBanner['mimetype'],
'file_size' => 0,
'file_metadata' => [
'sizes' => config('Images')
->podcastBannerSizes,
->podcastBannerSizes,
],
]);
], 'fs');
}
if (! $this->banner instanceof Image) {

View File

@ -10,6 +10,7 @@ declare(strict_types=1);
use App\Entities\Episode;
use JamesHeinrich\GetID3\WriteTags;
use Modules\Media\FileManagers\FileManagerInterface;
if (! function_exists('write_audio_file_tags')) {
/**
@ -23,13 +24,16 @@ if (! function_exists('write_audio_file_tags')) {
// Initialize getID3 tag-writing module
$tagwriter = new WriteTags();
$tagwriter->filename = media_path($episode->audio->file_path);
$tagwriter->filename = $episode->audio->file_name;
// set various options (optional)
$tagwriter->tagformats = ['id3v2.4'];
$tagwriter->tag_encoding = $TextEncoding;
$APICdata = file_get_contents(media_path($episode->cover->id3_path));
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$APICdata = $fileManager->getFileContents($episode->cover->id3_key);
// TODO: variables used for podcast specific tags
// $podcastUrl = $episode->podcast->link;

View File

@ -13,6 +13,7 @@ namespace MediaClipper;
use App\Entities\Episode;
use Exception;
use GdImage;
use Modules\Media\FileManagers\FileManagerInterface;
/**
* TODO: refactor this by splitting process modules into different classes (image generation, subtitles clip, video
@ -35,9 +36,9 @@ class VideoClipper
public bool $error = false;
public string $videoClipFilePath;
public string $videoClipFileKey;
protected string $videoClipOutput;
public string $videoClipOutput;
protected float $duration;
@ -83,15 +84,11 @@ class VideoClipper
$this->colors = config('MediaClipper')
->themes[$theme];
helper(['media']);
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$this->audioInput = media_path($this->episode->audio->file_path);
$this->episodeCoverPath = media_path($this->episode->cover->file_path);
$podcastFolder = media_path("podcasts/{$this->episode->podcast->handle}");
$this->videoClipOutput = $podcastFolder . "/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4";
$this->videoClipFilePath = "podcasts/{$this->episode->podcast->handle}/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4";
$this->audioInput = $fileManager->getFileInput($this->episode->audio->file_key);
$this->episodeCoverPath = $fileManager->getFileInput($this->episode->cover->file_key);
// Temporary files to generate clip
$tempFile = tempnam(WRITEPATH . 'temp', "{$this->episode->slug}-{$this->start}-{$this->end}");
@ -102,7 +99,10 @@ class VideoClipper
);
}
$this->videoClipFileKey = "podcasts/{$this->episode->podcast->handle}/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4";
$this->tempFileOutput = $tempFile;
$this->videoClipOutput = $tempFile . '-video-clip.mp4';
$this->soundbiteOutput = $tempFile . '-soundbite.mp3';
$this->subtitlesClipOutput = $tempFile . '-subtitle.srt';
$this->videoClipBgOutput = $tempFile . '-bg.png';
@ -120,19 +120,22 @@ class VideoClipper
throw new Exception('Episode does not have a transcript!');
}
if ($this->episode->transcript->json_path) {
$this->generateSubtitlesClipFromJson($this->episode->transcript->json_path);
if ($this->episode->transcript->json_url) {
$this->generateSubtitlesClipFromJson($this->episode->transcript->json_key);
} else {
$subtitlesInput = media_path($this->episode->transcript->file_path);
$subtitlesInput = $this->episode->transcript->file_url;
$subtitleClipCmd = "ffmpeg -y -i {$subtitlesInput} -ss {$this->start} -t {$this->duration} {$this->subtitlesClipOutput}";
exec($subtitleClipCmd);
}
}
public function generateSubtitlesClipFromJson(string $jsonFileInput): void
public function generateSubtitlesClipFromJson(string $jsonFileKey): void
{
$jsonTranscriptString = file_get_contents($jsonFileInput);
if ($jsonTranscriptString === false) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$jsonTranscriptString = $fileManager->getFileContents($jsonFileKey);
if ($jsonTranscriptString === '') {
throw new Exception('Cannot get transcript json contents.');
}

View File

@ -134,17 +134,17 @@ class ComponentRenderer
foreach ($pathsToDiscover as $basePath) {
// Look for a class component first
$filePath = $basePath . $this->config->componentsDirectory . '/' . $namePath . '.php';
$fileKey = $basePath . $this->config->componentsDirectory . '/' . $namePath . '.php';
if (is_file($filePath)) {
return $filePath;
if (is_file($fileKey)) {
return $fileKey;
}
$snakeCaseName = strtolower(preg_replace('~(?<!^)(?<!\/)[A-Z]~', '_$0', $namePath) ?? '');
$filePath = $basePath . $this->config->componentsDirectory . '/' . $snakeCaseName . '.php';
$fileKey = $basePath . $this->config->componentsDirectory . '/' . $snakeCaseName . '.php';
if (is_file($filePath)) {
return $filePath;
if (is_file($fileKey)) {
return $fileKey;
}
}
@ -204,18 +204,18 @@ class ComponentRenderer
{
// Locate the class in the same folder as the view
$class = $name . '.php';
$filePath = str_replace($name . '.php', $class, $view);
$fileKey = str_replace($name . '.php', $class, $view);
if ($filePath === '') {
if ($fileKey === '') {
return null;
}
if (! file_exists($filePath)) {
if (! file_exists($fileKey)) {
return null;
}
$className = service('locator')
->getClassname($filePath);
->getClassname($fileKey);
if (! class_exists($className)) {
return null;

View File

@ -440,8 +440,8 @@ class PodcastModel extends Model
$podcastActor = (new ActorModel())->find($podcast->actor_id);
if ($podcastActor) {
$podcastActor->avatar_image_url = $podcast->cover->thumbnail_url;
$podcastActor->avatar_image_mimetype = $podcast->cover->thumbnail_mimetype;
$podcastActor->avatar_image_url = $podcast->cover->federation_url;
$podcastActor->avatar_image_mimetype = $podcast->cover->federation_mimetype;
(new ActorModel())->update($podcast->actor_id, $podcastActor);
}
@ -468,9 +468,9 @@ class PodcastModel extends Model
$actor->display_name = $podcast->title;
$actor->summary = $podcast->description_html;
$actor->avatar_image_url = $podcast->cover->federation_url;
$actor->avatar_image_mimetype = $podcast->cover->file_mimetype;
$actor->avatar_image_mimetype = $podcast->cover->federation_mimetype;
$actor->cover_image_url = $podcast->banner->federation_url;
$actor->cover_image_mimetype = $podcast->banner->file_mimetype;
$actor->cover_image_mimetype = $podcast->banner->federation_mimetype;
if ($actor->hasChanged()) {
$actorModel->update($actor->id, $actor);

View File

@ -27,9 +27,9 @@ if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
$c = str_pad((string) ($i + 1), 3, ' ', STR_PAD_LEFT);
if (isset($error['file'])) {
$filepath = clean_path($error['file']) . ':' . $error['line'];
$fileKey = clean_path($error['file']) . ':' . $error['line'];
CLI::write($c . $padFile . CLI::color($filepath, 'yellow'));
CLI::write($c . $padFile . CLI::color($fileKey, 'yellow'));
} else {
CLI::write($c . $padFile . CLI::color('[internal function]', 'yellow'));
}

View File

@ -17,21 +17,22 @@
"opawg/user-agents-php": "^v1.0",
"adaures/ipcat-php": "^v1.0.0",
"adaures/podcast-persons-taxonomy": "^v1.0.0",
"phpseclib/phpseclib": "~2.0.41",
"phpseclib/phpseclib": "~2.0.42",
"michalsn/codeigniter4-uuid": "dev-develop",
"essence/essence": "^3.5.4",
"codeigniter4/settings": "^v2.1.0",
"chrisjean/php-ico": "^1.0.4",
"melbahja/seo": "^v2.1.1",
"codeigniter4/shield": "v1.0.0-beta.3"
"codeigniter4/shield": "v1.0.0-beta.3",
"aws/aws-sdk-php": "^3.261.10"
},
"require-dev": {
"mikey179/vfsstream": "^v1.6.11",
"phpunit/phpunit": "^10.0.11",
"captainhook/captainhook": "^5.14.4",
"symplify/easy-coding-standard": "^11.2.9",
"phpstan/phpstan": "^1.10.0",
"rector/rector": "^0.15.17",
"phpunit/phpunit": "^10.0.16",
"captainhook/captainhook": "^5.15.2",
"symplify/easy-coding-standard": "^11.2.10",
"phpstan/phpstan": "^1.10.6",
"rector/rector": "^0.15.21",
"symplify/coding-standard": "^11.3.0"
},
"autoload": {

1010
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,10 @@
version: "3"
version: "3.8"
networks:
castopod:
ipam:
config:
- subnet: 172.20.0.0/24
services:
app:
@ -18,7 +21,8 @@ services:
- redis
- mariadb
networks:
- castopod
castopod:
ipv4_address: 172.20.0.2
redis:
image: redis:alpine
@ -28,7 +32,8 @@ services:
volumes:
- redis:/data
networks:
- castopod
castopod:
ipv4_address: 172.20.0.3
mariadb:
image: mariadb:10.2
@ -44,7 +49,8 @@ services:
MYSQL_USER: castopod
MYSQL_PASSWORD: castopod
networks:
- castopod
castopod:
ipv4_address: 172.20.0.4
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
@ -59,7 +65,22 @@ services:
depends_on:
- mariadb
networks:
- castopod
castopod:
ipv4_address: 172.20.0.5
s3:
image: adobe/s3mock:latest
container_name: castopod_s3
environment:
- debug=true
- root=/data
ports:
- 9090:9090
volumes:
- ./data/s3:/data:cached
networks:
castopod:
ipv4_address: 172.20.0.6
volumes:
redis:

View File

@ -43,7 +43,6 @@ to help you kickstart your contribution.
app.forceGlobalSecureRequests=false
app.baseURL="http://localhost:8080/"
app.mediaBaseURL="http://localhost:8080/"
admin.gateway="cp-admin"
auth.gateway="cp-auth"
@ -62,7 +61,21 @@ to help you kickstart your contribution.
# You may not want to use redis as your cache handler
# Comment/remove the two lines above and uncomment
# the next line for file caching.
# -----------------------
#cache.handler="file"
######################################
# Media config
######################################
media.baseURL="http://localhost:8080/"
# S3
# Uncomment to store s3 objects using adobe/s3mock service
# -----------------------
#media.fileManager="s3"
#media.s3.bucket="castopod"
#media.s3.endpoint="http://172.20.0.6:9090/"
#media.s3.path_style_endpoint=true
```
> _NB._ You can tweak your environment by setting more environment variables

View File

@ -11,9 +11,9 @@ declare(strict_types=1);
namespace Modules\Admin\Controllers;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PodcastModel;
use CodeIgniter\I18n\Time;
use Modules\Media\Models\MediaModel;
class DashboardController extends BaseController
{

View File

@ -17,12 +17,12 @@ use App\Entities\Podcast;
use App\Entities\Post;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
use Modules\Media\Models\MediaModel;
class EpisodeController extends BaseController
{
@ -933,7 +933,7 @@ class EpisodeController extends BaseController
if ($episodeMedia !== null && ! $episodeMedia->deleteFile()) {
$warnings[] = lang('Episode.messages.deleteFileError', [
'type' => $episodeMedia->type,
'file_path' => $episodeMedia->file_path,
'file_key' => $episodeMedia->file_key,
]);
}
}

View File

@ -17,7 +17,6 @@ use App\Models\ActorModel;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\LanguageModel;
use App\Models\MediaModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
@ -32,6 +31,7 @@ use Modules\Analytics\Models\AnalyticsPodcastModel;
use Modules\Analytics\Models\AnalyticsWebsiteByBrowserModel;
use Modules\Analytics\Models\AnalyticsWebsiteByEntryPageModel;
use Modules\Analytics\Models\AnalyticsWebsiteByRefererModel;
use Modules\Media\Models\MediaModel;
class PodcastController extends BaseController
{

View File

@ -306,6 +306,7 @@ class PodcastImportController extends BaseController
$slugs = [];
for ($itemNumber = 1; $itemNumber <= $lastItem; ++$itemNumber) {
$item = $feed->channel[0]->item[$itemsCount - $itemNumber];
log_message('critical', (string) $item->title);
$nsItunes = $item->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
$nsPodcast = $item->children(

View File

@ -12,6 +12,7 @@ namespace Modules\Admin\Controllers;
use App\Models\ClipModel;
use CodeIgniter\Controller;
use CodeIgniter\Files\File;
use CodeIgniter\I18n\Time;
use Exception;
use MediaClipper\VideoClipper;
@ -65,7 +66,7 @@ class SchedulerController extends Controller
$clipModel = new ClipModel();
if ($exitCode === 0) {
// success, video was generated
$scheduledClip->setMedia($clipper->videoClipFilePath);
$scheduledClip->setMedia(new File($clipper->videoClipOutput), $clipper->videoClipFileKey);
$clipModel->update($scheduledClip->id, [
'media_id' => $scheduledClip->media_id,
'status' => 'passed',

View File

@ -10,15 +10,18 @@ declare(strict_types=1);
namespace Modules\Admin\Controllers;
use App\Entities\Podcast;
use App\Models\ActorModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\RedirectResponse;
use Modules\Media\Entities\Audio;
use Modules\Media\FileManagers\FileManagerInterface;
use Modules\Media\FileManagers\FS;
use Modules\Media\Models\MediaModel;
use PHP_ICO;
class SettingsController extends BaseController
@ -61,7 +64,10 @@ class SettingsController extends BaseController
delete_files(media_path('/site'));
// save original in disk
$originalFilename = save_media($siteIconFile, 'site', 'icon');
$originalFilename = (new FS(config('Media')))->save(
$siteIconFile,
'site/icon.' . $siteIconFile->getExtension()
);
// convert jpeg image to png if not
if ($siteIconFile->getClientMimeType() !== 'image/png') {
@ -113,23 +119,14 @@ class SettingsController extends BaseController
public function regenerateImages(): RedirectResponse
{
helper('media');
/** @var Podcast[] $allPodcasts */
$allPodcasts = (new PodcastModel())->findAll();
$imageExt = ['jpg', 'png', 'webp'];
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
foreach ($allPodcasts as $podcast) {
foreach ($imageExt as $ext) {
$podcastImages = glob(media_path("/podcasts/{$podcast->handle}/*_*{$ext}"));
if ($podcastImages) {
foreach ($podcastImages as $podcastImage) {
if (is_file($podcastImage)) {
unlink($podcastImage);
}
}
}
}
$fileManager->deletePodcastImageSizes($podcast->handle);
$podcast->cover->saveSizes();
if ($podcast->banner_id !== null) {
@ -143,16 +140,7 @@ class SettingsController extends BaseController
}
}
foreach ($imageExt as $ext) {
$personsImages = glob(media_path("/persons/*_*{$ext}"));
if ($personsImages) {
foreach ($personsImages as $personsImage) {
if (is_file($personsImage)) {
unlink($personsImage);
}
}
}
}
$fileManager->deletePersonImagesSizes();
$persons = (new PersonModel())->findAll();
foreach ($persons as $person) {
@ -180,115 +168,12 @@ class SettingsController extends BaseController
(new EpisodeCommentModel())->resetRepliesCount();
}
helper('media');
if ($this->request->getPost('rewrite_media') === 'yes') {
$imageExt = ['jpg', 'png', 'webp'];
// Delete all podcast image sizes to recreate them
$allPodcasts = (new PodcastModel())->findAll();
foreach ($allPodcasts as $podcast) {
foreach ($imageExt as $ext) {
$podcastImages = glob(media_path("/podcasts/{$podcast->handle}/*_*{$ext}"));
if ($podcastImages) {
foreach ($podcastImages as $podcastImage) {
if (is_file($podcastImage)) {
unlink($podcastImage);
}
}
}
}
}
// Delete all person image sizes to recreate them
foreach ($imageExt as $ext) {
$personsImages = glob(media_path("/persons/*_*{$ext}"));
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((string) $image->file_path, 'podcasts')) {
if (str_ends_with((string) $image->file_path, 'banner.jpg') || str_ends_with(
(string) $image->file_path,
'banner.png'
) || str_ends_with((string) $image->file_path, 'banner.jpeg')) {
$image->sizes = config('Images')
->podcastBannerSizes;
} else {
$image->sizes = config('Images')
->podcastCoverSizes;
}
} elseif (str_starts_with((string) $image->file_path, 'persons')) {
$image->sizes = config('Images')
->personAvatarSizes;
} else {
$image->sizes = [];
}
$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);
}
}
}
}
if ($this->request->getPost('clear_cache') === 'yes') {
cache()->clean();
}
if ($this->request->getPost('rename_episodes_files') === 'yes') {
/** @var Audio[] $allAudio */
$allAudio = (new MediaModel('audio'))->getAllOfType();
foreach ($allAudio as $audio) {

View File

@ -15,10 +15,10 @@ use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\ClipModel;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Modules\Media\Models\MediaModel;
class SoundbiteController extends BaseController
{

View File

@ -15,10 +15,10 @@ use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\ClipModel;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Modules\Media\Models\MediaModel;
class VideoClipsController extends BaseController
{

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -87,7 +87,7 @@ return [
image {ar golo}
audio {an aodio}
other {ar media}
} ({file_path}). Gallout a rit lemel kuit ar restr-mañ diouzh ar gantenn dre zorn.',
} ({file_key}). Gallout a rit lemel kuit ar restr-mañ diouzh ar gantenn dre zorn.',
'sameSlugError' => 'Bez ez eus eus ur rann gant ar berradur-mañ (slug) dija.',
],
'form' => [

View File

@ -79,7 +79,7 @@ return [
audio {l\'àudio}
other {el material}
} de l\'episodi.',
'deleteFileError' => 'No s\'ha pogut esborrar el fitxer {file_path} {type, select,
'deleteFileError' => 'No s\'ha pogut esborrar el fitxer {file_key} {type, select,
transcript {de la transcripció}
chapters {dels episodis}
image {de la portada}

View File

@ -85,7 +85,7 @@ return [
image {Cover}
audio {Audio}
other {Medien}
}-Datei {file_path}. Sie können es manuell von der Festplatte entfernen.',
}-Datei {file_key}. Sie können es manuell von der Festplatte entfernen.',
'sameSlugError' => 'Eine Folge mit dem ausgewählten Slug existiert bereits.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {καλύψτε}
audio {ήχος}
other {πολυμέσα}
} αρχείο {file_path}. Μπορείτε να το αφαιρέσετε χειροκίνητα από το δίσκο σας.',
} αρχείο {file_key}. Μπορείτε να το αφαιρέσετε χειροκίνητα από το δίσκο σας.',
'sameSlugError' => 'Ένα επεισόδιο με το επιλεγμένο slug υπάρχει ήδη.',
],
'form' => [

View File

@ -86,7 +86,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -79,7 +79,7 @@ return [
audio {audio}
other {media}
}.',
'deleteFileError' => 'Hubo un problema al tratar de eliminar el archivo {file_path} {type, select,
'deleteFileError' => 'Hubo un problema al tratar de eliminar el archivo {file_key} {type, select,
transcript {de la transcripción}
chapters {de los episodios}
image {de la portada}

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {couverture}
audio {audio}
other {média}
} fichier {file_path}. Vous pouvez le supprimer manuellement de votre disque.',
} fichier {file_key}. Vous pouvez le supprimer manuellement de votre disque.',
'sameSlugError' => 'Il existe déjà un épisode avec le slug choisi.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {da imaxe}
audio {do audio}
other {do multimedia}
} {file_path}. Deberías eliminala manualmente do disco.',
} {file_key}. Deberías eliminala manualmente do disco.',
'sameSlugError' => 'Xa existe un episodio co id de url elexido.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -86,7 +86,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {capa}
audio {áudio}
other {mídia}
} {file_path}. Você pode removê-lo manualmente do seu disco.',
} {file_key}. Você pode removê-lo manualmente do seu disco.',
'sameSlugError' => 'Um episódio com o slug escolhido já existe.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -89,7 +89,7 @@ return [
image {obrázok}
audio {zvuk}
other {médiá}
} súbor {file_path}. Môžete ho z disku odstrániť ručne.',
} súbor {file_key}. Môžete ho z disku odstrániť ručne.',
'sameSlugError' => 'Epizóda s takýmto trvalým odkazom už existuje.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {cover}
audio {audio}
other {media}
} file {file_path}. You may manually remove it from your disk.',
} file {file_key}. You may manually remove it from your disk.',
'sameSlugError' => 'An episode with the chosen slug already exists.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {omslag}
audio {ljud}
other {media}
} fil {file_path}. Du kan manuellt ta bort den från disken.',
} fil {file_key}. Du kan manuellt ta bort den från disken.',
'sameSlugError' => 'Ett avsnitt med den valda slug finns redan.',
],
'form' => [

View File

@ -85,7 +85,7 @@ return [
image {封面}
audio {音频}
other {媒体}
} 文件 {file_path}。您可以手动将其从磁盘删除。',
} 文件 {file_key}。您可以手动将其从磁盘删除。',
'sameSlugError' => '选中的剧集已存在。',
],
'form' => [

View File

@ -59,9 +59,7 @@ class Analytics extends BaseConfig
*/
public function getAudioUrl(Episode $episode, array $params): string
{
helper(['media', 'setting']);
$audioFileURI = new URI(media_base_url($episode->audio->file_path));
$audioFileURI = new URI(service('file_manager')->getUrl($episode->audio->file_key));
$audioFileURI->setQueryArray($params);
// Wrap episode url with OP3 if episode is public and OP3 is enabled on this podcast

View File

@ -13,9 +13,9 @@ declare(strict_types=1);
namespace Modules\Analytics\Models;
use App\Entities\Media\BaseMedia;
use App\Models\MediaModel;
use CodeIgniter\Model;
use Modules\Analytics\Entities\AnalyticsPodcasts;
use Modules\Media\Models\MediaModel;
class AnalyticsPodcastModel extends Model
{

View File

@ -171,11 +171,11 @@ class RolesDoc extends BaseCommand
return $newFileContents;
}
private function detectLocaleFromPath($filePath): string
private function detectLocaleFromPath($fileKey): string
{
preg_match(
'~docs\/src\/(?:([a-z]{2}(?:-[A-Za-z]{2,})?)\/)getting-started\/auth\.md~',
(string) $filePath,
(string) $fileKey,
$match
);

View File

@ -362,7 +362,7 @@ if (! function_exists('linkify')) {
$text = match ($protocol) {
'http', 'https' => preg_replace_callback(
'~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i',
static function (array $match) use ($protocol, &$links) {
static function (array $match) use ($protocol, &$links): string {
if ($match[1] !== '' && $match[1] !== '0') {
$protocol = $match[1];
}
@ -446,7 +446,7 @@ if (! function_exists('linkify')) {
'~' .
preg_quote($protocol, '~') .
'://([^\s<]+?)(?<![\.,:])~i',
static function (array $match) use ($protocol, &$links) {
static function (array $match) use ($protocol, &$links): string {
return '<' .
array_push(
$links,

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Modules\Media\Config;
use CodeIgniter\Config\BaseConfig;
use Modules\Media\FileManagers\FS;
use Modules\Media\FileManagers\S3;
class Media extends BaseConfig
{
public string $fileManager = 'fs';
/**
* @var array<string, string>
*/
public array $fileManagers = [
'fs' => FS::class,
's3' => S3::class,
];
/**
* @var array<string, null|string|bool>
*/
public array $s3 = [
'bucket' => 'castopod',
'key' => '',
'secret' => '',
'region' => '',
'protocol' => '',
'endpoint' => '',
'debug' => false,
'path_style_endpoint' => false,
];
/**
* --------------------------------------------------------------------------
* Media Base URL
* --------------------------------------------------------------------------
*
* URL to your media root. Typically this will be your base URL,
* WITH a trailing slash:
*
* http://cdn.example.com/
*/
public string $baseURL = 'http://localhost:8080/';
/**
* --------------------------------------------------------------------------
* Media root folder
* --------------------------------------------------------------------------
* Defines the root folder for media files storage
*/
public string $root = 'media';
/**
* @var array<string, string>
*/
public array $folders = [
'podcasts' => 'podcasts',
'persons' => 'persons',
];
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Modules\Media\Config;
use CodeIgniter\Config\BaseService;
use Exception;
use Modules\Media\Config\Media as MediaConfig;
use Modules\Media\FileManagers\FileManagerInterface;
/**
* Services Configuration file.
*
* Services are simply other classes/libraries that the system uses to do its job. This is used by CodeIgniter to allow
* the core of the framework to be swapped out easily without affecting the usage within the rest of your application.
*
* This file holds any application-specific services, or service overrides that you might need. An example has been
* included with the general method format you should use for your service methods. For more examples, see the core
* Services file at system/Config/Services.php.
*/
class Services extends BaseService
{
public static function file_manager(bool $getShared = true): FileManagerInterface
{
if ($getShared) {
return self::getSharedInstance('file_manager');
}
/** @var MediaConfig $config * */
$config = config('Media');
$fileManagerClass = $config->fileManagers[$config->fileManager];
$fileManager = new $fileManagerClass($config);
if ($fileManager instanceof FileManagerInterface) {
return $fileManager;
}
throw new Exception('File Manager service must extend FileManagerInterface');
}
}

View File

@ -8,7 +8,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
namespace Media\Database\Migrations;
use CodeIgniter\Database\Migration;
@ -22,7 +22,7 @@ class AddMedia extends Migration
'unsigned' => true,
'auto_increment' => true,
],
'file_path' => [
'file_key' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
@ -72,7 +72,7 @@ class AddMedia extends Migration
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('file_path');
$this->forge->addUniqueKey('file_key');
$this->forge->addForeignKey('uploaded_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('media');

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Media\Database\Migrations;
use CodeIgniter\Database\Migration;
class RenameMediafileKey extends Migration
{
public function up(): void
{
$fields = [
'file_key' => [
'name' => 'file_key',
'type' => 'VARCHAR',
'constraint' => 255,
],
];
$this->forge->modifyColumn('media', $fields);
}
public function down(): void
{
$fields = [
'file_key' => [
'name' => 'file_key',
'type' => 'VARCHAR',
'constraint' => 255,
],
];
$this->forge->modifyColumn('media', $fields);
}
}

View File

@ -8,7 +8,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities\Media;
namespace Modules\Media\Entities;
use CodeIgniter\Files\File;
use JamesHeinrich\GetID3\GetID3;
@ -39,7 +39,7 @@ class Audio extends BaseMedia
parent::setFile($file);
$getID3 = new GetID3();
$audioMetadata = $getID3->analyze(media_path($this->file_path));
$audioMetadata = $getID3->analyze($file->getRealPath());
// remove heavy image data from metadata
unset($audioMetadata['comments']['picture']);

View File

@ -8,20 +8,22 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities\Media;
namespace Modules\Media\Entities;
use App\Models\MediaModel;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use Modules\Media\FileManagers\FileManagerInterface;
use Modules\Media\FileManagers\FS;
use Modules\Media\FileManagers\S3;
use Modules\Media\Models\MediaModel;
/**
* @property int $id
* @property string $file_path
* @property string $file_key
* @property string $file_url
* @property string $file_name
* @property string $file_directory
* @property string $file_extension
* @property string $file_name
* @property int $file_size
* @property string $file_mimetype
* @property array|null $file_metadata
@ -45,8 +47,7 @@ class BaseMedia extends Entity
*/
protected $casts = [
'id' => 'integer',
'file_extension' => 'string',
'file_path' => 'string',
'file_key' => 'string',
'file_size' => 'int',
'file_mimetype' => 'string',
'file_metadata' => '?json-array',
@ -57,27 +58,41 @@ class BaseMedia extends Entity
'updated_by' => 'integer',
];
protected FileManagerInterface $fileManager;
/**
* @param array<string, mixed>|null $data
* @param 'fs'|'s3'|null $fileManager
*/
public function __construct(array $data = null)
public function __construct(array $data = null, string $fileManager = null)
{
parent::__construct($data);
if ($fileManager !== null) {
$this->fileManager = match ($fileManager) {
'fs' => new FS(config('Media')),
's3' => new S3(config('Media'))
};
} else {
/** @var FileManagerInterface $fileManagerService */
$fileManagerService = service('file_manager');
$this->fileManager = $fileManagerService;
}
$this->initFileProperties();
}
public function initFileProperties(): void
{
if ($this->file_path !== '') {
helper('media');
if ($this->file_key !== '') {
[
'filename' => $filename,
'dirname' => $dirname,
'extension' => $extension,
] = pathinfo($this->file_path);
] = pathinfo($this->file_key);
$this->attributes['file_url'] = media_base_url($this->file_path);
$this->attributes['file_url'] = $this->fileManager->getUrl($this->file_key);
$this->attributes['file_name'] = $filename;
$this->attributes['file_directory'] = $dirname;
$this->attributes['file_extension'] = $extension;
@ -86,49 +101,49 @@ class BaseMedia extends Entity
public function setFile(File $file): self
{
helper('media');
$this->attributes['type'] = $this->type;
$this->attributes['file_mimetype'] = $file->getMimeType();
$this->attributes['file_metadata'] = json_encode(lstat((string) $file), JSON_INVALID_UTF8_IGNORE);
$this->attributes['file_path'] = save_media(
$file,
$this->attributes['file_directory'],
$this->attributes['file_name']
);
if ($filesize = filesize(media_path($this->file_path))) {
if ($filesize = $file->getSize()) {
$this->attributes['file_size'] = $filesize;
}
$this->attributes['file'] = $file;
return $this;
}
public function deleteFile(): bool
public function saveFile(): bool
{
helper('media');
return unlink(media_path($this->file_path));
if (! $this->attributes['file'] || ! $this->file_key) {
return false;
}
$this->attributes['file_key'] = $this->fileManager->save($this->attributes['file'], $this->file_key);
return true;
}
public function delete(): bool|BaseResult
public function deleteFile(): bool
{
$mediaModel = new MediaModel();
return $mediaModel->delete($this->id);
return $this->fileManager->delete($this->file_key);
}
public function rename(): bool
{
$newFilePath = $this->file_directory . '/' . (new File(''))->getRandomName() . '.' . $this->file_extension;
$newFileKey = $this->file_directory . '/' . (new File(''))->getRandomName() . '.' . $this->file_extension;
$db = db_connect();
$db->transStart();
if (! (new MediaModel())->update($this->id, [
'file_path' => $newFilePath,
'file_key' => $newFileKey,
])) {
return false;
}
if (! rename(media_path($this->file_path), media_path($newFilePath))) {
if (! $this->fileManager->rename($this->file_key, $newFileKey)) {
$db->transRollback();
return false;
}

View File

@ -8,7 +8,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities\Media;
namespace Modules\Media\Entities;
class Chapters extends BaseMedia
{

View File

@ -8,7 +8,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities\Media;
namespace Modules\Media\Entities;
class Document extends BaseMedia
{

View File

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Media\Entities;
use CodeIgniter\Files\File;
use Config\Services;
/**
* @property array $sizes
*/
class Image extends BaseMedia
{
protected string $type = 'image';
/**
* @var array<string, array<string, int|string>>
*/
protected array $sizes = [];
public function initFileProperties(): void
{
parent::initFileProperties();
if ($this->file_key !== '' && $this->file_metadata !== null && array_key_exists(
'sizes',
$this->file_metadata
)) {
$this->sizes = $this->file_metadata['sizes'];
$this->initSizeProperties();
}
}
public function initSizeProperties(): bool
{
helper('filesystem');
$fileKeyWithoutExt = path_without_ext($this->file_key);
foreach ($this->sizes as $name => $size) {
$extension = array_key_exists('extension', $size) ? $size['extension'] : $this->file_extension;
$mimetype = array_key_exists('mimetype', $size) ? $size['mimetype'] : $this->file_mimetype;
$this->{$name . '_key'} = $fileKeyWithoutExt . '_' . $name . '.' . $extension;
$this->{$name . '_url'} = $this->fileManager->getUrl($this->{$name . '_key'});
$this->{$name . '_mimetype'} = $mimetype;
}
return true;
}
/**
* @param array<string, string> $data
*/
public function setAttributes(array $data): self
{
parent::setAttributes($data);
if ($this->attributes === []) {
return $this;
}
if ($this->file_metadata !== [] && array_key_exists('sizes', $this->file_metadata)) {
$this->sizes = $this->file_metadata['sizes'];
$this->attributes['sizes'] = $this->file_metadata['sizes'];
$this->initFileProperties();
$this->initSizeProperties();
}
return $this;
}
public function setFile(File $file): self
{
parent::setFile($file);
if ($this->file_mimetype === 'image/jpeg' && $metadata = @exif_read_data(
$file->getRealPath(),
null,
true
)) {
$metadata['sizes'] = $this->attributes['sizes'];
$this->attributes['file_size'] = $metadata['FILE']['FileSize'];
} else {
$metadata = [
'sizes' => $this->attributes['sizes'],
];
}
$this->attributes['file_metadata'] = json_encode($metadata, JSON_INVALID_UTF8_IGNORE);
return $this;
}
public function saveFile(): bool
{
if ($this->attributes['sizes'] !== []) {
$this->initFileProperties();
$this->saveSizes();
}
return parent::saveFile();
}
public function deleteFile(): bool
{
if (parent::deleteFile()) {
return $this->deleteSizes();
}
return false;
}
public function saveSizes(): void
{
$tempImagePath = '';
if (! array_key_exists('file', $this->attributes) && $this->file_key) {
// no original file instance set to save sizes from
// download image temporarily to generate sizes from
$tempImagePath = (string) tempnam(WRITEPATH . 'temp', 'img_');
$imageContent = $this->fileManager->getFileContents($this->file_key);
file_put_contents($tempImagePath, $imageContent);
$this->attributes['file'] = new File($tempImagePath, true);
}
// save derived sizes
$imageService = Services::image();
foreach ($this->sizes as $name => $size) {
$tempFilePath = tempnam(WRITEPATH . 'temp', 'img_');
$imageService
->withFile($this->attributes['file']->getRealPath())
->resize($size['width'], $size['height'])
->save($tempFilePath);
$newImage = new File($tempFilePath, true);
$this->fileManager
->save($newImage, $this->{$name . '_key'});
}
if ($tempImagePath !== '') {
unlink($tempImagePath);
}
}
private function deleteSizes(): bool
{
// delete all derived sizes
foreach (array_keys($this->sizes) as $name) {
$pathProperty = $name . '_key';
if (! $this->fileManager->delete($this->{$pathProperty})) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Media\Entities;
use CodeIgniter\Files\File;
use Modules\Media\TranscriptParser;
class Transcript extends BaseMedia
{
public ?string $json_key = null;
public ?string $json_url = null;
protected string $type = 'transcript';
public function __construct(?array $data = null)
{
parent::__construct($data);
if ($this->file_key && $this->file_metadata && array_key_exists('json_key', $this->file_metadata)) {
helper('media');
$this->json_key = $this->file_metadata['json_key'];
$this->json_url = $this->fileManager
->getUrl($this->json_key);
}
}
public function setFile(File $file): self
{
parent::setFile($file);
$metadata = lstat((string) $file) ?? [];
helper('filesystem');
$fileKeyWithoutExt = path_without_ext($this->file_key);
$jsonfileKey = $fileKeyWithoutExt . '.json';
// set metadata (generated json file path)
$this->json_key = $jsonfileKey;
$metadata['json_key'] = $this->json_key;
$this->attributes['file_metadata'] = json_encode($metadata, JSON_INVALID_UTF8_IGNORE);
$this->file = $file;
return $this;
}
public function saveFile(): bool
{
$this->saveJsonTranscript();
return parent::saveFile();
}
public function deleteFile(): bool
{
if (! parent::deleteFile()) {
return false;
}
if ($this->json_key) {
return $this->fileManager->delete($this->json_key);
}
return true;
}
private function saveJsonTranscript(): bool
{
$srtContent = file_get_contents($this->file->getRealPath());
$transcriptParser = new TranscriptParser();
if ($srtContent === false) {
return false;
}
if (! $transcriptJson = $transcriptParser->loadString($srtContent)->parseSrt()) {
return false;
}
$tempFilePath = WRITEPATH . 'uploads/' . $this->file->getRandomName();
file_put_contents($tempFilePath, $transcriptJson);
$newTranscriptJson = new File($tempFilePath, true);
$this->fileManager
->save($newTranscriptJson, $this->json_key);
return true;
}
}

View File

@ -8,7 +8,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities\Media;
namespace Modules\Media\Entities;
class Video extends BaseMedia
{

View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Modules\Media\FileManagers;
use CodeIgniter\Files\File;
use Exception;
use Modules\Media\Config\Media as MediaConfig;
class FS implements FileManagerInterface
{
public function __construct(
protected MediaConfig $config
) {
$this->config = $config;
}
/**
* Saves a file to the corresponding folder in `public/media`
*/
public function save(File $file, string $path): string | false
{
if ((pathinfo($path, PATHINFO_EXTENSION) === '') && (($extension = $file->getExtension()) !== '')) {
$path = $path . '.' . $extension;
}
$mediaRoot = $this->config->root;
if (! file_exists(dirname($mediaRoot . '/' . $path))) {
mkdir(dirname($mediaRoot . '/' . $path), 0777, true);
}
if (! file_exists(dirname($mediaRoot . '/' . $path) . '/index.html')) {
touch(dirname($mediaRoot . '/' . $path) . '/index.html');
}
try {
// move to media folder, overwrite file if already existing
$file->move($mediaRoot . '/', $path, true);
} catch (Exception) {
return false;
}
return $path;
}
public function delete(string $key): bool
{
helper('media');
return unlink(media_path($key));
}
public function getUrl(string $key): string
{
$appConfig = config('App');
$mediaBaseUrl = $this->config->baseURL === '' ? $appConfig->baseURL : $this->config->baseURL;
return rtrim((string) $mediaBaseUrl, '/') .
'/' .
$this->config->root .
'/' .
$key;
}
public function rename(string $oldKey, string $newKey): bool
{
helper('media');
return rename(media_path($oldKey), media_path($newKey));
}
public function getFileContents(string $key): string
{
helper('media');
return (string) file_get_contents(media_path($key));
}
public function getFileInput(string $key): string
{
helper('media');
return media_path($key);
}
public function deletePodcastImageSizes(string $podcastHandle): bool
{
helper('media');
$allPodcastImagesPaths = [];
foreach (['jpg', 'png', 'webp'] as $ext) {
$images = glob(media_path("/podcasts/{$podcastHandle}/*_*{$ext}"));
if (! $images) {
return false;
}
array_push($allPodcastImagesPaths, ...$images);
}
foreach ($allPodcastImagesPaths as $podcastImagePath) {
if (is_file($podcastImagePath)) {
unlink($podcastImagePath);
}
}
return true;
}
public function deletePersonImagesSizes(): bool
{
helper('media');
$allPersonsImagesPaths = [];
foreach (['jpg', 'png', 'webp'] as $ext) {
$images = glob(media_path("/persons/*_*{$ext}"));
if (! $images) {
return false;
}
array_push($allPersonsImagesPaths, ...$images);
}
foreach ($allPersonsImagesPaths as $personImagePath) {
if (is_file($personImagePath)) {
unlink($personImagePath);
}
}
return true;
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Modules\Media\FileManagers;
use CodeIgniter\Files\File;
interface FileManagerInterface
{
public function save(File $file, string $key): string | false;
public function delete(string $key): bool;
public function getUrl(string $key): string;
public function rename(string $oldKey, string $newKey): bool;
public function getFileContents(string $key): string;
public function getFileInput(string $key): string;
public function deletePodcastImageSizes(string $podcastHandle): bool;
public function deletePersonImagesSizes(): bool;
}

View File

@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Modules\Media\FileManagers;
use Aws\Credentials\Credentials;
use Aws\S3\S3Client;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\URI;
use Exception;
use Modules\Media\Config\Media as MediaConfig;
class S3 implements FileManagerInterface
{
public S3Client $s3;
public function __construct(
protected MediaConfig $config
) {
$this->s3 = new S3Client([
'version' => 'latest',
'region' => $config->s3['region'],
'endpoint' => $config->s3['endpoint'],
'credentials' => new Credentials((string) $config->s3['key'], (string) $config->s3['secret']),
'debug' => $config->s3['debug'],
'use_path_style_endpoint' => $config->s3['path_style_endpoint'],
]);
// create bucket if it does not already exist
if (! $this->s3->doesBucketExist((string) $this->config->s3['bucket'])) {
try {
$this->s3->createBucket([
'Bucket' => $this->config->s3['bucket'],
]);
} catch (Exception $exception) {
log_message('critical', $exception->getMessage());
}
}
}
public function save(File $file, string $key): string|false
{
try {
$this->s3->putObject([
'Bucket' => $this->config->s3['bucket'],
'Key' => $key,
'SourceFile' => $file,
]);
} catch (Exception) {
return false;
}
// delete file after storage in s3
unlink($file->getRealPath());
return $key;
}
public function delete(string $key): bool
{
try {
$this->s3->deleteObject([
'Bucket' => $this->config->s3['bucket'],
'Key' => $key,
]);
} catch (Exception) {
return false;
}
return true;
}
public function getUrl(string $key): string
{
$url = new URI((string) $this->config->s3['endpoint']);
if ($this->config->s3['path_style_endpoint'] === true) {
$url->setPath($this->config->s3['bucket'] . '/' . $key);
return (string) $url;
}
$url->setHost($this->config->s3['bucket'] . '.' . $url->getHost());
$url->setPath($key);
return (string) $url;
}
public function rename(string $oldKey, string $newKey): bool
{
try {
// copy old object with new key
$this->s3->copyObject([
'Bucket' => $this->config->s3['bucket'],
'CopySource' => $this->config->s3['bucket'] . '/' . $oldKey,
'Key' => $newKey,
]);
} catch (Exception) {
return false;
}
// delete old object
return $this->delete($oldKey);
}
public function getFileContents(string $key): string
{
$result = $this->s3->getObject([
'Bucket' => $this->config->s3['bucket'],
'Key' => $key,
]);
return (string) $result->get('Body');
}
public function getFileInput(string $key): string
{
return $this->getUrl($key);
}
public function deletePodcastImageSizes(string $podcastHandle): bool
{
$results = $this->s3->getPaginator('ListObjectsV2', [
'Bucket' => $this->config->s3['bucket'],
'Prefix' => 'podcasts/' . $podcastHandle . '/',
]);
$keys = [];
foreach ($results as $result) {
$key = array_map(static function ($object) {
return $object['Key'];
}, $result['Contents']);
array_push($keys, ...preg_grep("~^podcasts\/{$podcastHandle}\/.*_.*.\.(jpg|png|webp)$~", $key));
}
$objectsToDelete = array_map(static function ($key): array {
return [
'Key' => $key,
];
}, $keys);
if ($objectsToDelete === []) {
return true;
}
try {
$this->s3->deleteObjects([
'Bucket' => $this->config->s3['bucket'],
'Delete' => [
'Objects' => $objectsToDelete,
],
]);
} catch (Exception) {
return false;
}
return true;
}
public function deletePersonImagesSizes(): bool
{
$objects = $this->s3->getIterator('ListObjectsV2', [
'Bucket' => $this->config->s3['bucket'],
'prefix' => 'persons/',
]);
$objectsKeys = array_map(static function ($object) {
return $object['Key'];
}, iterator_to_array($objects));
$podcastImageKeys = preg_grep("~^persons\/.*_.*.\.(jpg|png|webp)$~", $objectsKeys);
return (bool) $podcastImageKeys;
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
if (! function_exists('path_without_ext')) {
function path_without_ext(string $path): string
{
$fileKeyInfo = pathinfo($path);
if ($fileKeyInfo['dirname'] === '.' && ! str_starts_with($path, '.')) {
return $fileKeyInfo['filename'];
}
if ($fileKeyInfo['dirname'] === '/') {
return '/' . $fileKeyInfo['filename'];
}
return implode('/', [$fileKeyInfo['dirname'], $fileKeyInfo['filename']]);
}
}

View File

@ -9,39 +9,10 @@ declare(strict_types=1);
*/
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Mimes;
use Config\Services;
if (! function_exists('save_media')) {
/**
* Saves a file to the corresponding podcast folder in `public/media`
*/
function save_media(File | UploadedFile $file, string $folder = '', string $filename = null): string
{
if (($extension = $file->getExtension()) !== '') {
$filename = $filename . '.' . $extension;
}
$mediaRoot = config('App')
->mediaRoot . '/' . $folder;
if (! file_exists($mediaRoot)) {
mkdir($mediaRoot, 0777, true);
}
if (! file_exists($mediaRoot . '/index.html')) {
touch($mediaRoot . '/index.html');
}
// move to media folder, overwrite file if already existing
$file->move($mediaRoot . '/', $filename, true);
return $folder . '/' . $filename;
}
}
if (! function_exists('download_file')) {
function download_file(string $fileUrl, string $mimetype = ''): File
{
@ -86,10 +57,10 @@ if (! function_exists('download_file')) {
bin2hex(random_bytes(10)) .
'.' .
$extension;
$tmpFilePath = WRITEPATH . 'uploads/' . $tmpFilename;
file_put_contents($tmpFilePath, $response->getBody());
$tmpfileKey = WRITEPATH . 'uploads/' . $tmpFilename;
file_put_contents($tmpfileKey, $response->getBody());
return new File($tmpFilePath);
return new File($tmpfileKey);
}
}
@ -108,32 +79,6 @@ if (! function_exists('media_path')) {
$uri = trim($uri, '/');
return config('App')->mediaRoot . '/' . $uri;
}
}
if (! function_exists('media_base_url')) {
/**
* Return the media base URL to use in views
*
* @param string|string[] $uri URI string or array of URI segments
*/
function media_base_url(string | array $uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
$appConfig = config('App');
$mediaBaseUrl = $appConfig->mediaBaseURL === '' ? $appConfig->baseURL : $appConfig->mediaBaseURL;
return rtrim((string) $mediaBaseUrl, '/') .
'/' .
$appConfig->mediaRoot .
'/' .
$uri;
return config('Media')->root . '/' . $uri;
}
}

View File

@ -8,18 +8,18 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Models;
namespace Modules\Media\Models;
use App\Entities\Media\Audio;
use App\Entities\Media\Chapters;
use App\Entities\Media\Document;
use App\Entities\Media\Image;
use App\Entities\Media\Transcript;
use App\Entities\Media\Video;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Model;
use CodeIgniter\Validation\ValidationInterface;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Document;
use Modules\Media\Entities\Image;
use Modules\Media\Entities\Transcript;
use Modules\Media\Entities\Video;
class MediaModel extends Model
{
@ -52,7 +52,7 @@ class MediaModel extends Model
*/
protected $allowedFields = [
'id',
'file_path',
'file_key',
'file_size',
'file_mimetype',
'file_metadata',
@ -86,26 +86,14 @@ class MediaModel extends Model
ConnectionInterface &$db = null,
ValidationInterface $validation = null
) {
switch ($fileType) {
case 'audio':
$this->returnType = Audio::class;
break;
case 'video':
$this->returnType = Video::class;
break;
case 'image':
$this->returnType = Image::class;
break;
case 'transcript':
$this->returnType = Transcript::class;
break;
case 'chapters':
$this->returnType = Chapters::class;
break;
default:
// do nothing, keep Document class as default
break;
}
$this->returnType = match ($fileType) {
'audio' => Audio::class,
'video' => Video::class,
'image' => Image::class,
'transcript' => Transcript::class,
'chapters' => Chapters::class,
default => Document::class
};
parent::__construct($db, $validation);
}
@ -135,8 +123,15 @@ class MediaModel extends Model
*/
public function saveMedia(object $media): int | false
{
// save file first
if (! $media->saveFile()) {
return false;
}
// insert record in database
if (! $mediaId = $this->insert($media, true)) {
$this->db->transRollback();
return false;
}
@ -148,6 +143,11 @@ class MediaModel extends Model
*/
public function updateMedia(object $media): bool
{
// save file first
if (! $media->saveFile()) {
return false;
}
return $this->update($media->id, $media);
}
@ -166,9 +166,14 @@ class MediaModel extends Model
return $result;
}
public function deleteMedia(object $media): bool|BaseResult
/**
* @param Document|Audio|Video|Image|Transcript|Chapters $media
*/
public function deleteMedia($media): bool|BaseResult
{
$media->deleteFile();
if (! $media->deleteFile()) {
return false;
}
return $this->delete($media->id);
}

View File

@ -10,7 +10,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Libraries;
namespace Modules\Media;
use stdClass;

View File

@ -29,12 +29,12 @@
"dependencies": {
"@amcharts/amcharts4": "^4.10.34",
"@amcharts/amcharts4-geodata": "^4.1.26",
"@codemirror/commands": "^6.2.1",
"@codemirror/commands": "^6.2.2",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.9.1",
"@floating-ui/dom": "^1.2.1",
"@codemirror/view": "^6.9.2",
"@floating-ui/dom": "^1.2.4",
"@github/clipboard-copy-element": "^1.1.2",
"@github/hotkey": "^2.0.1",
"@github/markdown-toolbar-element": "^2.1.1",
@ -48,8 +48,8 @@
"leaflet.markercluster": "^1.5.3",
"lit": "^2.6.1",
"marked": "^4.2.12",
"wavesurfer.js": "^6.4.0",
"xml-formatter": "^3.2.0"
"wavesurfer.js": "^6.5.2",
"xml-formatter": "^3.3.2"
},
"devDependencies": {
"@commitlint/cli": "^17.4.4",
@ -61,22 +61,22 @@
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.9",
"@types/leaflet": "^1.9.1",
"@types/leaflet": "^1.9.2",
"@types/marked": "^4.0.8",
"@types/wavesurfer.js": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"all-contributors-cli": "^6.24.0",
"commitizen": "^4.3.0",
"cross-env": "^7.0.3",
"cssnano": "^5.1.15",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.7.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"lint-staged": "^13.1.2",
"lint-staged": "^13.2.0",
"postcss": "^8.4.21",
"postcss-import": "^15.1.0",
"postcss-nesting": "^11.2.1",
@ -84,13 +84,13 @@
"postcss-reporter": "^7.0.5",
"prettier": "2.8.4",
"prettier-plugin-organize-imports": "^3.2.2",
"semantic-release": "^20.1.0",
"semantic-release": "^20.1.1",
"stylelint": "^15.2.0",
"stylelint-config-standard": "^30.0.1",
"svgo": "^3.0.2",
"tailwindcss": "^3.2.7",
"typescript": "^4.9.5",
"vite": "4.1.3",
"vite": "^4.1.4",
"vite-plugin-pwa": "^0.14.4",
"workbox-build": "^6.5.4",
"workbox-core": "^6.5.4",

View File

@ -11,6 +11,7 @@ parameters:
- modules/Analytics/Helpers
- modules/Auth/Helpers
- modules/Fediverse/Helpers
- modules/Media/Helpers
- modules/PremiumPodcasts/Helpers
- vendor/codeigniter4/framework/system/Helpers
- vendor/codeigniter4/settings/src/Helpers
@ -28,5 +29,5 @@ parameters:
ignoreErrors:
- '#Cannot access property [\$a-z_]+ on ((array\|)?object)#'
- '#^Call to an undefined method CodeIgniter\\Database\\ConnectionInterface#'
- '#^Access to an undefined property App\\Entities\\Media\\Image#'
- '#^Access to an undefined property Modules\\Media\\Entities\\Image#'
- '#^Call to an undefined method CodeIgniter\\HTTP\\RequestInterface#'

View File

@ -3,14 +3,14 @@ lockfileVersion: 5.4
specifiers:
"@amcharts/amcharts4": ^4.10.34
"@amcharts/amcharts4-geodata": ^4.1.26
"@codemirror/commands": ^6.2.1
"@codemirror/commands": ^6.2.2
"@codemirror/lang-xml": ^6.0.2
"@codemirror/language": ^6.6.0
"@codemirror/state": ^6.2.0
"@codemirror/view": ^6.9.1
"@codemirror/view": ^6.9.2
"@commitlint/cli": ^17.4.4
"@commitlint/config-conventional": ^17.4.4
"@floating-ui/dom": ^1.2.1
"@floating-ui/dom": ^1.2.4
"@github/clipboard-copy-element": ^1.1.2
"@github/hotkey": ^2.0.1
"@github/markdown-toolbar-element": ^2.1.1
@ -23,11 +23,11 @@ specifiers:
"@tailwindcss/line-clamp": ^0.4.2
"@tailwindcss/nesting": 0.0.0-insiders.565cd3e
"@tailwindcss/typography": ^0.5.9
"@types/leaflet": ^1.9.1
"@types/leaflet": ^1.9.2
"@types/marked": ^4.0.8
"@types/wavesurfer.js": ^6.0.3
"@typescript-eslint/eslint-plugin": ^5.53.0
"@typescript-eslint/parser": ^5.53.0
"@typescript-eslint/eslint-plugin": ^5.55.0
"@typescript-eslint/parser": ^5.55.0
"@vime/core": ^5.4.0
all-contributors-cli: ^6.24.0
choices.js: ^10.2.0
@ -36,15 +36,15 @@ specifiers:
cross-env: ^7.0.3
cssnano: ^5.1.15
cz-conventional-changelog: ^3.3.0
eslint: ^8.34.0
eslint-config-prettier: ^8.6.0
eslint: ^8.36.0
eslint-config-prettier: ^8.7.0
eslint-plugin-prettier: ^4.2.1
flatpickr: ^4.6.13
husky: ^8.0.3
is-ci: ^3.0.1
leaflet: ^1.9.3
leaflet.markercluster: ^1.5.3
lint-staged: ^13.1.2
lint-staged: ^13.2.0
lit: ^2.6.1
marked: ^4.2.12
postcss: ^8.4.21
@ -54,30 +54,30 @@ specifiers:
postcss-reporter: ^7.0.5
prettier: 2.8.4
prettier-plugin-organize-imports: ^3.2.2
semantic-release: ^20.1.0
semantic-release: ^20.1.1
stylelint: ^15.2.0
stylelint-config-standard: ^30.0.1
svgo: ^3.0.2
tailwindcss: ^3.2.7
typescript: ^4.9.5
vite: 4.1.3
vite: ^4.1.4
vite-plugin-pwa: ^0.14.4
wavesurfer.js: ^6.4.0
wavesurfer.js: ^6.5.2
workbox-build: ^6.5.4
workbox-core: ^6.5.4
workbox-routing: ^6.5.4
workbox-strategies: ^6.5.4
xml-formatter: ^3.2.0
xml-formatter: ^3.3.2
dependencies:
"@amcharts/amcharts4": 4.10.34
"@amcharts/amcharts4-geodata": 4.1.26
"@codemirror/commands": 6.2.1
"@codemirror/lang-xml": 6.0.2_@codemirror+view@6.9.1
"@codemirror/commands": 6.2.2
"@codemirror/lang-xml": 6.0.2_@codemirror+view@6.9.2
"@codemirror/language": 6.6.0
"@codemirror/state": 6.2.0
"@codemirror/view": 6.9.1
"@floating-ui/dom": 1.2.1
"@codemirror/view": 6.9.2
"@floating-ui/dom": 1.2.4
"@github/clipboard-copy-element": 1.1.2
"@github/hotkey": 2.0.1
"@github/markdown-toolbar-element": 2.1.1
@ -91,35 +91,35 @@ dependencies:
leaflet.markercluster: 1.5.3_leaflet@1.9.3
lit: 2.6.1
marked: 4.2.12
wavesurfer.js: 6.4.0
xml-formatter: 3.2.0
wavesurfer.js: 6.5.2
xml-formatter: 3.3.2
devDependencies:
"@commitlint/cli": 17.4.4
"@commitlint/config-conventional": 17.4.4
"@semantic-release/changelog": 6.0.2_semantic-release@20.1.0
"@semantic-release/exec": 6.0.3_semantic-release@20.1.0
"@semantic-release/git": 10.0.1_semantic-release@20.1.0
"@semantic-release/gitlab": 11.0.1_semantic-release@20.1.0
"@semantic-release/changelog": 6.0.2_semantic-release@20.1.1
"@semantic-release/exec": 6.0.3_semantic-release@20.1.1
"@semantic-release/git": 10.0.1_semantic-release@20.1.1
"@semantic-release/gitlab": 11.0.1_semantic-release@20.1.1
"@tailwindcss/forms": 0.5.3_tailwindcss@3.2.7
"@tailwindcss/line-clamp": 0.4.2_tailwindcss@3.2.7
"@tailwindcss/typography": 0.5.9_tailwindcss@3.2.7
"@types/leaflet": 1.9.1
"@types/leaflet": 1.9.2
"@types/marked": 4.0.8
"@types/wavesurfer.js": 6.0.3
"@typescript-eslint/eslint-plugin": 5.53.0_ny4s7qc6yg74faf3d6xty2ofzy
"@typescript-eslint/parser": 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
"@typescript-eslint/eslint-plugin": 5.55.0_342y7v4tc7ytrrysmit6jo4wri
"@typescript-eslint/parser": 5.55.0_vgl77cfdswitgr47lm5swmv43m
all-contributors-cli: 6.24.0
commitizen: 4.3.0
cross-env: 7.0.3
cssnano: 5.1.15_postcss@8.4.21
cz-conventional-changelog: 3.3.0
eslint: 8.34.0
eslint-config-prettier: 8.6.0_eslint@8.34.0
eslint-plugin-prettier: 4.2.1_u5wnrdwibbfomslmnramz52buy
eslint: 8.36.0
eslint-config-prettier: 8.7.0_eslint@8.36.0
eslint-plugin-prettier: 4.2.1_eqzx3hpkgx5nnvxls3azrcc7dm
husky: 8.0.3
is-ci: 3.0.1
lint-staged: 13.1.2
lint-staged: 13.2.0
postcss: 8.4.21
postcss-import: 15.1.0_postcss@8.4.21
postcss-nesting: 11.2.1_postcss@8.4.21
@ -127,14 +127,14 @@ devDependencies:
postcss-reporter: 7.0.5_postcss@8.4.21
prettier: 2.8.4
prettier-plugin-organize-imports: 3.2.2_silln3pw57har7jydmecgzoypa
semantic-release: 20.1.0
semantic-release: 20.1.1
stylelint: 15.2.0
stylelint-config-standard: 30.0.1_stylelint@15.2.0
svgo: 3.0.2
tailwindcss: 3.2.7_postcss@8.4.21
typescript: 4.9.5
vite: 4.1.3
vite-plugin-pwa: 0.14.4_rcpzravakhu7gk56p6427hsr2y
vite: 4.1.4
vite-plugin-pwa: 0.14.4_vizhyq4kcdharmiplw7eejneda
workbox-build: 6.5.4
workbox-core: 6.5.4
workbox-routing: 6.5.4
@ -1648,7 +1648,7 @@ packages:
to-fast-properties: 2.0.0
dev: true
/@codemirror/autocomplete/6.4.2_dtwlkgx6567fllxi7sgvnep6hy:
/@codemirror/autocomplete/6.4.2_m2g2fjrvetqbsl7zxwctz5ljh4:
resolution:
{
integrity: sha512-8WE2xp+D0MpWEv5lZ6zPW1/tf4AGb358T5GWYiKEuCP8MvFfT3tH2mIF9Y2yr2e3KbHuSvsVhosiEyqCpiJhZQ==,
@ -1660,29 +1660,29 @@ packages:
dependencies:
"@codemirror/language": 6.6.0
"@codemirror/state": 6.2.0
"@codemirror/view": 6.9.1
"@codemirror/view": 6.9.2
"@lezer/common": 1.0.2
dev: false
/@codemirror/commands/6.2.1:
/@codemirror/commands/6.2.2:
resolution:
{
integrity: sha512-FFiNKGuHA5O8uC6IJE5apI5rT9gyjlw4whqy4vlcX0wE/myxL6P1s0upwDhY4HtMWLOwzwsp0ap3bjdQhvfDOA==,
integrity: sha512-s9lPVW7TxXrI/7voZ+HmD/yiAlwAYn9PH5SUVSUhsxXHhv4yl5eZ3KLntSoTynfdgVYM0oIpccQEWRBQgmNZyw==,
}
dependencies:
"@codemirror/language": 6.6.0
"@codemirror/state": 6.2.0
"@codemirror/view": 6.9.1
"@codemirror/view": 6.9.2
"@lezer/common": 1.0.2
dev: false
/@codemirror/lang-xml/6.0.2_@codemirror+view@6.9.1:
/@codemirror/lang-xml/6.0.2_@codemirror+view@6.9.2:
resolution:
{
integrity: sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==,
}
dependencies:
"@codemirror/autocomplete": 6.4.2_dtwlkgx6567fllxi7sgvnep6hy
"@codemirror/autocomplete": 6.4.2_m2g2fjrvetqbsl7zxwctz5ljh4
"@codemirror/language": 6.6.0
"@codemirror/state": 6.2.0
"@lezer/common": 1.0.2
@ -1698,7 +1698,7 @@ packages:
}
dependencies:
"@codemirror/state": 6.2.0
"@codemirror/view": 6.9.1
"@codemirror/view": 6.9.2
"@lezer/common": 1.0.2
"@lezer/highlight": 1.1.3
"@lezer/lr": 1.3.3
@ -1712,7 +1712,7 @@ packages:
}
dependencies:
"@codemirror/state": 6.2.0
"@codemirror/view": 6.9.1
"@codemirror/view": 6.9.2
crelt: 1.0.5
dev: false
@ -1723,7 +1723,7 @@ packages:
}
dependencies:
"@codemirror/state": 6.2.0
"@codemirror/view": 6.9.1
"@codemirror/view": 6.9.2
crelt: 1.0.5
dev: false
@ -1734,10 +1734,10 @@ packages:
}
dev: false
/@codemirror/view/6.9.1:
/@codemirror/view/6.9.2:
resolution:
{
integrity: sha512-bzfSjJn9dAADVpabLKWKNmMG4ibyTV2e3eOGowjElNPTdTkSbi6ixPYHm2u0ADcETfKsi2/R84Rkmi91dH9yEg==,
integrity: sha512-ci0r/v6aKOSlzOs7/STMTYP3jX/+YMq2dAfAJcLIB6uom4ThtrUlzeuS7GTRGNqJJ+qAJR1vGWfXgu4CO/0myQ==,
}
dependencies:
"@codemirror/state": 6.2.0
@ -2574,16 +2574,37 @@ packages:
dev: true
optional: true
/@eslint/eslintrc/1.4.1:
/@eslint-community/eslint-utils/4.2.0_eslint@8.36.0:
resolution:
{
integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==,
integrity: sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
dependencies:
eslint: 8.36.0
eslint-visitor-keys: 3.3.0
dev: true
/@eslint-community/regexpp/4.4.0:
resolution:
{
integrity: sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==,
}
engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 }
dev: true
/@eslint/eslintrc/2.0.1:
resolution:
{
integrity: sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dependencies:
ajv: 6.12.6
debug: 4.3.4
espree: 9.4.1
espree: 9.5.0
globals: 13.20.0
ignore: 5.2.4
import-fresh: 3.3.0
@ -2594,20 +2615,28 @@ packages:
- supports-color
dev: true
/@floating-ui/core/1.2.1:
/@eslint/js/8.36.0:
resolution:
{
integrity: sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==,
integrity: sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dev: true
/@floating-ui/core/1.2.4:
resolution:
{
integrity: sha512-SQOeVbMwb1di+mVWWJLpsUTToKfqVNioXys011beCAhyOIFtS+GQoW4EQSneuxzmQKddExDwQ+X0hLl4lJJaSQ==,
}
dev: false
/@floating-ui/dom/1.2.1:
/@floating-ui/dom/1.2.4:
resolution:
{
integrity: sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==,
integrity: sha512-4+k+BLhtWj+peCU60gp0+rHeR8+Ohqx6kjJf/lHMnJ8JD5Qj6jytcq1+SZzRwD7rvHKRhR7TDiWWddrNrfwQLg==,
}
dependencies:
"@floating-ui/core": 1.2.1
"@floating-ui/core": 1.2.4
dev: false
/@foliojs-fork/fontkit/1.9.1:
@ -3147,7 +3176,7 @@ packages:
rollup: 3.17.2
dev: true
/@semantic-release/changelog/6.0.2_semantic-release@20.1.0:
/@semantic-release/changelog/6.0.2_semantic-release@20.1.1:
resolution:
{
integrity: sha512-jHqfTkoPbDEOAgAP18mGP53IxeMwxTISN+GwTRy9uLu58UjARoZU8ScCgWGeO2WPkEsm57H8AkyY02W2ntIlIw==,
@ -3160,10 +3189,10 @@ packages:
aggregate-error: 3.1.0
fs-extra: 11.1.0
lodash: 4.17.21
semantic-release: 20.1.0
semantic-release: 20.1.1
dev: true
/@semantic-release/commit-analyzer/9.0.2_semantic-release@20.1.0:
/@semantic-release/commit-analyzer/9.0.2_semantic-release@20.1.1:
resolution:
{
integrity: sha512-E+dr6L+xIHZkX4zNMe6Rnwg4YQrWNXK+rNsvwOPpdFppvZO1olE2fIgWhv89TkQErygevbjsZFSIxp+u6w2e5g==,
@ -3179,7 +3208,7 @@ packages:
import-from: 4.0.0
lodash: 4.17.21
micromatch: 4.0.5
semantic-release: 20.1.0
semantic-release: 20.1.1
transitivePeerDependencies:
- supports-color
dev: true
@ -3192,7 +3221,7 @@ packages:
engines: { node: ">=14.17" }
dev: true
/@semantic-release/exec/6.0.3_semantic-release@20.1.0:
/@semantic-release/exec/6.0.3_semantic-release@20.1.1:
resolution:
{
integrity: sha512-bxAq8vLOw76aV89vxxICecEa8jfaWwYITw6X74zzlO0mc/Bgieqx9kBRz9z96pHectiTAtsCwsQcUyLYWnp3VQ==,
@ -3207,12 +3236,12 @@ packages:
execa: 5.1.1
lodash: 4.17.21
parse-json: 5.2.0
semantic-release: 20.1.0
semantic-release: 20.1.1
transitivePeerDependencies:
- supports-color
dev: true
/@semantic-release/git/10.0.1_semantic-release@20.1.0:
/@semantic-release/git/10.0.1_semantic-release@20.1.1:
resolution:
{
integrity: sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==,
@ -3229,12 +3258,12 @@ packages:
lodash: 4.17.21
micromatch: 4.0.5
p-reduce: 2.1.0
semantic-release: 20.1.0
semantic-release: 20.1.1
transitivePeerDependencies:
- supports-color
dev: true
/@semantic-release/github/8.0.7_semantic-release@20.1.0:
/@semantic-release/github/8.0.7_semantic-release@20.1.1:
resolution:
{
integrity: sha512-VtgicRIKGvmTHwm//iqTh/5NGQwsncOMR5vQK9pMT92Aem7dv37JFKKRuulUsAnUOIlO4G8wH3gPiBAA0iW0ww==,
@ -3258,14 +3287,14 @@ packages:
mime: 3.0.0
p-filter: 2.1.0
p-retry: 4.6.2
semantic-release: 20.1.0
semantic-release: 20.1.1
url-join: 4.0.1
transitivePeerDependencies:
- encoding
- supports-color
dev: true
/@semantic-release/gitlab/11.0.1_semantic-release@20.1.0:
/@semantic-release/gitlab/11.0.1_semantic-release@20.1.1:
resolution:
{
integrity: sha512-CWXHlLZonwrUR2pbYaoERVu1cDVsi5W4H0WXDDCcDwicMmizsTkKJlpP9CQowoluqHKIJYrLkr2b+lYXCnBJZw==,
@ -3286,13 +3315,13 @@ packages:
hpagent: 1.2.0
lodash-es: 4.17.21
parse-url: 8.1.0
semantic-release: 20.1.0
semantic-release: 20.1.1
url-join: 4.0.1
transitivePeerDependencies:
- supports-color
dev: true
/@semantic-release/npm/9.0.2_semantic-release@20.1.0:
/@semantic-release/npm/9.0.2_semantic-release@20.1.1:
resolution:
{
integrity: sha512-zgsynF6McdzxPnFet+a4iO9HpAlARXOM5adz7VGVCvj0ne8wtL2ZOQoDV2wZPDmdEotDIbVeJjafhelZjs9j6g==,
@ -3312,12 +3341,12 @@ packages:
rc: 1.2.8
read-pkg: 5.2.0
registry-auth-token: 5.0.1
semantic-release: 20.1.0
semantic-release: 20.1.1
semver: 7.3.8
tempy: 1.0.1
dev: true
/@semantic-release/release-notes-generator/10.0.3_semantic-release@20.1.0:
/@semantic-release/release-notes-generator/10.0.3_semantic-release@20.1.1:
resolution:
{
integrity: sha512-k4x4VhIKneOWoBGHkx0qZogNjCldLPRiAjnIpMnlUh6PtaWXp/T+C9U7/TaNDDtgDa5HMbHl4WlREdxHio6/3w==,
@ -3336,7 +3365,7 @@ packages:
into-stream: 6.0.0
lodash: 4.17.21
read-pkg-up: 7.0.1
semantic-release: 20.1.0
semantic-release: 20.1.1
transitivePeerDependencies:
- supports-color
dev: true
@ -3523,10 +3552,10 @@ packages:
}
dev: true
/@types/leaflet/1.9.1:
/@types/leaflet/1.9.2:
resolution:
{
integrity: sha512-lYawM3I3lLO6rmBASaqdGgY6zUL4YHr3H79/axx7FNYyPXuj0P1DZHbkNo8Itbv0i7Y9EryLWtDXXROMygXhRA==,
integrity: sha512-vrokGIGVO8RSNXQBcWdEJ4Xy6E9kLQHZfpxIkFjSD1OhqTKOjYLFJDG6JCoAWYm/n755fdNCyrpna6/00kVajw==,
}
dependencies:
"@types/geojson": 7946.0.10
@ -3598,10 +3627,10 @@ packages:
"@types/debounce": 1.2.1
dev: true
/@typescript-eslint/eslint-plugin/5.53.0_ny4s7qc6yg74faf3d6xty2ofzy:
/@typescript-eslint/eslint-plugin/5.55.0_342y7v4tc7ytrrysmit6jo4wri:
resolution:
{
integrity: sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw==,
integrity: sha512-IZGc50rtbjk+xp5YQoJvmMPmJEYoC53SiKPXyqWfv15XoD2Y5Kju6zN0DwlmaGJp1Iw33JsWJcQ7nw0lGCGjVg==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
@ -3612,16 +3641,16 @@ packages:
typescript:
optional: true
dependencies:
"@typescript-eslint/parser": 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
"@typescript-eslint/scope-manager": 5.53.0
"@typescript-eslint/type-utils": 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
"@typescript-eslint/utils": 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
"@eslint-community/regexpp": 4.4.0
"@typescript-eslint/parser": 5.55.0_vgl77cfdswitgr47lm5swmv43m
"@typescript-eslint/scope-manager": 5.55.0
"@typescript-eslint/type-utils": 5.55.0_vgl77cfdswitgr47lm5swmv43m
"@typescript-eslint/utils": 5.55.0_vgl77cfdswitgr47lm5swmv43m
debug: 4.3.4
eslint: 8.34.0
eslint: 8.36.0
grapheme-splitter: 1.0.4
ignore: 5.2.4
natural-compare-lite: 1.4.0
regexpp: 3.2.0
semver: 7.3.8
tsutils: 3.21.0_typescript@4.9.5
typescript: 4.9.5
@ -3629,10 +3658,10 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser/5.53.0_7kw3g6rralp5ps6mg3uyzz6azm:
/@typescript-eslint/parser/5.55.0_vgl77cfdswitgr47lm5swmv43m:
resolution:
{
integrity: sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ==,
integrity: sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
@ -3642,31 +3671,31 @@ packages:
typescript:
optional: true
dependencies:
"@typescript-eslint/scope-manager": 5.53.0
"@typescript-eslint/types": 5.53.0
"@typescript-eslint/typescript-estree": 5.53.0_typescript@4.9.5
"@typescript-eslint/scope-manager": 5.55.0
"@typescript-eslint/types": 5.55.0
"@typescript-eslint/typescript-estree": 5.55.0_typescript@4.9.5
debug: 4.3.4
eslint: 8.34.0
eslint: 8.36.0
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/scope-manager/5.53.0:
/@typescript-eslint/scope-manager/5.55.0:
resolution:
{
integrity: sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==,
integrity: sha512-OK+cIO1ZGhJYNCL//a3ROpsd83psf4dUJ4j7pdNVzd5DmIk+ffkuUIX2vcZQbEW/IR41DYsfJTB19tpCboxQuw==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dependencies:
"@typescript-eslint/types": 5.53.0
"@typescript-eslint/visitor-keys": 5.53.0
"@typescript-eslint/types": 5.55.0
"@typescript-eslint/visitor-keys": 5.55.0
dev: true
/@typescript-eslint/type-utils/5.53.0_7kw3g6rralp5ps6mg3uyzz6azm:
/@typescript-eslint/type-utils/5.55.0_vgl77cfdswitgr47lm5swmv43m:
resolution:
{
integrity: sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw==,
integrity: sha512-ObqxBgHIXj8rBNm0yh8oORFrICcJuZPZTqtAFh0oZQyr5DnAHZWfyw54RwpEEH+fD8suZaI0YxvWu5tYE/WswA==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
@ -3676,28 +3705,28 @@ packages:
typescript:
optional: true
dependencies:
"@typescript-eslint/typescript-estree": 5.53.0_typescript@4.9.5
"@typescript-eslint/utils": 5.53.0_7kw3g6rralp5ps6mg3uyzz6azm
"@typescript-eslint/typescript-estree": 5.55.0_typescript@4.9.5
"@typescript-eslint/utils": 5.55.0_vgl77cfdswitgr47lm5swmv43m
debug: 4.3.4
eslint: 8.34.0
eslint: 8.36.0
tsutils: 3.21.0_typescript@4.9.5
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/types/5.53.0:
/@typescript-eslint/types/5.55.0:
resolution:
{
integrity: sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==,
integrity: sha512-M4iRh4AG1ChrOL6Y+mETEKGeDnT7Sparn6fhZ5LtVJF1909D5O4uqK+C5NPbLmpfZ0XIIxCdwzKiijpZUOvOug==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dev: true
/@typescript-eslint/typescript-estree/5.53.0_typescript@4.9.5:
/@typescript-eslint/typescript-estree/5.55.0_typescript@4.9.5:
resolution:
{
integrity: sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==,
integrity: sha512-I7X4A9ovA8gdpWMpr7b1BN9eEbvlEtWhQvpxp/yogt48fy9Lj3iE3ild/1H3jKBBIYj5YYJmS2+9ystVhC7eaQ==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
@ -3706,8 +3735,8 @@ packages:
typescript:
optional: true
dependencies:
"@typescript-eslint/types": 5.53.0
"@typescript-eslint/visitor-keys": 5.53.0
"@typescript-eslint/types": 5.55.0
"@typescript-eslint/visitor-keys": 5.55.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
@ -3718,37 +3747,37 @@ packages:
- supports-color
dev: true
/@typescript-eslint/utils/5.53.0_7kw3g6rralp5ps6mg3uyzz6azm:
/@typescript-eslint/utils/5.55.0_vgl77cfdswitgr47lm5swmv43m:
resolution:
{
integrity: sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g==,
integrity: sha512-FkW+i2pQKcpDC3AY6DU54yl8Lfl14FVGYDgBTyGKB75cCwV3KpkpTMFi9d9j2WAJ4271LR2HeC5SEWF/CZmmfw==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
"@eslint-community/eslint-utils": 4.2.0_eslint@8.36.0
"@types/json-schema": 7.0.11
"@types/semver": 7.3.13
"@typescript-eslint/scope-manager": 5.53.0
"@typescript-eslint/types": 5.53.0
"@typescript-eslint/typescript-estree": 5.53.0_typescript@4.9.5
eslint: 8.34.0
"@typescript-eslint/scope-manager": 5.55.0
"@typescript-eslint/types": 5.55.0
"@typescript-eslint/typescript-estree": 5.55.0_typescript@4.9.5
eslint: 8.36.0
eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@8.34.0
semver: 7.3.8
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/@typescript-eslint/visitor-keys/5.53.0:
/@typescript-eslint/visitor-keys/5.55.0:
resolution:
{
integrity: sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==,
integrity: sha512-q2dlHHwWgirKh1D3acnuApXG+VNXpEY5/AwRxDVuEQpxWaB0jCDe0jFMVMALJ3ebSfuOVE8/rMS+9ZOYGg1GWw==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dependencies:
"@typescript-eslint/types": 5.53.0
"@typescript-eslint/types": 5.55.0
eslint-visitor-keys: 3.3.0
dev: true
@ -4678,13 +4707,13 @@ packages:
integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==,
}
dependencies:
"@codemirror/autocomplete": 6.4.2_dtwlkgx6567fllxi7sgvnep6hy
"@codemirror/commands": 6.2.1
"@codemirror/autocomplete": 6.4.2_m2g2fjrvetqbsl7zxwctz5ljh4
"@codemirror/commands": 6.2.2
"@codemirror/language": 6.6.0
"@codemirror/lint": 6.1.1
"@codemirror/search": 6.2.3
"@codemirror/state": 6.2.0
"@codemirror/view": 6.9.1
"@codemirror/view": 6.9.2
dev: false
/codepage/1.15.0:
@ -4752,6 +4781,14 @@ packages:
delayed-stream: 1.0.0
dev: true
/commander/10.0.0:
resolution:
{
integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==,
}
engines: { node: ">=14" }
dev: true
/commander/2.20.3:
resolution:
{
@ -4766,14 +4803,6 @@ packages:
}
engines: { node: ">= 10" }
/commander/9.5.0:
resolution:
{
integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==,
}
engines: { node: ^12.20.0 || >=14 }
dev: true
/commitizen/4.3.0:
resolution:
{
@ -6066,19 +6095,19 @@ packages:
source-map: 0.1.43
dev: false
/eslint-config-prettier/8.6.0_eslint@8.34.0:
/eslint-config-prettier/8.7.0_eslint@8.36.0:
resolution:
{
integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==,
integrity: sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA==,
}
hasBin: true
peerDependencies:
eslint: ">=7.0.0"
dependencies:
eslint: 8.34.0
eslint: 8.36.0
dev: true
/eslint-plugin-prettier/4.2.1_u5wnrdwibbfomslmnramz52buy:
/eslint-plugin-prettier/4.2.1_eqzx3hpkgx5nnvxls3azrcc7dm:
resolution:
{
integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==,
@ -6092,8 +6121,8 @@ packages:
eslint-config-prettier:
optional: true
dependencies:
eslint: 8.34.0
eslint-config-prettier: 8.6.0_eslint@8.34.0
eslint: 8.36.0
eslint-config-prettier: 8.7.0_eslint@8.36.0
prettier: 2.8.4
prettier-linter-helpers: 1.0.0
dev: true
@ -6120,27 +6149,6 @@ packages:
estraverse: 5.3.0
dev: true
/eslint-utils/3.0.0_eslint@8.34.0:
resolution:
{
integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==,
}
engines: { node: ^10.0.0 || ^12.0.0 || >= 14.0.0 }
peerDependencies:
eslint: ">=5"
dependencies:
eslint: 8.34.0
eslint-visitor-keys: 2.1.0
dev: true
/eslint-visitor-keys/2.1.0:
resolution:
{
integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==,
}
engines: { node: ">=10" }
dev: true
/eslint-visitor-keys/3.3.0:
resolution:
{
@ -6149,15 +6157,18 @@ packages:
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dev: true
/eslint/8.34.0:
/eslint/8.36.0:
resolution:
{
integrity: sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==,
integrity: sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
hasBin: true
dependencies:
"@eslint/eslintrc": 1.4.1
"@eslint-community/eslint-utils": 4.2.0_eslint@8.36.0
"@eslint-community/regexpp": 4.4.0
"@eslint/eslintrc": 2.0.1
"@eslint/js": 8.36.0
"@humanwhocodes/config-array": 0.11.8
"@humanwhocodes/module-importer": 1.0.1
"@nodelib/fs.walk": 1.2.8
@ -6168,9 +6179,8 @@ packages:
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.1.1
eslint-utils: 3.0.0_eslint@8.34.0
eslint-visitor-keys: 3.3.0
espree: 9.4.1
espree: 9.5.0
esquery: 1.4.2
esutils: 2.0.3
fast-deep-equal: 3.1.3
@ -6192,7 +6202,6 @@ packages:
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.1
regexpp: 3.2.0
strip-ansi: 6.0.1
strip-json-comments: 3.1.1
text-table: 0.2.0
@ -6200,10 +6209,10 @@ packages:
- supports-color
dev: true
/espree/9.4.1:
/espree/9.5.0:
resolution:
{
integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==,
integrity: sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dependencies:
@ -6354,6 +6363,24 @@ packages:
strip-final-newline: 3.0.0
dev: true
/execa/7.1.0:
resolution:
{
integrity: sha512-T6nIJO3LHxUZ6ahVRaxXz9WLEruXLqdcluA+UuTptXmLM7nDAn9lx9IfkxPyzEL21583qSt4RmL44pO71EHaJQ==,
}
engines: { node: ^14.18.0 || ^16.14.0 || >=18.0.0 }
dependencies:
cross-spawn: 7.0.3
get-stream: 6.0.1
human-signals: 4.3.0
is-stream: 3.0.0
merge-stream: 2.0.0
npm-run-path: 5.1.0
onetime: 6.0.0
signal-exit: 3.0.7
strip-final-newline: 3.0.0
dev: true
/expand-tilde/2.0.2:
resolution:
{
@ -7231,6 +7258,14 @@ packages:
engines: { node: ">=12.20.0" }
dev: true
/human-signals/4.3.0:
resolution:
{
integrity: sha512-zyzVyMjpGBX2+6cDVZeFPCdtOtdsxOeseRhB9tkQ6xXmGUNrcnBzdEKPy3VPNYz+4gy1oukVOXcrJCunSyc6QQ==,
}
engines: { node: ">=14.18.0" }
dev: true
/husky/8.0.3:
resolution:
{
@ -8063,6 +8098,14 @@ packages:
engines: { node: ">=10" }
dev: true
/lilconfig/2.1.0:
resolution:
{
integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==,
}
engines: { node: ">=10" }
dev: true
/lines-and-columns/1.2.4:
resolution:
{
@ -8070,20 +8113,20 @@ packages:
}
dev: true
/lint-staged/13.1.2:
/lint-staged/13.2.0:
resolution:
{
integrity: sha512-K9b4FPbWkpnupvK3WXZLbgu9pchUJ6N7TtVZjbaPsoizkqFUDkUReUL25xdrCljJs7uLUF3tZ7nVPeo/6lp+6w==,
integrity: sha512-GbyK5iWinax5Dfw5obm2g2ccUiZXNGtAS4mCbJ0Lv4rq6iEtfBSjOYdcbOtAIFtM114t0vdpViDDetjVTSd8Vw==,
}
engines: { node: ^14.13.1 || >=16.0.0 }
hasBin: true
dependencies:
chalk: 5.2.0
cli-truncate: 3.1.0
colorette: 2.0.19
commander: 9.5.0
commander: 10.0.0
debug: 4.3.4
execa: 6.1.0
lilconfig: 2.0.6
execa: 7.1.0
lilconfig: 2.1.0
listr2: 5.0.7
micromatch: 4.0.5
normalize-path: 3.0.0
@ -9923,7 +9966,7 @@ packages:
ts-node:
optional: true
dependencies:
lilconfig: 2.0.6
lilconfig: 2.1.0
postcss: 8.4.21
yaml: 1.10.2
dev: true
@ -10835,14 +10878,6 @@ packages:
define-properties: 1.2.0
functions-have-names: 1.2.3
/regexpp/3.2.0:
resolution:
{
integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==,
}
engines: { node: ">=8" }
dev: true
/regexpu-core/5.3.1:
resolution:
{
@ -11158,24 +11193,24 @@ packages:
get-assigned-identifiers: 1.2.0
dev: false
/semantic-release/20.1.0:
/semantic-release/20.1.1:
resolution:
{
integrity: sha512-+9+n6RIr0Fz0F53cXrjpawxWlUg3O7/qr1jF9lrE+/v6WqwBrSWnavVHTPaf2WLerET2EngoqI0M4pahkKl6XQ==,
integrity: sha512-jXDr8y7ozo42N4+G9m/P5Qyx5oQO4aOS66a+Up8XECzEOFIpEoo3ngnr4R5lSix/sVJW69/fgNkOUZhsGFiQ5g==,
}
engines: { node: ">=18" }
hasBin: true
dependencies:
"@semantic-release/commit-analyzer": 9.0.2_semantic-release@20.1.0
"@semantic-release/commit-analyzer": 9.0.2_semantic-release@20.1.1
"@semantic-release/error": 3.0.0
"@semantic-release/github": 8.0.7_semantic-release@20.1.0
"@semantic-release/npm": 9.0.2_semantic-release@20.1.0
"@semantic-release/release-notes-generator": 10.0.3_semantic-release@20.1.0
"@semantic-release/github": 8.0.7_semantic-release@20.1.1
"@semantic-release/npm": 9.0.2_semantic-release@20.1.1
"@semantic-release/release-notes-generator": 10.0.3_semantic-release@20.1.1
aggregate-error: 4.0.1
cosmiconfig: 8.0.0
debug: 4.3.4
env-ci: 8.0.0
execa: 6.1.0
execa: 7.1.0
figures: 5.0.0
find-versions: 5.1.0
get-stream: 6.0.1
@ -12504,7 +12539,7 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
/vite-plugin-pwa/0.14.4_rcpzravakhu7gk56p6427hsr2y:
/vite-plugin-pwa/0.14.4_vizhyq4kcdharmiplw7eejneda:
resolution:
{
integrity: sha512-M7Ct0so8OlouMkTWgXnl8W1xU95glITSKIe7qswZf1tniAstO2idElGCnsrTJ5NPNSx1XqfTCOUj8j94S6FD7Q==,
@ -12518,17 +12553,17 @@ packages:
fast-glob: 3.2.12
pretty-bytes: 6.1.0
rollup: 3.17.2
vite: 4.1.3
vite: 4.1.4
workbox-build: 6.5.4
workbox-window: 6.5.4
transitivePeerDependencies:
- supports-color
dev: true
/vite/4.1.3:
/vite/4.1.4:
resolution:
{
integrity: sha512-0Zqo4/Fr/swSOBmbl+HAAhOjrqNwju+yTtoe4hQX9UsARdcuc9njyOdr6xU0DDnV7YP0RT6mgTTOiRtZgxfCxA==,
integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==,
}
engines: { node: ^14.18.0 || >=16.0.0 }
hasBin: true
@ -12568,10 +12603,10 @@ packages:
}
dev: false
/wavesurfer.js/6.4.0:
/wavesurfer.js/6.5.2:
resolution:
{
integrity: sha512-+i9GkYdMTm998goOWvycWqCgxfbolnuGTPgrdGss7ZbQDuu+n6AHdnJwYnqxW8RZo5A2ck6ztzXbrXGuCi0m5g==,
integrity: sha512-1GfjeFlaaYnlOcwZ3M0MjYgmAVzL4dKARfJIlM9L/NVECFRwMsV7wtOWA1ZBukjFABt+DL+JiZOEIAtomqSMJg==,
}
dev: false
@ -12958,20 +12993,20 @@ packages:
word: 0.3.0
dev: false
/xml-formatter/3.2.0:
/xml-formatter/3.3.2:
resolution:
{
integrity: sha512-PYROODIUDHz1SDFePg2VThajPOuSmvo/PrYRKARcSc9xxKKs62EN9uar60IIxxknzmOSNDAxlylpw34bQp0g/Q==,
integrity: sha512-ld34F1b7+2UQGNkfsAV4MN3/b7cdUstyMj3XJhzKFasOPtMToVCkqmrNcmrRuSlPxgH1K9tXPkqr75gAT3ix2g==,
}
engines: { node: ">= 14" }
dependencies:
xml-parser-xo: 4.0.2
xml-parser-xo: 4.0.5
dev: false
/xml-parser-xo/4.0.2:
/xml-parser-xo/4.0.5:
resolution:
{
integrity: sha512-tM9LyyGumFAf7VD3GLlcN7eIbpvgzmt7PAseAMO6thgEq6VIEHPxlcWVIzMmn6pqGD1NTZS8mSPfhePo6AETVw==,
integrity: sha512-UWXOHMQ4ySxpUiU3S/9KzPOhninlL8SN1xFfWgX9WjgoZWoLKtEeJIEz4jhKtdFsoZBCYjg9rDEP3qfnpiHagQ==,
}
engines: { node: ">= 14" }
dev: false

View File

View File

View File

@ -11,6 +11,7 @@ use Rector\CodingStyle\Rector\String_\SymplifyQuoteEscapeRector;
use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersion;
use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector;
use Rector\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector;
use Rector\EarlyReturn\Rector\If_\ChangeAndIfToEarlyReturnRector;
use Rector\EarlyReturn\Rector\If_\ChangeOrIfContinueToMultiContinueRector;
use Rector\EarlyReturn\Rector\If_\ChangeOrIfReturnToEarlyReturnRector;
@ -66,6 +67,10 @@ return static function (RectorConfig $rectorConfig): void {
NewlineAfterStatementRector::class => [__DIR__ . '/app/Views'],
RemoveUnreachableStatementRector::class => [
__DIR__ . '/modules/Install/Controllers/InstallController.php',
],
ChangeAndIfToEarlyReturnRector::class => [__DIR__ . '/modules/Install/Controllers/InstallController.php'],
]);

View File

@ -18,7 +18,7 @@
<img slot="preview_image" src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->cover->description ?>" loading="lazy" />
</video-clip-previewer>
<audio-clipper start-time="<?= old('start_time', 0) ?>" duration="<?= old('duration', 30) ?>" min-duration="10" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>">
<audio slot="audio" src="<?= $episode->audio_url ?>" preload="auto">
<audio slot="audio" src="<?= $episode->audio->file_url ?>" preload="auto">
Your browser does not support the <code>audio</code> element.
</audio>
<input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />

View File

@ -78,7 +78,6 @@
subtitle="<?= lang('Settings.housekeeping.subtitle') ?>" >
<Forms.Toggler name="reset_counts" value="yes" size="small" checked="false" hint="<?= lang('Settings.housekeeping.reset_counts_helper') ?>"><?= lang('Settings.housekeeping.reset_counts') ?></Forms.Toggler>
<Forms.Toggler name="rewrite_media" value="yes" size="small" checked="false" hint="<?= lang('Settings.housekeeping.rewrite_media_helper') ?>"><?= lang('Settings.housekeeping.rewrite_media') ?></Forms.Toggler>
<Forms.Toggler name="rename_episodes_files" value="yes" size="small" checked="false" hint="<?= lang('Settings.housekeeping.rename_episodes_files_hint') ?>"><?= lang('Settings.housekeeping.rename_episodes_files') ?></Forms.Toggler>
<Forms.Toggler name="clear_cache" value="yes" size="small" checked="false" hint="<?= lang('Settings.housekeeping.clear_cache_helper') ?>"><?= lang('Settings.housekeeping.clear_cache') ?></Forms.Toggler>