Skip to content
Snippets Groups Projects
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
No related branches found
No related tags found
No related merge requests found
Showing
with 644 additions and 463 deletions
......@@ -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
{
helper('media');
if (! $this->audio) {
$this->audio = (new MediaModel('audio'))->getMediaById($this->audio_id);
}
return new File(media_path($this->audio_file_path));
return $this->audio;
}
public function getTranscriptFile(): ?File
public function getTranscript(): ?Media
{
if ($this->attributes['transcript_file_path']) {
helper('media');
return new File(media_path($this->attributes['transcript_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(): ?File
public function getChaptersFile(): ?Media
{
if ($this->attributes['chapters_file_path']) {
helper('media');
return new File(media_path($this->attributes['chapters_file_path']));
if ($this->chapters_id !== null && $this->chapters === null) {
$this->chapters = (new MediaModel('document'))->getMediaById($this->chapters_id);
}
return null;
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;
}
/**
......
......@@ -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<string, array<string, int|string>>
* @param array<string, mixed>|null $data
*/
protected array $sizes = [];
/**
* @param array<string, array<string, int|string>> $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.');
}
$dirname = '';
$filename = '';
$extension = '';
parent::__construct($data);
if ($file !== null) {
$dirname = $file->getPath();
$filename = $file->getBasename();
$extension = $file->getExtension();
$mimetype = $file->getMimeType();
if ($this->file_path && $this->file_metadata) {
$this->sizes = $this->file_metadata['sizes'];
$this->initSizeProperties();
}
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)
public function initSizeProperties(): bool
{
// 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;
helper('media');
$extension = '.' . $this->extension;
$extension = $this->file_extension;
$mimetype = $this->mimetype;
if ($fileSuffix !== '') {
$sizeName = substr($fileSuffix, 1);
if (array_key_exists('extension', $this->sizes[$sizeName])) {
$extension = '.' . $this->sizes[$sizeName]['extension'];
foreach ($this->sizes as $name => $size) {
if (array_key_exists('extension', $size)) {
$extension = $size['extension'];
}
if (array_key_exists('mimetype', $this->sizes[$sizeName])) {
$mimetype = $this->sizes[$sizeName]['mimetype'];
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;
}
$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;
}
}
public function getMimetype(): string
{
return $this->mimetype;
}
public function getFile(): File
{
return $this->file;
return true;
}
/**
* @param array<string, array<string, int|string>> $sizes
*/
public function saveImage(array $sizes, string $dirname, string $filename): void
public function setFile(File $file): self
{
helper('media');
parent::setFile($file);
$this->dirname = $dirname;
$this->filename = $filename;
$this->sizes = $sizes;
$metadata = exif_read_data(media_path($this->file_path), null, true);
save_media($this->file, $this->dirname, $this->filename);
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<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}));
}
return $this;
}
}
<?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}));
}
}
}
<?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\Entity\Entity;
use CodeIgniter\Files\File;
/**
* @property int $id
* @property string $file_path
* @property string $file_directory
* @property string $file_extension
* @property string $file_name
* @property int $file_size
* @property string $file_content_type
* @property array $file_metadata
* @property 'image'|'audio'|'video'|'document' $type
* @property string $description
* @property string|null $language_code
* @property int $uploaded_by
* @property int $updated_by
*/
class Media extends Entity
{
protected File $file;
protected string $type = 'document';
/**
* @var string[]
*/
protected $dates = ['uploaded_at', 'updated_at', 'deleted_at'];
/**
* @var array<string, string>
*/
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<string, mixed>|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;
}
}
<?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\Entity\Entity;
use CodeIgniter\Files\File;
/**
* @property int $id
* @property string $file_path
* @property string $file_directory
* @property string $file_extension
* @property string $file_name
* @property int $file_size
* @property string $file_content_type
* @property array $file_metadata
* @property 'image'|'audio'|'video'|'document' $type
* @property string $description
* @property string|null $language_code
* @property int $uploaded_by
* @property int $updated_by
*/
class Media extends Entity
{
protected File $file;
/**
* @var string[]
*/
protected $dates = ['uploaded_at', 'updated_at', 'deleted_at'];
/**
* @var array<string, string>
*/
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;
}
}
}
......@@ -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',
......
......@@ -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,
],
]);
}
if (! $this->banner instanceof Image) {
$this->banner = (new MediaModel('image'))->getMediaById($this->banner_id);
}
return new Image(null, $this->banner_path, $this->banner_mimetype, config('Images') ->podcastBannerSizes);
return $this->banner;
}
public function getLink(): string
......
......@@ -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<string, string|double|int>
*/
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'];
......
......@@ -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) {
......
......@@ -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 ?? '')
......
......@@ -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);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment