diff --git a/Dockerfile b/Dockerfile index 723d1bc2..0b3f2e6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,6 +50,8 @@ RUN apt-get update \ # gd for image processing && docker-php-ext-configure gd --with-webp --with-jpeg --with-freetype \ && docker-php-ext-install gd \ + && docker-php-ext-install exif \ + && docker-php-ext-enable exif \ # redis extension for cache && pecl install -o -f redis \ && rm -rf /tmp/pear \ diff --git a/app/Controllers/MapController.php b/app/Controllers/MapController.php index c4a6cd4b..785a1352 100644 --- a/app/Controllers/MapController.php +++ b/app/Controllers/MapController.php @@ -46,7 +46,7 @@ class MapController extends BaseController 'location_url' => $episode->location->url, 'episode_link' => $episode->link, 'podcast_link' => $episode->podcast->link, - 'cover_path' => $episode->cover->thumbnail_url, + 'cover_url' => $episode->cover->thumbnail_url, 'podcast_title' => $episode->podcast->title, 'episode_title' => $episode->title, ]; diff --git a/app/Database/Migrations/2020-05-29-120000_add_media.php b/app/Database/Migrations/2020-05-29-120000_add_media.php new file mode 100644 index 00000000..711ba069 --- /dev/null +++ b/app/Database/Migrations/2020-05-29-120000_add_media.php @@ -0,0 +1,83 @@ +forge->addField([ + 'id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'file_path' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + 'file_size' => [ + 'type' => 'INT', + 'unsigned' => true, + 'comment' => 'File size in bytes', + ], + 'file_content_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 45, + ], + 'file_metadata' => [ + 'type' => 'JSON', + 'nullable' => true, + ], + 'type' => [ + 'type' => 'ENUM', + 'constraint' => ['image', 'audio', 'video', 'transcript', 'chapters', 'document'], + ], + 'description' => [ + 'type' => 'TEXT', + ], + 'language_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 2, + ], + 'uploaded_by' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'updated_by' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'uploaded_at' => [ + 'type' => 'DATETIME', + ], + 'updated_at' => [ + 'type' => 'DATETIME', + ], + 'deleted_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->addForeignKey('uploaded_by', 'users', 'id'); + $this->forge->addForeignKey('updated_by', 'users', 'id'); + $this->forge->createTable('media'); + } + + public function down(): void + { + $this->forge->dropTable('media'); + } +} diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php index 0663392a..ff2f913c 100644 --- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php +++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php @@ -46,25 +46,13 @@ class AddPodcasts extends Migration 'description_html' => [ 'type' => 'TEXT', ], - 'cover_path' => [ - 'type' => 'VARCHAR', - 'constraint' => 255, + 'cover_id' => [ + 'type' => 'INT', + 'unsigned' => true, ], - // constraint is 13 because the longest safe mimetype for images is image/svg+xml, - // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types - 'cover_mimetype' => [ - 'type' => 'VARCHAR', - 'constraint' => 13, - ], - 'banner_path' => [ - 'type' => 'VARCHAR', - 'constraint' => 255, - 'null' => true, - 'default' => null, - ], - 'banner_mimetype' => [ - 'type' => 'VARCHAR', - 'constraint' => 13, + 'banner_id' => [ + 'type' => 'INT', + 'unsigned' => true, 'null' => true, 'default' => null, ], @@ -209,6 +197,8 @@ class AddPodcasts extends Migration $this->forge->addUniqueKey('guid'); $this->forge->addUniqueKey('actor_id'); $this->forge->addForeignKey('actor_id', config('Fediverse')->tablesPrefix . 'actors', 'id', '', 'CASCADE'); + $this->forge->addForeignKey('cover_id', 'media', 'id'); + $this->forge->addForeignKey('banner_id', 'media', 'id'); $this->forge->addForeignKey('category_id', 'categories', 'id'); $this->forge->addForeignKey('language_code', 'languages', 'code'); $this->forge->addForeignKey('created_by', 'users', 'id'); diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php index aea2a023..04656978 100644 --- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php +++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php @@ -40,29 +40,9 @@ class AddEpisodes extends Migration 'type' => 'VARCHAR', 'constraint' => 128, ], - 'audio_file_path' => [ - 'type' => 'VARCHAR', - 'constraint' => 255, - ], - 'audio_file_duration' => [ - // exact value for duration with max 99999,999 ~ 27.7 hours - 'type' => 'DECIMAL(8,3)', - 'unsigned' => true, - 'comment' => 'Playtime in seconds', - ], - 'audio_file_mimetype' => [ - 'type' => 'VARCHAR', - 'constraint' => 255, - ], - 'audio_file_size' => [ + 'audio_id' => [ 'type' => 'INT', 'unsigned' => true, - 'comment' => 'File size in bytes', - ], - 'audio_file_header_size' => [ - 'type' => 'INT', - 'unsigned' => true, - 'comment' => 'Header size in bytes', ], 'description_markdown' => [ 'type' => 'TEXT', @@ -70,34 +50,27 @@ class AddEpisodes extends Migration 'description_html' => [ 'type' => 'TEXT', ], - 'cover_path' => [ - 'type' => 'VARCHAR', - 'constraint' => 255, + 'cover_id' => [ + 'type' => 'INT', + 'unsigned' => true, 'null' => true, ], - // constraint is 13 because the longest safe mimetype for images is image/svg+xml, - // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types - 'cover_mimetype' => [ - 'type' => 'VARCHAR', - 'constraint' => 13, + 'transcript_id' => [ + 'type' => 'INT', + 'unsigned' => true, 'null' => true, ], - 'transcript_file_path' => [ - 'type' => 'VARCHAR', - 'constraint' => 255, - 'null' => true, - ], - 'transcript_file_remote_url' => [ + 'transcript_remote_url' => [ 'type' => 'VARCHAR', 'constraint' => 512, 'null' => true, ], - 'chapters_file_path' => [ - 'type' => 'VARCHAR', - 'constraint' => 255, + 'chapters_id' => [ + 'type' => 'INT', + 'unsigned' => true, 'null' => true, ], - 'chapters_file_remote_url' => [ + 'chapters_remote_url' => [ 'type' => 'VARCHAR', 'constraint' => 512, 'null' => true, @@ -183,6 +156,10 @@ class AddEpisodes extends Migration $this->forge->addPrimaryKey('id'); $this->forge->addUniqueKey(['podcast_id', 'slug']); $this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE'); + $this->forge->addForeignKey('audio_id', 'media', 'id'); + $this->forge->addForeignKey('cover_id', 'media', 'id'); + $this->forge->addForeignKey('transcript_id', 'media', 'id'); + $this->forge->addForeignKey('chapters_id', 'media', 'id'); $this->forge->addForeignKey('created_by', 'users', 'id'); $this->forge->addForeignKey('updated_by', 'users', 'id'); $this->forge->createTable('episodes'); diff --git a/app/Database/Migrations/2020-12-25-120000_add_persons.php b/app/Database/Migrations/2020-12-25-120000_add_persons.php index 58e26df3..66b53ba1 100644 --- a/app/Database/Migrations/2020-12-25-120000_add_persons.php +++ b/app/Database/Migrations/2020-12-25-120000_add_persons.php @@ -42,16 +42,9 @@ class AddPersons extends Migration 'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.', 'null' => true, ], - 'avatar_path' => [ - 'type' => 'VARCHAR', - 'constraint' => 255, - 'null' => true, - ], - // constraint is 13 because the longest safe mimetype for images is image/svg+xml, - // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types - 'avatar_mimetype' => [ - 'type' => 'VARCHAR', - 'constraint' => 13, + 'avatar_id' => [ + 'type' => 'INT', + 'unsigned' => true, 'null' => true, ], 'created_by' => [ @@ -71,6 +64,7 @@ class AddPersons extends Migration ]); $this->forge->addKey('id', true); + $this->forge->addForeignKey('avatar_id', 'media', 'id'); $this->forge->addForeignKey('created_by', 'users', 'id'); $this->forge->addForeignKey('updated_by', 'users', 'id'); $this->forge->createTable('persons'); diff --git a/app/Database/Migrations/2020-06-05-180000_add_soundbites.php b/app/Database/Migrations/2021-12-09-130000_add_clips.php similarity index 71% rename from app/Database/Migrations/2020-06-05-180000_add_soundbites.php rename to app/Database/Migrations/2021-12-09-130000_add_clips.php index 90573d51..068c66b3 100644 --- a/app/Database/Migrations/2020-06-05-180000_add_soundbites.php +++ b/app/Database/Migrations/2021-12-09-130000_add_clips.php @@ -3,9 +3,7 @@ declare(strict_types=1); /** - * Class AddSoundbites Creates soundbites table in database - * - * @copyright 2020 Podlibre + * @copyright 2021 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ @@ -14,7 +12,7 @@ namespace App\Database\Migrations; use CodeIgniter\Database\Migration; -class AddSoundbites extends Migration +class AddClips extends Migration { public function up(): void { @@ -37,7 +35,7 @@ class AddSoundbites extends Migration 'unsigned' => true, ], 'duration' => [ - // soundbite duration cannot be higher than 9999,999 seconds ~ 2.77 hours + // clip duration cannot be higher than 9999,999 seconds ~ 2.77 hours 'type' => 'DECIMAL(7,3)', 'unsigned' => true, ], @@ -46,6 +44,21 @@ class AddSoundbites extends Migration 'constraint' => 128, 'null' => true, ], + 'type' => [ + 'type' => 'ENUM', + 'constraint' => ['audio', 'video'], + ], + 'media_id' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'status' => [ + 'type' => 'ENUM', + 'constraint' => ['queued', 'pending', 'generating', 'passed', 'failed'], + ], + 'logs' => [ + 'type' => 'TEXT', + ], 'created_by' => [ 'type' => 'INT', 'unsigned' => true, @@ -65,17 +78,19 @@ class AddSoundbites extends Migration 'null' => true, ], ]); + $this->forge->addKey('id', true); - $this->forge->addUniqueKey(['episode_id', 'start_time', 'duration']); + $this->forge->addUniqueKey(['episode_id', 'start_time', 'duration', 'type']); $this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE'); $this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE'); + $this->forge->addForeignKey('media_id', 'media', 'id', '', 'CASCADE'); $this->forge->addForeignKey('created_by', 'users', 'id'); $this->forge->addForeignKey('updated_by', 'users', 'id'); - $this->forge->createTable('soundbites'); + $this->forge->createTable('clips'); } public function down(): void { - $this->forge->dropTable('soundbites'); + $this->forge->dropTable('clips'); } } diff --git a/app/Entities/Audio.php b/app/Entities/Audio.php new file mode 100644 index 00000000..4a342d41 --- /dev/null +++ b/app/Entities/Audio.php @@ -0,0 +1,51 @@ +|null $data + */ + public function __construct(array $data = null) + { + parent::__construct($data); + + if ($this->file_metadata) { + $this->duration = (float) $this->file_metadata['playtime_seconds']; + $this->header_size = (int) $this->file_metadata['avdataoffset']; + } + } + + public function setFile(File $file): self + { + parent::setFile($file); + + $getID3 = new GetID3(); + $audioMetadata = $getID3->analyze((string) $file); + + $this->attributes['file_content_type'] = $audioMetadata['mimetype']; + $this->attributes['file_size'] = $audioMetadata['filesize']; + $this->attributes['description'] = $audioMetadata['comments']['comment']; + $this->attributes['file_metadata'] = $audioMetadata; + + return $this; + } +} diff --git a/app/Entities/Soundbite.php b/app/Entities/Clip.php similarity index 84% rename from app/Entities/Soundbite.php rename to app/Entities/Clip.php index f6e85cfd..550cf403 100644 --- a/app/Entities/Soundbite.php +++ b/app/Entities/Clip.php @@ -22,7 +22,7 @@ use CodeIgniter\Entity\Entity; * @property int $created_by * @property int $updated_by */ -class Soundbite extends Entity +class Clip extends Entity { /** * @var array @@ -33,7 +33,11 @@ class Soundbite extends Entity 'episode_id' => 'integer', 'start_time' => 'double', 'duration' => 'double', + 'type' => 'string', 'label' => '?string', + 'media_id' => 'integer', + 'status' => 'string', + 'logs' => 'string', 'created_by' => 'integer', 'updated_by' => 'integer', ]; diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 711e2bc7..b5ca1846 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -11,14 +11,14 @@ declare(strict_types=1); namespace App\Entities; use App\Libraries\SimpleRSSElement; +use App\Models\ClipsModel; use App\Models\EpisodeCommentModel; +use App\Models\MediaModel; use App\Models\PersonModel; use App\Models\PodcastModel; use App\Models\PostModel; -use App\Models\SoundbiteModel; use CodeIgniter\Entity\Entity; use CodeIgniter\Files\File; -use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\I18n\Time; use League\CommonMark\CommonMarkConverter; use RuntimeException; @@ -31,30 +31,22 @@ use RuntimeException; * @property string $guid * @property string $slug * @property string $title - * @property File $audio_file - * @property string $audio_file_url + * @property int $audio_id + * @property Audio $audio * @property string $audio_file_analytics_url * @property string $audio_file_web_url * @property string $audio_file_opengraph_url - * @property string $audio_file_path - * @property double $audio_file_duration - * @property string $audio_file_mimetype - * @property int $audio_file_size - * @property int $audio_file_header_size * @property string|null $description Holds text only description, striped of any markdown or html special characters * @property string $description_markdown * @property string $description_html + * @property int $cover_id * @property Image $cover - * @property string|null $cover_path - * @property string|null $cover_mimetype - * @property File|null $transcript_file - * @property string|null $transcript_file_url - * @property string|null $transcript_file_path - * @property string|null $transcript_file_remote_url - * @property File|null $chapters_file - * @property string|null $chapters_file_url - * @property string|null $chapters_file_path - * @property string|null $chapters_file_remote_url + * @property int|null $transcript_id + * @property Media|null $transcript + * @property string|null $transcript_remote_url + * @property int|null $chapters_id + * @property Media|null $chapters + * @property string|null $chapters_remote_url * @property string|null $parental_advisory * @property int $number * @property int $season_number @@ -86,15 +78,15 @@ class Episode extends Entity protected string $link; - protected File $audio_file; + protected Audio $audio; - protected string $audio_file_url; + protected string $audio_url; - protected string $audio_file_analytics_url; + protected string $audio_analytics_url; - protected string $audio_file_web_url; + protected string $audio_web_url; - protected string $audio_file_opengraph_url; + protected string $audio_opengraph_url; protected string $embed_url; @@ -102,9 +94,9 @@ class Episode extends Entity protected ?string $description = null; - protected File $transcript_file; + protected ?Media $transcript; - protected File $chapters_file; + protected ?Media $chapters; /** * @var Person[]|null @@ -112,9 +104,9 @@ class Episode extends Entity protected ?array $persons = null; /** - * @var Soundbite[]|null + * @var Clip[]|null */ - protected ?array $soundbites = null; + protected ?array $clips = null; /** * @var Post[]|null @@ -146,19 +138,14 @@ class Episode extends Entity 'guid' => 'string', 'slug' => 'string', 'title' => 'string', - 'audio_file_path' => 'string', - 'audio_file_duration' => 'double', - 'audio_file_mimetype' => 'string', - 'audio_file_size' => 'integer', - 'audio_file_header_size' => 'integer', + 'audio_id' => 'integer', 'description_markdown' => 'string', 'description_html' => 'string', - 'cover_path' => '?string', - 'cover_mimetype' => '?string', - 'transcript_file_path' => '?string', - 'transcript_file_remote_url' => '?string', - 'chapters_file_path' => '?string', - 'chapters_file_remote_url' => '?string', + 'cover_id' => '?integer', + 'transcript_id' => '?integer', + 'transcript_remote_url' => '?string', + 'chapters_id' => '?integer', + 'chapters_remote_url' => '?string', 'parental_advisory' => '?string', 'number' => '?integer', 'season_number' => '?integer', @@ -199,108 +186,45 @@ class Episode extends Entity public function getCover(): Image { - if ($coverPath = $this->attributes['cover_path']) { - return new Image(null, $coverPath, $this->attributes['cover_mimetype'], config( - 'Images' - )->podcastCoverSizes); + if (! $this->cover instanceof Image) { + $this->cover = (new MediaModel('image'))->getMediaById($this->cover_id); } - return $this->getPodcast() - ->cover; + return $this->cover; } - /** - * Saves an audio file - */ - public function setAudioFile(UploadedFile | File $audioFile): static + public function getAudio(): Audio { - helper(['media', 'id3']); - - $audioMetadata = get_file_tags($audioFile); - - $this->attributes['audio_file_path'] = save_media( - $audioFile, - 'podcasts/' . $this->getPodcast()->handle, - $this->attributes['slug'], - ); - $this->attributes['audio_file_duration'] = - $audioMetadata['playtime_seconds']; - $this->attributes['audio_file_mimetype'] = $audioMetadata['mime_type']; - $this->attributes['audio_file_size'] = $audioMetadata['filesize']; - $this->attributes['audio_file_header_size'] = - $audioMetadata['avdataoffset']; - - return $this; - } - - /** - * Saves an episode transcript file - */ - public function setTranscriptFile(UploadedFile | File $transcriptFile): static - { - helper('media'); - - $this->attributes['transcript_file_path'] = save_media( - $transcriptFile, - 'podcasts/' . $this->getPodcast() - ->handle, - $this->attributes['slug'] . '-transcript', - ); - - return $this; - } - - /** - * Saves an episode chapters file - */ - public function setChaptersFile(UploadedFile | File $chaptersFile): static - { - helper('media'); - - $this->attributes['chapters_file_path'] = save_media( - $chaptersFile, - 'podcasts/' . $this->getPodcast() - ->handle, - $this->attributes['slug'] . '-chapters', - ); - - return $this; - } - - public function getAudioFile(): File - { - helper('media'); - - return new File(media_path($this->audio_file_path)); - } - - public function getTranscriptFile(): ?File - { - if ($this->attributes['transcript_file_path']) { - helper('media'); - - return new File(media_path($this->attributes['transcript_file_path'])); + if (! $this->audio) { + $this->audio = (new MediaModel('audio'))->getMediaById($this->audio_id); } - return null; + return $this->audio; } - public function getChaptersFile(): ?File + public function getTranscript(): ?Media { - if ($this->attributes['chapters_file_path']) { - helper('media'); - - return new File(media_path($this->attributes['chapters_file_path'])); + if ($this->transcript_id !== null && $this->transcript === null) { + $this->transcript = (new MediaModel('document'))->getMediaById($this->transcript_id); } - return null; + return $this->transcript; + } + + public function getChaptersFile(): ?Media + { + if ($this->chapters_id !== null && $this->chapters === null) { + $this->chapters = (new MediaModel('document'))->getMediaById($this->chapters_id); + } + + return $this->chapters; } public function getAudioFileUrl(): string { helper('media'); - return media_base_url($this->audio_file_path); + return media_base_url($this->audio->file_path); } public function getAudioFileAnalyticsUrl(): string @@ -308,15 +232,15 @@ class Episode extends Entity helper('analytics'); // remove 'podcasts/' from audio file path - $strippedAudioFilePath = substr($this->audio_file_path, 9); + $strippedAudioFilePath = substr($this->audio->file_path, 9); return generate_episode_analytics_url( $this->podcast_id, $this->id, $strippedAudioFilePath, - $this->audio_file_duration, - $this->audio_file_size, - $this->audio_file_header_size, + $this->audio->duration, + $this->audio->file_size, + $this->audio->header_size, $this->published_at, ); } @@ -332,28 +256,26 @@ class Episode extends Entity } /** - * Gets transcript url from transcript file uri if it exists or returns the transcript_file_remote_url which can be - * null. + * Gets transcript url from transcript file uri if it exists or returns the transcript_remote_url which can be null. */ - public function getTranscriptFileUrl(): ?string + public function getTranscriptUrl(): ?string { - if ($this->attributes['transcript_file_path']) { - return media_base_url($this->attributes['transcript_file_path']); + if ($this->transcript !== null) { + return $this->transcript->url; } - return $this->attributes['transcript_file_remote_url']; + return $this->transcript_remote_url; } /** - * Gets chapters file url from chapters file uri if it exists or returns the chapters_file_remote_url which can be - * null. + * Gets chapters file url from chapters file uri if it exists or returns the chapters_remote_url which can be null. */ public function getChaptersFileUrl(): ?string { - if ($this->chapters_file_path) { - return media_base_url($this->chapters_file_path); + if ($this->chapters) { + return $this->chapters->url; } - return $this->chapters_file_remote_url; + return $this->chapters_remote_url; } /** @@ -375,21 +297,21 @@ class Episode extends Entity } /** - * Returns the episode’s soundbites + * Returns the episode’s clips * - * @return Soundbite[] + * @return Clip[] */ - public function getSoundbites(): array + public function getClips(): array { if ($this->id === null) { - throw new RuntimeException('Episode must be created before getting soundbites.'); + throw new RuntimeException('Episode must be created before getting clips.'); } - if ($this->soundbites === null) { - $this->soundbites = (new SoundbiteModel())->getEpisodeSoundbites($this->getPodcast() ->id, $this->id); + if ($this->clips === null) { + $this->clips = (new ClipsModel())->getEpisodeClips($this->getPodcast() ->id, $this->id); } - return $this->soundbites; + return $this->clips; } /** diff --git a/app/Entities/Image.php b/app/Entities/Image.php index be46133f..758e2fa4 100644 --- a/app/Entities/Image.php +++ b/app/Entities/Image.php @@ -10,176 +10,68 @@ declare(strict_types=1); namespace App\Entities; -use CodeIgniter\Entity\Entity; use CodeIgniter\Files\File; -use Config\Images; -use RuntimeException; -/** - * @property File|null $file - * @property string $dirname - * @property string $filename - * @property string $extension - * @property string $mimetype - * @property string $path - * @property string $url - */ -class Image extends Entity +class Image extends Media { - protected Images $config; - - protected File $file; - - protected string $dirname; - - protected string $filename; - - protected string $extension; - - protected string $mimetype; + protected string $type = 'image'; /** - * @var array> + * @param array|null $data */ - protected array $sizes = []; - - /** - * @param array> $sizes - * @param File $file - */ - public function __construct(?File $file, string $path = '', string $mimetype = '', array $sizes = []) + public function __construct(array $data = null) { - if ($file === null && $path === '') { - throw new RuntimeException('File or path must be set to create an Image.'); - } + parent::__construct($data); - $dirname = ''; - $filename = ''; - $extension = ''; - - if ($file !== null) { - $dirname = $file->getPath(); - $filename = $file->getBasename(); - $extension = $file->getExtension(); - $mimetype = $file->getMimeType(); - } - - if ($path !== '') { - [ - 'filename' => $filename, - 'dirname' => $dirname, - 'extension' => $extension, - ] = pathinfo($path); - } - - if ($file === null) { - helper('media'); - $file = new File(media_path($path)); - } - - $this->file = $file; - $this->dirname = $dirname; - $this->filename = $filename; - $this->extension = $extension; - $this->mimetype = $mimetype; - $this->sizes = $sizes; - } - - public function __get($property) - { - // Convert to CamelCase for the method - $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $property))); - - // if a get* method exists for this property, - // call that method to get this value. - // @phpstan-ignore-next-line - if (method_exists($this, $method)) { - return $this->{$method}(); - } - - $fileSuffix = ''; - if ($lastUnderscorePosition = strrpos($property, '_')) { - $fileSuffix = '_' . substr($property, 0, $lastUnderscorePosition); - } - - $path = ''; - if ($this->dirname !== '.') { - $path .= $this->dirname . '/'; - } - $path .= $this->filename . $fileSuffix; - - $extension = '.' . $this->extension; - $mimetype = $this->mimetype; - if ($fileSuffix !== '') { - $sizeName = substr($fileSuffix, 1); - if (array_key_exists('extension', $this->sizes[$sizeName])) { - $extension = '.' . $this->sizes[$sizeName]['extension']; - } - if (array_key_exists('mimetype', $this->sizes[$sizeName])) { - $mimetype = $this->sizes[$sizeName]['mimetype']; - } - } - $path .= $extension; - - if (str_ends_with($property, 'mimetype')) { - return $mimetype; - } - - if (str_ends_with($property, 'url')) { - helper('media'); - - return media_base_url($path); - } - - if (str_ends_with($property, 'path')) { - return $path; + if ($this->file_path && $this->file_metadata) { + $this->sizes = $this->file_metadata['sizes']; + $this->initSizeProperties(); } } - public function getMimetype(): string - { - return $this->mimetype; - } - - public function getFile(): File - { - return $this->file; - } - - /** - * @param array> $sizes - */ - public function saveImage(array $sizes, string $dirname, string $filename): void + public function initSizeProperties(): bool { helper('media'); - $this->dirname = $dirname; - $this->filename = $filename; - $this->sizes = $sizes; + $extension = $this->file_extension; + $mimetype = $this->mimetype; + foreach ($this->sizes as $name => $size) { + if (array_key_exists('extension', $size)) { + $extension = $size['extension']; + } + if (array_key_exists('mimetype', $size)) { + $mimetype = $size['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; + } - save_media($this->file, $this->dirname, $this->filename); + return true; + } + public function setFile(File $file): self + { + parent::setFile($file); + + $metadata = exif_read_data(media_path($this->file_path), null, true); + + if ($metadata) { + $metadata['sizes'] = $this->sizes; + $this->attributes['file_size'] = $metadata['FILE']['FileSize']; + $this->attributes['file_metadata'] = json_encode($metadata); + } + + // save derived sizes $imageService = service('image'); - - foreach ($sizes as $name => $size) { + foreach ($this->sizes as $name => $size) { $pathProperty = $name . '_path'; $imageService - ->withFile(media_path($this->path)) + ->withFile(media_path($this->file_path)) ->resize($size['width'], $size['height']); $imageService->save(media_path($this->{$pathProperty})); } - } - /** - * @param array $sizes - */ - public function delete(array $sizes): void - { - helper('media'); - - foreach (array_keys($sizes) as $name) { - $pathProperty = $name . '_path'; - unlink(media_path($this->{$pathProperty})); - } + return $this; } } diff --git a/app/Entities/ImageOLD.php b/app/Entities/ImageOLD.php new file mode 100644 index 00000000..d46b29e1 --- /dev/null +++ b/app/Entities/ImageOLD.php @@ -0,0 +1,123 @@ +> + */ + public array $sizes = []; + + protected Images $config; + + protected string $type = 'image'; + + public function __get($property) + { + if (str_ends_with($property, '_url') || str_ends_with($property, '_path') || str_ends_with( + $property, + '_mimetype' + )) { + $this->initSizeProperties(); + } + + parent::__get($property); + } + + public function setFileMetadata(string $metadata): self + { + $this->attributes['file_metadata'] = $metadata; + + $metadataArray = json_decode($metadata, true); + if (! array_key_exists('sizes', $metadataArray)) { + return $this; + } + + $this->sizes = $metadataArray['sizes']; + + return $this; + } + + public function initSizeProperties(): bool + { + if ($this->file_path === '') { + return false; + } + + if ($this->sizes === []) { + $this->sizes = $this->file_metadata['sizes']; + } + + helper('media'); + + $extension = $this->file_extension; + $mimetype = $this->mimetype; + foreach ($this->sizes as $name => $size) { + if (array_key_exists('extension', $size)) { + $extension = $size['extension']; + } + if (array_key_exists('mimetype', $size)) { + $mimetype = $size['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 saveInDisk(File $file, string $dirname, string $filename): void + { + // save original + parent::saveInDisk($file, $dirname, $filename); + + $this->initSizeProperties(); + + // 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})); + } + } + + public function injectFileData(File $file): void + { + $metadata = exif_read_data(media_path($this->file_path), null, true); + + if ($metadata) { + $metadata['sizes'] = $this->sizes; + $this->file_size = $metadata['FILE']['FileSize']; + $this->file_metadata = $metadata; + } + } + + /** + * @param array $sizes + */ + public function delete(array $sizes): void + { + helper('media'); + + foreach (array_keys($sizes) as $name) { + $pathProperty = $name . '_path'; + unlink(media_path($this->{$pathProperty})); + } + } +} diff --git a/app/Entities/Media.php b/app/Entities/Media.php new file mode 100644 index 00000000..b979edbc --- /dev/null +++ b/app/Entities/Media.php @@ -0,0 +1,95 @@ + + */ + protected $casts = [ + 'id' => 'integer', + 'file_path' => 'string', + 'file_size' => 'int', + 'file_content_type' => 'string', + 'file_metadata' => 'json-array', + 'type' => 'string', + 'description' => 'string', + 'language_code' => '?string', + 'uploaded_by' => 'integer', + 'updated_by' => 'integer', + ]; + + /** + * @param array|null $data + */ + public function __construct(array $data = null) + { + parent::__construct($data); + + if ($this->file_path) { + [ + 'filename' => $filename, + 'dirname' => $dirname, + 'extension' => $extension, + ] = pathinfo($this->file_path); + + $this->file_name = $filename; + $this->file_directory = $dirname; + $this->file_extension = $extension; + } + } + + public function setFile(File $file): self + { + helper('media'); + + $this->attributes['file_content_type'] = $file->getMimeType(); + $this->attributes['file_metadata'] = json_encode(lstat((string) $file)); + $this->attributes['file_path'] = save_media( + $file, + $this->attributes['file_directory'], + $this->attributes['file_name'] + ); + if ($filesize = filesize(media_path($this->file_path))) { + $this->attributes['file_size'] = $filesize; + } + + return $this; + } +} diff --git a/app/Entities/MediaOLD.php b/app/Entities/MediaOLD.php new file mode 100644 index 00000000..d585ad85 --- /dev/null +++ b/app/Entities/MediaOLD.php @@ -0,0 +1,93 @@ + + */ + protected $casts = [ + 'id' => 'integer', + 'file_path' => 'string', + 'file_size' => 'string', + 'file_content_type' => 'string', + 'file_metadata' => 'json-array', + 'type' => 'string', + 'description' => 'string', + 'language_code' => '?string', + 'uploaded_by' => 'integer', + 'updated_by' => 'integer', + ]; + + public function setFilePath(string $path): self + { + $this->attributes['file_path'] = $path; + + [ + 'filename' => $filename, + 'dirname' => $dirname, + 'extension' => $extension, + ] = pathinfo($path); + + $this->file_name = $filename; + $this->file_directory = $dirname; + $this->file_extension = $extension; + + return $this; + } + + public function saveInDisk(File $file, string $dirname, string $filename): void + { + helper('media'); + + $this->file_content_type = $file->getMimeType(); + + $filePath = save_media($file, $dirname, $filename); + + $this->file_path = $filePath; + } + + public function injectFileData(File $file): void + { + $this->file_content_type = $file->getMimeType(); + $this->type = 'document'; + + if ($filesize = filesize(media_path($this->file_path))) { + $this->file_size = $filesize; + } + } +} diff --git a/app/Entities/Person.php b/app/Entities/Person.php index 3204053a..10e9fd22 100644 --- a/app/Entities/Person.php +++ b/app/Entities/Person.php @@ -19,20 +19,15 @@ use RuntimeException; * @property string $full_name * @property string $unique_name * @property string|null $information_url + * @property int $avatar_id * @property Image $avatar - * @property string $avatar_path - * @property string $avatar_mimetype * @property int $created_by * @property int $updated_by * @property object[]|null $roles */ class Person extends Entity { - protected Image $avatar; - - protected ?int $podcast_id = null; - - protected ?int $episode_id = null; + protected ?Image $avatar = null; /** * @var object[]|null @@ -47,8 +42,7 @@ class Person extends Entity 'full_name' => 'string', 'unique_name' => 'string', 'information_url' => '?string', - 'avatar_path' => '?string', - 'avatar_mimetype' => '?string', + 'avatar_id' => '?int', 'podcast_id' => '?integer', 'episode_id' => '?integer', 'created_by' => 'integer', diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index 16d00f64..1663a6b3 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -13,6 +13,7 @@ namespace App\Entities; use App\Libraries\SimpleRSSElement; use App\Models\CategoryModel; use App\Models\EpisodeModel; +use App\Models\MediaModel; use App\Models\PersonModel; use App\Models\PlatformModel; use App\Models\UserModel; @@ -34,12 +35,10 @@ use RuntimeException; * @property string|null $description Holds text only description, striped of any markdown or html special characters * @property string $description_markdown * @property string $description_html + * @property int $cover_id * @property Image $cover - * @property string $cover_path - * @property string $cover_mimetype + * @property int|null $banner_id * @property Image|null $banner - * @property string|null $banner_path - * @property string|null $banner_mimetype * @property string $language_code * @property int $category_id * @property Category|null $category @@ -87,9 +86,9 @@ class Podcast extends Entity protected ?Actor $actor = null; - protected Image $cover; + protected ?Image $cover = null; - protected ?Image $banner; + protected ?Image $banner = null; protected ?string $description = null; @@ -150,10 +149,8 @@ class Podcast extends Entity 'title' => 'string', 'description_markdown' => 'string', 'description_html' => 'string', - 'cover_path' => 'string', - 'cover_mimetype' => 'string', - 'banner_path' => '?string', - 'banner_mimetype' => '?string', + 'cover_id' => 'int', + 'banner_id' => '?int', 'language_code' => 'string', 'category_id' => 'integer', 'parental_advisory' => '?string', @@ -195,66 +192,36 @@ class Podcast extends Entity return $this->actor; } - /** - * Saves a podcast cover to the corresponding podcast folder in `public/media/podcast_name/` - */ - public function setCover(Image $cover): static - { - // Save image - $cover->saveImage(config('Images')->podcastCoverSizes, 'podcasts/' . $this->attributes['handle'], 'cover'); - - $this->attributes['cover_path'] = $cover->path; - $this->attributes['cover_mimetype'] = $cover->mimetype; - - return $this; - } - public function getCover(): Image { - return new Image(null, $this->cover_path, $this->cover_mimetype, config('Images')->podcastCoverSizes); - } - - /** - * Saves a podcast cover to the corresponding podcast folder in `public/media/podcast_name/` - */ - public function setBanner(?Image $banner): static - { - if ($banner === null) { - $this->attributes['banner_path'] = null; - $this->attributes['banner_mimetype'] = null; - - return $this; + if (! $this->cover instanceof Image) { + $this->cover = (new MediaModel('image'))->getMediaById($this->cover_id); } - // Save image - $banner->saveImage( - config('Images') - ->podcastBannerSizes, - 'podcasts/' . $this->attributes['handle'], - 'banner' - ); - - $this->attributes['banner_path'] = $banner->path; - $this->attributes['banner_mimetype'] = $banner->mimetype; - - return $this; + return $this->cover; } public function getBanner(): Image { - if ($this->attributes['banner_path'] === null) { - return new Image( - null, - config('Images') + if ($this->banner_id === null) { + return new Image([ + 'file_path' => config('Images') ->podcastBannerDefaultPath, - config('Images') + 'file_mimetype' => config('Images') ->podcastBannerDefaultMimeType, - config('Images') - ->podcastBannerSizes - ); + 'file_size' => 0, + 'file_metadata' => [ + 'sizes' => config('Images') + ->podcastBannerSizes, + ], + ]); } - return new Image(null, $this->banner_path, $this->banner_mimetype, config('Images') ->podcastBannerSizes); + if (! $this->banner instanceof Image) { + $this->banner = (new MediaModel('image'))->getMediaById($this->banner_id); + } + + return $this->banner; } public function getLink(): string diff --git a/app/Helpers/id3_helper.php b/app/Helpers/id3_helper.php index c975f54b..abf89a61 100644 --- a/app/Helpers/id3_helper.php +++ b/app/Helpers/id3_helper.php @@ -10,29 +10,8 @@ declare(strict_types=1); use App\Entities\Episode; use CodeIgniter\Files\File; -use JamesHeinrich\GetID3\GetID3; use JamesHeinrich\GetID3\WriteTags; -if (! function_exists('get_file_tags')) { - /** - * Gets audio file metadata and ID3 info - * - * @return array - */ - function get_file_tags(File $file): array - { - $getID3 = new GetID3(); - $FileInfo = $getID3->analyze((string) $file); - - return [ - 'filesize' => $FileInfo['filesize'], - 'mime_type' => $FileInfo['mime_type'], - 'avdataoffset' => $FileInfo['avdataoffset'], - 'playtime_seconds' => $FileInfo['playtime_seconds'], - ]; - } -} - if (! function_exists('write_audio_file_tags')) { /** * Write audio file metadata / ID3 tags @@ -45,7 +24,7 @@ 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 = media_path($episode->audio->file_path); // set various options (optional) $tagwriter->tagformats = ['id3v2.4']; diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php index adb7d3bd..fbb845d8 100644 --- a/app/Helpers/rss_helper.php +++ b/app/Helpers/rss_helper.php @@ -211,8 +211,8 @@ if (! function_exists('get_rss_feed')) { ? '' : '?_from=' . urlencode($serviceSlug)), ); - $enclosure->addAttribute('length', (string) $episode->audio_file_size); - $enclosure->addAttribute('type', $episode->audio_file_mimetype); + $enclosure->addAttribute('length', (string) $episode->audio->file_size); + $enclosure->addAttribute('type', $episode->audio->file_content_type); $item->addChild('guid', $episode->guid); $item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123)); @@ -230,7 +230,7 @@ if (! function_exists('get_rss_feed')) { } } $item->addChildWithCDATA('description', $episode->getDescriptionHtml($serviceSlug)); - $item->addChild('duration', (string) $episode->audio_file_duration, $itunesNamespace); + $item->addChild('duration', (string) $episode->audio->duration, $itunesNamespace); $item->addChild('link', $episode->link); $episodeItunesImage = $item->addChild('image', null, $itunesNamespace); $episodeItunesImage->addAttribute('href', $episode->cover->feed_url); @@ -255,7 +255,7 @@ if (! function_exists('get_rss_feed')) { $comments->addAttribute('uri', url_to('episode-comments', $podcast->handle, $episode->slug)); $comments->addAttribute('contentType', 'application/podcast-activity+json'); - if ($episode->transcript_file_url) { + if ($episode->transcript->file_url) { $transcriptElement = $item->addChild('transcript', null, $podcastNamespace); $transcriptElement->addAttribute('url', $episode->transcript_file_url); $transcriptElement->addAttribute( @@ -267,16 +267,17 @@ if (! function_exists('get_rss_feed')) { $transcriptElement->addAttribute('language', $podcast->language_code); } - if ($episode->chapters_file_url) { + if ($episode->chapters->file_url) { $chaptersElement = $item->addChild('chapters', null, $podcastNamespace); $chaptersElement->addAttribute('url', $episode->chapters_file_url); $chaptersElement->addAttribute('type', 'application/json+chapters'); } - foreach ($episode->soundbites as $soundbite) { - $soundbiteElement = $item->addChild('soundbite', $soundbite->label, $podcastNamespace); - $soundbiteElement->addAttribute('start_time', (string) $soundbite->start_time); - $soundbiteElement->addAttribute('duration', (string) $soundbite->duration); + foreach ($episode->clip as $clip) { + // TODO: differentiate video from soundbites? + $soundbiteElement = $item->addChild('soundbite', $clip->label, $podcastNamespace); + $soundbiteElement->addAttribute('start_time', (string) $clip->start_time); + $soundbiteElement->addAttribute('duration', (string) $clip->duration); } foreach ($episode->persons as $person) { diff --git a/app/Helpers/seo_helper.php b/app/Helpers/seo_helper.php index c392d0d5..72b0c790 100644 --- a/app/Helpers/seo_helper.php +++ b/app/Helpers/seo_helper.php @@ -64,9 +64,9 @@ if (! function_exists('get_episode_metatags')) { 'image' => $episode->cover->feed_url, 'description' => $episode->description, 'datePublished' => $episode->published_at->format(DATE_ISO8601), - 'timeRequired' => iso8601_duration($episode->audio_file_duration), + 'timeRequired' => iso8601_duration($episode->audio->duration), 'associatedMedia' => new Thing('MediaObject', [ - 'contentUrl' => $episode->audio_file_url, + 'contentUrl' => $episode->audio->file_url, ]), 'partOfSeries' => new Thing('PodcastSeries', [ 'name' => $episode->podcast->title, @@ -87,7 +87,7 @@ if (! function_exists('get_episode_metatags')) { ->og('image:height', (string) config('Images')->podcastCoverSizes['large']['height']) ->og('locale', $episode->podcast->language_code) ->og('audio', $episode->audio_file_opengraph_url) - ->og('audio:type', $episode->audio_file_mimetype) + ->og('audio:type', $episode->audio->file_content_type) ->meta('article:published_time', $episode->published_at->format(DATE_ISO8601)) ->meta('article:modified_time', $episode->updated_at->format(DATE_ISO8601)) ->twitter('audio:partner', $episode->podcast->publisher ?? '') diff --git a/app/Libraries/MediaClipper/VideoClip.php b/app/Libraries/MediaClipper/VideoClip.php index d1f11de5..647e8545 100644 --- a/app/Libraries/MediaClipper/VideoClip.php +++ b/app/Libraries/MediaClipper/VideoClip.php @@ -79,10 +79,10 @@ class VideoClip helper(['media']); - $this->audioInput = media_path($this->episode->audio_file_path); + $this->audioInput = media_path($this->episode->audio->file_path); $this->episodeCoverPath = media_path($this->episode->cover->path); - if ($this->episode->transcript_file_path !== null) { - $this->subtitlesInput = media_path($this->episode->transcript_file_path); + if ($this->episode->transcript !== null) { + $this->subtitlesInput = media_path($this->episode->transcript->file_path); } $podcastFolder = media_path("podcasts/{$this->episode->podcast->handle}"); @@ -167,7 +167,6 @@ class VideoClip "{$this->videoClipOutput}", ]; - // dd(implode(' ', $videoClipCmd)); return implode(' ', $videoClipCmd); } diff --git a/app/Libraries/PodcastEpisode.php b/app/Libraries/PodcastEpisode.php index 11bc23eb..5d7aece5 100644 --- a/app/Libraries/PodcastEpisode.php +++ b/app/Libraries/PodcastEpisode.php @@ -52,24 +52,24 @@ class PodcastEpisode extends ObjectType $this->image = [ 'type' => 'Image', - 'mediaType' => $episode->cover_mimetype, + 'mediaType' => $episode->cover->file_content_type, 'url' => $episode->cover->feed_url, ]; // add audio file $this->audio = [ - 'id' => $episode->audio_file_url, + 'id' => $episode->audio->file_url, 'type' => 'Audio', 'name' => $episode->title, - 'size' => $episode->audio_file_size, - 'duration' => $episode->audio_file_duration, + 'size' => $episode->audio->file_size, + 'duration' => $episode->audio->duration, 'url' => [ - 'href' => $episode->audio_file_url, + 'href' => $episode->audio->file_url, 'type' => 'Link', - 'mediaType' => $episode->audio_file_mimetype, + 'mediaType' => $episode->audio->file_content_type, ], - 'transcript' => $episode->transcript_file_url, - 'chapters' => $episode->chapters_file_url, + 'transcript' => $episode->transcript->file_url, + 'chapters' => $episode->chapters->file_url, ]; $this->comments = url_to('episode-comments', $episode->podcast->handle, $episode->slug); diff --git a/app/Models/SoundbiteModel.php b/app/Models/ClipModel.php similarity index 83% rename from app/Models/SoundbiteModel.php rename to app/Models/ClipModel.php index e17140b0..6a7b2d8c 100644 --- a/app/Models/SoundbiteModel.php +++ b/app/Models/ClipModel.php @@ -12,16 +12,16 @@ declare(strict_types=1); namespace App\Models; -use App\Entities\Soundbite; +use App\Entities\Clip; use CodeIgniter\Database\BaseResult; use CodeIgniter\Model; -class SoundbiteModel extends Model +class ClipsModel extends Model { /** * @var string */ - protected $table = 'soundbites'; + protected $table = 'clips'; /** * @var string @@ -35,6 +35,7 @@ class SoundbiteModel extends Model 'podcast_id', 'episode_id', 'label', + 'type', 'start_time', 'duration', 'created_by', @@ -44,7 +45,7 @@ class SoundbiteModel extends Model /** * @var string */ - protected $returnType = Soundbite::class; + protected $returnType = Clip::class; /** * @var bool @@ -71,23 +72,23 @@ class SoundbiteModel extends Model */ protected $beforeDelete = ['clearCache']; - public function deleteSoundbite(int $podcastId, int $episodeId, int $soundbiteId): BaseResult | bool + public function deleteClip(int $podcastId, int $episodeId, int $clipId): BaseResult | bool { return $this->delete([ 'podcast_id' => $podcastId, 'episode_id' => $episodeId, - 'id' => $soundbiteId, + 'id' => $clipId, ]); } /** - * Gets all soundbites for an episode + * Gets all clips for an episode * - * @return Soundbite[] + * @return Clip[] */ - public function getEpisodeSoundbites(int $podcastId, int $episodeId): array + public function getEpisodeClips(int $podcastId, int $episodeId): array { - $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_soundbites"; + $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_clips"; if (! ($found = cache($cacheName))) { $found = $this->where([ 'episode_id' => $episodeId, @@ -114,7 +115,7 @@ class SoundbiteModel extends Model ); cache() - ->delete("podcast#{$episode->podcast_id}_episode#{$episode->id}_soundbites"); + ->delete("podcast#{$episode->podcast_id}_episode#{$episode->id}_clips"); // delete cache for rss feed cache() diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index c73886f3..043e4fb6 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -68,18 +68,13 @@ class EpisodeModel extends Model 'guid', 'title', 'slug', - 'audio_file_path', - 'audio_file_duration', - 'audio_file_mimetype', - 'audio_file_size', - 'audio_file_header_size', + 'audio_file_id', 'description_markdown', 'description_html', - 'cover_path', - 'cover_mimetype', - 'transcript_file_path', + 'cover_id', + 'transcript_file_id', 'transcript_file_remote_url', - 'chapters_file_path', + 'chapters_file_id', 'chapters_file_remote_url', 'parental_advisory', 'number', @@ -119,7 +114,7 @@ class EpisodeModel extends Model 'podcast_id' => 'required', 'title' => 'required', 'slug' => 'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]', - 'audio_file_path' => 'required', + 'audio_file_id' => 'required', 'description_markdown' => 'required', 'number' => 'is_natural_no_zero|permit_empty', 'season_number' => 'is_natural_no_zero|permit_empty', diff --git a/app/Models/MediaModel.php b/app/Models/MediaModel.php new file mode 100644 index 00000000..6d760524 --- /dev/null +++ b/app/Models/MediaModel.php @@ -0,0 +1,109 @@ +returnType = Audio::class; + break; + case 'image': + $this->returnType = Image::class; + break; + default: + // do nothing, keep Media class as default + break; + } + + parent::__construct($db, $validation); + } + + /** + * @return Media|Image|Audio + */ + public function getMediaById(int $mediaId): object + { + $cacheName = "media#{$mediaId}"; + if (! ($found = cache($cacheName))) { + $builder = $this->where([ + 'id' => $mediaId, + ]); + + $result = $builder->first(); + $mediaClass = $this->returnType; + $found = new $mediaClass($result->toArray(false, true)); + + cache() + ->save($cacheName, $found, DECADE); + } + + return $found; + } + + /** + * @param Media|Image|Audio $media + */ + public function saveMedia(object $media): int | false + { + // insert record in database + if (! $mediaId = $this->insert($media, true)) { + return false; + } + + // @phpstan-ignore-next-line + return $mediaId; + } +} diff --git a/app/Models/MediaModelOLD.php b/app/Models/MediaModelOLD.php new file mode 100644 index 00000000..fcdc5660 --- /dev/null +++ b/app/Models/MediaModelOLD.php @@ -0,0 +1,112 @@ +returnType = Audio::class; + break; + case 'image': + $this->returnType = Image::class; + break; + default: + // do nothing, keep Media class as default + break; + } + + parent::__construct($db, $validation); + } + + /** + * @return Media|Image|Audio + */ + public function getMediaById(int $mediaId): object + { + $cacheName = "media#{$mediaId}"; + if (! ($found = cache($cacheName))) { + $builder = $this->where([ + 'id' => $mediaId, + ]); + + $found = $builder->first(); + + cache() + ->save($cacheName, $found, DECADE); + } + + return $found; + } + + /** + * @param Media|Image $media + */ + public function saveMedia(object $media): int | false + { + // insert record in database + if (! $mediaId = $this->insert($media, true)) { + return false; + } + + // @phpstan-ignore-next-line + return $mediaId; + } + + public function deleteFile(int $mediaId): void + { + // TODO: get file, delete it from disk & from database + } +} diff --git a/app/Models/PersonModel.php b/app/Models/PersonModel.php index 77cc85c2..9e568882 100644 --- a/app/Models/PersonModel.php +++ b/app/Models/PersonModel.php @@ -35,8 +35,7 @@ class PersonModel extends Model 'full_name', 'unique_name', 'information_url', - 'avatar_path', - 'avatar_mimetype', + 'avatar_id', 'created_by', 'updated_by', ]; diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index aef6207c..02a913bb 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -40,10 +40,8 @@ class PodcastModel extends Model 'description_html', 'episode_description_footer_markdown', 'episode_description_footer_html', - 'cover_path', - 'cover_mimetype', - 'banner_path', - 'banner_mimetype', + 'cover_id', + 'banner_id', 'language_code', 'category_id', 'parental_advisory', @@ -92,7 +90,7 @@ class PodcastModel extends Model 'handle' => 'required|regex_match[/^[a-zA-Z0-9\_]{1,32}$/]|is_unique[podcasts.handle,id,{id}]', 'description_markdown' => 'required', - 'cover_path' => 'required', + 'cover_id' => 'required', 'language_code' => 'required', 'category_id' => 'required', 'owner_email' => 'required|valid_email', @@ -460,7 +458,7 @@ class PodcastModel extends Model if ($podcastActor) { $podcastActor->avatar_image_url = $podcast->cover->thumbnail_url; - $podcastActor->avatar_image_mimetype = $podcast->cover_mimetype; + $podcastActor->avatar_image_mimetype = $podcast->cover->thumbnail_mimetype; (new ActorModel())->update($podcast->actor_id, $podcastActor); } diff --git a/app/Resources/js/modules/EpisodesMap.ts b/app/Resources/js/modules/EpisodesMap.ts index 23ae543b..a90e5585 100644 --- a/app/Resources/js/modules/EpisodesMap.ts +++ b/app/Resources/js/modules/EpisodesMap.ts @@ -47,7 +47,7 @@ const drawEpisodesMap = async (mapDivId: string, dataUrl: string) => { data[i].longitude, ]).bindPopup( '