Commit 6ecf2866 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: add media entity and link documents, images and audio files to it

parent 1d1490b0
......@@ -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 \
......
......@@ -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,
];
......
<?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\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddMedia extends Migration
{
public function up(): void
{
$this->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');
}
}
......@@ -46,25 +46,13 @@ class AddPodcasts extends Migration
'description_html' => [
'type' => 'TEXT',
],
'cover_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
// 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,
'cover_id' => [
'type' => 'INT',
'unsigned' => true,
],
'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');
......
......@@ -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' => [
'type' => 'INT',
'unsigned' => true,
'comment' => 'File size in bytes',
],
'audio_file_header_size' => [
'audio_id' => [
'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,
'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,
'cover_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'transcript_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'transcript_id' => [
'type' => 'INT',
'unsigned' => true,
'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');
......
......@@ -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');
......
......@@ -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');
}
}
<?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 JamesHeinrich\GetID3\GetID3;
/**
* @property float $duration
* @property int $header_size
*/
class Audio extends Media
{
protected string $type = 'audio';
/**
* @param array<string, mixed>|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;
}
}
......@@ -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<string, string>
......@@ -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',
];
......
......@@ -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
{
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
public function getAudio(): Audio
{
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