Commit fafaa7e6 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat(media): clean media api + create an entity per media type

parent b09acf6c
......@@ -31,7 +31,7 @@ class AddMedia extends Migration
'unsigned' => true,
'comment' => 'File size in bytes',
],
'file_content_type' => [
'file_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 45,
],
......@@ -42,6 +42,7 @@ class AddMedia extends Migration
'type' => [
'type' => 'ENUM',
'constraint' => ['image', 'audio', 'video', 'transcript', 'chapters', 'document'],
'default' => 'document',
],
'description' => [
'type' => 'TEXT',
......@@ -71,6 +72,7 @@ class AddMedia extends Migration
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('file_path');
$this->forge->addForeignKey('uploaded_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('media');
......
......@@ -198,7 +198,7 @@ class AddPodcasts extends Migration
$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('banner_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('category_id', 'categories', 'id');
$this->forge->addForeignKey('language_code', 'languages', 'code');
$this->forge->addForeignKey('created_by', 'users', 'id');
......
......@@ -157,9 +157,9 @@ class AddEpisodes extends Migration
$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('cover_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('transcript_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('chapters_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('episodes');
......
......@@ -64,7 +64,7 @@ class AddPersons extends Migration
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('avatar_id', 'media', 'id');
$this->forge->addForeignKey('avatar_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('persons');
......
<?php
declare(strict_types=1);
namespace App\Entities;
use CodeIgniter\Entity\Entity;
class BaseEntity extends Entity
{
}
......@@ -10,6 +10,10 @@ declare(strict_types=1);
namespace App\Entities;
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\ClipsModel;
use App\Models\EpisodeCommentModel;
......@@ -19,6 +23,7 @@ use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter;
use RuntimeException;
......@@ -42,10 +47,10 @@ use RuntimeException;
* @property int $cover_id
* @property Image $cover
* @property int|null $transcript_id
* @property Media|null $transcript
* @property Transcript|null $transcript
* @property string|null $transcript_remote_url
* @property int|null $chapters_id
* @property Media|null $chapters
* @property Chapters|null $chapters
* @property string|null $chapters_remote_url
* @property string|null $parental_advisory
* @property int $number
......@@ -69,7 +74,7 @@ use RuntimeException;
* @property Time|null $deleted_at;
*
* @property Person[] $persons;
* @property Soundbite[] $soundbites;
* @property Clip[] $clips;
* @property string $embed_url;
*/
class Episode extends Entity
......@@ -78,7 +83,7 @@ class Episode extends Entity
protected string $link;
protected Audio $audio;
protected ?Audio $audio = null;
protected string $audio_url;
......@@ -90,13 +95,13 @@ class Episode extends Entity
protected string $embed_url;
protected Image $cover;
protected ?Image $cover = null;
protected ?string $description = null;
protected ?Media $transcript;
protected ?Transcript $transcript = null;
protected ?Media $chapters;
protected ?Chapters $chapters = null;
/**
* @var Person[]|null
......@@ -161,25 +166,31 @@ class Episode extends Entity
'updated_by' => 'integer',
];
/**
* Saves an episode cover
*/
public function setCover(?Image $cover = null): static
public function setCover(?UploadedFile $file): self
{
if ($cover === null) {
if ($file === null || ! $file->isValid()) {
return $this;
}
// Save image
$cover->saveImage(
config('Images')
->podcastCoverSizes,
'podcasts/' . $this->getPodcast()->handle,
$this->attributes['slug']
);
$this->attributes['cover_mimetype'] = $cover->mimetype;
$this->attributes['cover_path'] = $cover->path;
if (array_key_exists('cover_id', $this->attributes) && $this->attributes['cover_id'] !== null) {
$this->getCover()
->setFile($file);
$this->getCover()
->updated_by = (int) user_id();
(new MediaModel('image'))->updateMedia($this->getCover());
} else {
$cover = new Image([
'file_name' => $this->attributes['slug'],
'file_directory' => 'podcasts/' . $this->attributes['handle'],
'sizes' => config('Images')
->podcastCoverSizes,
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
$cover->setFile($file);
$this->attributes['cover_id'] = (new MediaModel('image'))->saveMedia($cover);
}
return $this;
}
......@@ -193,25 +204,106 @@ class Episode extends Entity
return $this->cover;
}
public function setAudio(?UploadedFile $file): self
{
if ($file === null || ! $file->isValid()) {
return $this;
}
if ($this->audio_id !== null) {
$this->getAudio()
->setFile($file);
$this->getAudio()
->updated_by = (int) user_id();
(new MediaModel('audio'))->updateMedia($this->getAudio());
} else {
$transcript = new Audio([
'file_name' => $this->attributes['slug'],
'file_directory' => 'podcasts/' . $this->attributes['handle'],
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
$transcript->setFile($file);
$this->attributes['transcript_id'] = (new MediaModel())->saveMedia($transcript);
}
return $this;
}
public function getAudio(): Audio
{
if (! $this->audio) {
if (! $this->audio instanceof Audio) {
$this->audio = (new MediaModel('audio'))->getMediaById($this->audio_id);
}
return $this->audio;
}
public function getTranscript(): ?Media
public function setTranscript(?UploadedFile $file): self
{
if ($file === null || ! $file->isValid()) {
return $this;
}
if ($this->getTranscript() !== null) {
$this->getTranscript()
->setFile($file);
$this->getTranscript()
->updated_by = (int) user_id();
(new MediaModel('transcript'))->updateMedia($this->getTranscript());
} else {
$transcript = new Transcript([
'file_name' => $this->attributes['slug'] . '-transcript',
'file_directory' => 'podcasts/' . $this->attributes['handle'],
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
$transcript->setFile($file);
$this->attributes['transcript_id'] = (new MediaModel())->saveMedia($transcript);
}
return $this;
}
public function getTranscript(): ?Transcript
{
if ($this->transcript_id !== null && $this->transcript === null) {
$this->transcript = (new MediaModel('document'))->getMediaById($this->transcript_id);
$this->transcript = (new MediaModel('transcript'))->getMediaById($this->transcript_id);
}
return $this->transcript;
}
public function getChaptersFile(): ?Media
public function setChapters(?UploadedFile $file): self
{
if ($file === null || ! $file->isValid()) {
return $this;
}
if ($this->getChapters() !== null) {
$this->getChapters()
->setFile($file);
$this->getChapters()
->updated_by = (int) user_id();
(new MediaModel('chapters'))->updateMedia($this->getChapters());
} else {
$chapters = new Chapters([
'file_name' => $this->attributes['slug'] . '-chapters',
'file_directory' => 'podcasts/' . $this->attributes['handle'],
'uploaded_by' => user_id(),
'updated_by' => user_id(),
]);
$chapters->setFile($file);
$this->attributes['chapters_id'] = (new MediaModel())->saveMedia($chapters);
}
return $this;
}
public function getChapters(): ?Chapters
{
if ($this->chapters_id !== null && $this->chapters === null) {
$this->chapters = (new MediaModel('document'))->getMediaById($this->chapters_id);
......@@ -261,7 +353,7 @@ class Episode extends Entity
public function getTranscriptUrl(): ?string
{
if ($this->transcript !== null) {
return $this->transcript->url;
return $this->transcript->file_url;
}
return $this->transcript_remote_url;
}
......@@ -271,8 +363,8 @@ class Episode extends Entity
*/
public function getChaptersFileUrl(): ?string
{
if ($this->chapters) {
return $this->chapters->url;
if ($this->chapters !== null) {
return $this->chapters->file_url;
}
return $this->chapters_remote_url;
......
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Files\File;
use Config\Images;
class Image extends Media
{
/**
* @var array<string, array<string, int|string>>
*/
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<string, int[]> $sizes
*/
public function delete(array $sizes): void
{
helper('media');
foreach (array_keys($sizes) as $name) {
$pathProperty = $name . '_path';
unlink(media_path($this->{$pathProperty}));
}
}
}
......@@ -8,7 +8,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities;
namespace App\Entities\Media;
use CodeIgniter\Files\File;
use JamesHeinrich\GetID3\GetID3;
......@@ -17,7 +17,7 @@ use JamesHeinrich\GetID3\GetID3;
* @property float $duration
* @property int $header_size
*/
class Audio extends Media
class Audio extends BaseMedia
{
protected string $type = 'audio';
......@@ -41,7 +41,7 @@ class Audio extends Media
$getID3 = new GetID3();
$audioMetadata = $getID3->analyze((string) $file);
$this->attributes['file_content_type'] = $audioMetadata['mimetype'];
$this->attributes['file_mimetype'] = $audioMetadata['mimetype'];
$this->attributes['file_size'] = $audioMetadata['filesize'];
$this->attributes['description'] = $audioMetadata['comments']['comment'];
$this->attributes['file_metadata'] = $audioMetadata;
......
......@@ -8,7 +8,7 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities;
namespace App\Entities\Media;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
......@@ -16,11 +16,12 @@ use CodeIgniter\Files\File;
/**
* @property int $id
* @property string $file_path
* @property string $file_url
* @property string $file_directory
* @property string $file_extension
* @property string $file_name
* @property int $file_size
* @property string $file_content_type
* @property string $file_mimetype
* @property array $file_metadata
* @property 'image'|'audio'|'video'|'document' $type
* @property string $description
......@@ -28,7 +29,7 @@ use CodeIgniter\Files\File;
* @property int $uploaded_by
* @property int $updated_by
*/
class Media extends Entity
class BaseMedia extends Entity
{
protected File $file;
......@@ -44,9 +45,10 @@ class Media extends Entity
*/
protected $casts = [
'id' => 'integer',
'file_extension' => 'string',
'file_path' => 'string',
'file_size' => 'int',
'file_content_type' => 'string',
'file_mimetype' => 'string',
'file_metadata' => 'json-array',
'type' => 'string',
'description' => 'string',
......@@ -62,16 +64,23 @@ class Media extends Entity
{
parent::__construct($data);
if ($this->file_path) {
$this->initFileProperties();
}
public function initFileProperties(): void
{
if ($this->file_path !== '') {
helper('media');
[
'filename' => $filename,
'dirname' => $dirname,
'extension' => $extension,
] = pathinfo($this->file_path);
$this->file_name = $filename;
$this->file_directory = $dirname;
$this->file_extension = $extension;
$this->attributes['file_url'] = media_base_url($this->file_path);
$this->attributes['file_name'] = $filename;
$this->attributes['file_directory'] = $dirname;
$this->attributes['file_extension'] = $extension;
}
}
......@@ -79,7 +88,7 @@ class Media extends Entity
{
helper('media');
$this->attributes['file_content_type'] = $file->getMimeType();
$this->attributes['file_mimetype'] = $file->getMimeType();
$this->attributes['file_metadata'] = json_encode(lstat((string) $file));
$this->attributes['file_path'] = save_media(
$file,
......
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Media;
class Chapters extends BaseMedia
{
protected string $type = 'chapters';
}
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Media;
class Document extends BaseMedia
{
protected string $type = 'document';
}
......@@ -8,20 +8,20 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities;
namespace App\Entities\Media;
use CodeIgniter\Files\File;
class Image extends Media
/**
* @property array $sizes
*/
class Image extends BaseMedia
{
protected string $type = 'image';
/**
* @param array<string, mixed>|null $data
*/
public function __construct(array $data = null)
public function initFileProperties(): void
{
parent::__construct($data);
parent::initFileProperties();
if ($this->file_path && $this->file_metadata) {
$this->sizes = $this->file_metadata['sizes'];
......@@ -34,7 +34,7 @@ class Image extends Media
helper('media');
$extension = $this->file_extension;
$mimetype = $this->mimetype;
$mimetype = $this->file_mimetype;
foreach ($this->sizes as $name => $size) {
if (array_key_exists('extension', $size)) {
$extension = $size['extension'];
......@@ -62,6 +62,23 @@ class Image extends Media