Loading app/Database/Migrations/2021-12-09-130000_add_clips.php +0 −1 Original line number Diff line number Diff line Loading @@ -85,7 +85,6 @@ class AddClips extends Migration ]); $this->forge->addKey('id', true); $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'); Loading app/Entities/Clip/BaseClip.php +20 −6 Original line number Diff line number Diff line Loading @@ -17,8 +17,10 @@ use App\Entities\Podcast; use App\Models\EpisodeModel; use App\Models\MediaModel; use App\Models\PodcastModel; use App\Models\UserModel; use CodeIgniter\Entity\Entity; use CodeIgniter\Files\File; use Modules\Auth\Entities\User; /** * @property int $id Loading @@ -33,8 +35,10 @@ use CodeIgniter\Files\File; * @property string $type * @property int $media_id * @property Video|Audio $media * @property array|null $metadata * @property string $status * @property string $logs * @property User $user * @property int $created_by * @property int $updated_by */ Loading @@ -52,14 +56,17 @@ class BaseClip extends Entity 'duration' => 'double', 'type' => 'string', 'media_id' => '?integer', 'metadata' => 'json-array', 'metadata' => '?json-array', 'status' => 'string', 'logs' => 'string', 'created_by' => 'integer', 'updated_by' => 'integer', ]; public function __construct($data) /** * @param array<string, mixed>|null $data */ public function __construct(array $data = null) { parent::__construct($data); Loading @@ -80,13 +87,20 @@ class BaseClip extends Entity return (new EpisodeModel())->getEpisodeById($this->episode_id); } public function getUser(): ?User { return (new UserModel())->find($this->created_by); } public function setMedia(string $filePath = null): static { if ($filePath === null || ($file = new File($filePath)) === null) { if ($filePath === null) { return $this; } if ($this->media_id !== 0) { $file = new File($filePath); if ($this->media_id !== null) { $this->getMedia() ->setFile($file); $this->getMedia() Loading @@ -97,8 +111,8 @@ class BaseClip extends Entity 'file_path' => $filePath, 'language_code' => $this->getPodcast() ->language_code, 'uploaded_by' => user_id(), 'updated_by' => user_id(), 'uploaded_by' => $this->attributes['created_by'], 'updated_by' => $this->attributes['created_by'], ]); $media->setFile($file); Loading app/Entities/Clip/VideoClip.php +60 −2 Original line number Diff line number Diff line Loading @@ -10,20 +10,78 @@ declare(strict_types=1); namespace App\Entities\Clip; use App\Entities\Media\Video; use App\Models\MediaModel; use CodeIgniter\Files\File; /** * @property string $theme * @property array $theme * @property string $format */ class VideoClip extends BaseClip { protected string $type = 'video'; /** * @param array<string, mixed>|null $data */ public function __construct(array $data = null) { parent::__construct($data); if ($this->metadata !== null) { if ($this->metadata !== null && $this->metadata !== []) { $this->theme = $this->metadata['theme']; $this->format = $this->metadata['format']; } } /** * @param array<string, string> $theme */ public function setTheme(array $theme): self { // TODO: change? $this->attributes['metadata'] = json_decode($this->attributes['metadata'] ?? '[]', true); $this->attributes['theme'] = $theme; $this->attributes['metadata']['theme'] = $theme; $this->attributes['metadata'] = json_encode($this->attributes['metadata']); return $this; } public function setFormat(string $format): self { $this->attributes['metadata'] = json_decode($this->attributes['metadata'], true); $this->attributes['format'] = $format; $this->attributes['metadata']['format'] = $format; $this->attributes['metadata'] = json_encode($this->attributes['metadata']); return $this; } public function setMedia(string $filePath = null): static { if ($filePath === null) { return $this; } $file = new File($filePath); $video = new Video([ 'file_path' => $filePath, 'language_code' => $this->getPodcast() ->language_code, 'uploaded_by' => $this->attributes['created_by'], 'updated_by' => $this->attributes['created_by'], ]); $video->setFile($file); $this->attributes['media_id'] = (new MediaModel())->saveMedia($video); return $this; } } app/Entities/Episode.php +4 −4 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ declare(strict_types=1); namespace App\Entities; use App\Entities\Clip\BaseClip; use App\Entities\Clip\Soundbite; use App\Entities\Media\Audio; use App\Entities\Media\Chapters; use App\Entities\Media\Image; Loading Loading @@ -75,7 +75,7 @@ use RuntimeException; * @property Time|null $deleted_at; * * @property Person[] $persons; * @property Soundbites[] $soundbites; * @property Soundbite[] $soundbites; * @property string $embed_url; */ class Episode extends Entity Loading Loading @@ -110,7 +110,7 @@ class Episode extends Entity protected ?array $persons = null; /** * @var Soundbites[]|null * @var Soundbite[]|null */ protected ?array $soundbites = null; Loading Loading @@ -407,7 +407,7 @@ class Episode extends Entity /** * Returns the episode’s clips * * @return BaseClip[]|\App\Entities\Soundbites[] * @return Soundbite[] */ public function getSoundbites(): array { Loading app/Libraries/MediaClipper/VideoClipper.php +56 −5 Original line number Diff line number Diff line Loading @@ -31,6 +31,12 @@ class VideoClipper 'timestamp' => 'NotoSansMono-Regular.ttf', ]; public ?string $logs = null; public bool $error = false; public string $videoClipOutput; protected float $duration; protected string $audioInput; Loading @@ -45,8 +51,6 @@ class VideoClipper protected string $videoClipBgOutput; protected string $videoClipOutput; protected ?string $episodeNumbering = null; /** Loading Loading @@ -107,7 +111,10 @@ class VideoClipper } } public function generate(): string /** * @return int 0 for success, else error */ public function generate(): int { $this->soundbite(); $this->subtitlesClip(); Loading @@ -119,7 +126,7 @@ class VideoClipper $generateCmd = $this->getCmd(); return shell_exec($generateCmd . ' 2>&1'); return $this->cmd_exec($generateCmd); } public function getCmd(): string Loading Loading @@ -205,7 +212,7 @@ class VideoClipper return false; } $episodeCover = imagecreatefromjpeg($this->episodeCoverPath); $episodeCover = $this->createCoverImage(); if (! $episodeCover) { return false; } Loading Loading @@ -340,6 +347,41 @@ class VideoClipper return true; } /** * @return int 0 (success), 1 - 2 - 254 - 255 (error) */ private function cmd_exec(string $cmd): int { $outFile = tempnam(WRITEPATH . 'logs', 'cmd-out-'); if (! $outFile) { return 254; } $descriptorSpec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['file', $outFile, 'w'], // FFmpeg outputs to stderr by default ]; $proc = proc_open($cmd, $descriptorSpec, $pipes); if (! is_resource($proc)) { return 255; } fclose($pipes[0]); //Don't really want to give any input $exit = proc_close($proc); $this->logs = (string) file_get_contents($outFile); // remove temporary files unlink($outFile); return $exit; } private function getFont(string $name): string { return config('MediaClipper')->fontsFolder . self::FONTS[$name]; Loading @@ -364,6 +406,15 @@ class VideoClipper return $background; } private function createCoverImage(): GdImage | false { return match ($this->episode->cover->file_mimetype) { 'image/jpeg' => imagecreatefromjpeg($this->episodeCoverPath), 'image/png' => imagecreatefrompng($this->episodeCoverPath), default => imagecreate(1400, 1400), }; } private function scaleImage(GdImage $image, int $width, int $height): GdImage | false { return imagescale($image, $width, $height); Loading Loading
app/Database/Migrations/2021-12-09-130000_add_clips.php +0 −1 Original line number Diff line number Diff line Loading @@ -85,7 +85,6 @@ class AddClips extends Migration ]); $this->forge->addKey('id', true); $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'); Loading
app/Entities/Clip/BaseClip.php +20 −6 Original line number Diff line number Diff line Loading @@ -17,8 +17,10 @@ use App\Entities\Podcast; use App\Models\EpisodeModel; use App\Models\MediaModel; use App\Models\PodcastModel; use App\Models\UserModel; use CodeIgniter\Entity\Entity; use CodeIgniter\Files\File; use Modules\Auth\Entities\User; /** * @property int $id Loading @@ -33,8 +35,10 @@ use CodeIgniter\Files\File; * @property string $type * @property int $media_id * @property Video|Audio $media * @property array|null $metadata * @property string $status * @property string $logs * @property User $user * @property int $created_by * @property int $updated_by */ Loading @@ -52,14 +56,17 @@ class BaseClip extends Entity 'duration' => 'double', 'type' => 'string', 'media_id' => '?integer', 'metadata' => 'json-array', 'metadata' => '?json-array', 'status' => 'string', 'logs' => 'string', 'created_by' => 'integer', 'updated_by' => 'integer', ]; public function __construct($data) /** * @param array<string, mixed>|null $data */ public function __construct(array $data = null) { parent::__construct($data); Loading @@ -80,13 +87,20 @@ class BaseClip extends Entity return (new EpisodeModel())->getEpisodeById($this->episode_id); } public function getUser(): ?User { return (new UserModel())->find($this->created_by); } public function setMedia(string $filePath = null): static { if ($filePath === null || ($file = new File($filePath)) === null) { if ($filePath === null) { return $this; } if ($this->media_id !== 0) { $file = new File($filePath); if ($this->media_id !== null) { $this->getMedia() ->setFile($file); $this->getMedia() Loading @@ -97,8 +111,8 @@ class BaseClip extends Entity 'file_path' => $filePath, 'language_code' => $this->getPodcast() ->language_code, 'uploaded_by' => user_id(), 'updated_by' => user_id(), 'uploaded_by' => $this->attributes['created_by'], 'updated_by' => $this->attributes['created_by'], ]); $media->setFile($file); Loading
app/Entities/Clip/VideoClip.php +60 −2 Original line number Diff line number Diff line Loading @@ -10,20 +10,78 @@ declare(strict_types=1); namespace App\Entities\Clip; use App\Entities\Media\Video; use App\Models\MediaModel; use CodeIgniter\Files\File; /** * @property string $theme * @property array $theme * @property string $format */ class VideoClip extends BaseClip { protected string $type = 'video'; /** * @param array<string, mixed>|null $data */ public function __construct(array $data = null) { parent::__construct($data); if ($this->metadata !== null) { if ($this->metadata !== null && $this->metadata !== []) { $this->theme = $this->metadata['theme']; $this->format = $this->metadata['format']; } } /** * @param array<string, string> $theme */ public function setTheme(array $theme): self { // TODO: change? $this->attributes['metadata'] = json_decode($this->attributes['metadata'] ?? '[]', true); $this->attributes['theme'] = $theme; $this->attributes['metadata']['theme'] = $theme; $this->attributes['metadata'] = json_encode($this->attributes['metadata']); return $this; } public function setFormat(string $format): self { $this->attributes['metadata'] = json_decode($this->attributes['metadata'], true); $this->attributes['format'] = $format; $this->attributes['metadata']['format'] = $format; $this->attributes['metadata'] = json_encode($this->attributes['metadata']); return $this; } public function setMedia(string $filePath = null): static { if ($filePath === null) { return $this; } $file = new File($filePath); $video = new Video([ 'file_path' => $filePath, 'language_code' => $this->getPodcast() ->language_code, 'uploaded_by' => $this->attributes['created_by'], 'updated_by' => $this->attributes['created_by'], ]); $video->setFile($file); $this->attributes['media_id'] = (new MediaModel())->saveMedia($video); return $this; } }
app/Entities/Episode.php +4 −4 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ declare(strict_types=1); namespace App\Entities; use App\Entities\Clip\BaseClip; use App\Entities\Clip\Soundbite; use App\Entities\Media\Audio; use App\Entities\Media\Chapters; use App\Entities\Media\Image; Loading Loading @@ -75,7 +75,7 @@ use RuntimeException; * @property Time|null $deleted_at; * * @property Person[] $persons; * @property Soundbites[] $soundbites; * @property Soundbite[] $soundbites; * @property string $embed_url; */ class Episode extends Entity Loading Loading @@ -110,7 +110,7 @@ class Episode extends Entity protected ?array $persons = null; /** * @var Soundbites[]|null * @var Soundbite[]|null */ protected ?array $soundbites = null; Loading Loading @@ -407,7 +407,7 @@ class Episode extends Entity /** * Returns the episode’s clips * * @return BaseClip[]|\App\Entities\Soundbites[] * @return Soundbite[] */ public function getSoundbites(): array { Loading
app/Libraries/MediaClipper/VideoClipper.php +56 −5 Original line number Diff line number Diff line Loading @@ -31,6 +31,12 @@ class VideoClipper 'timestamp' => 'NotoSansMono-Regular.ttf', ]; public ?string $logs = null; public bool $error = false; public string $videoClipOutput; protected float $duration; protected string $audioInput; Loading @@ -45,8 +51,6 @@ class VideoClipper protected string $videoClipBgOutput; protected string $videoClipOutput; protected ?string $episodeNumbering = null; /** Loading Loading @@ -107,7 +111,10 @@ class VideoClipper } } public function generate(): string /** * @return int 0 for success, else error */ public function generate(): int { $this->soundbite(); $this->subtitlesClip(); Loading @@ -119,7 +126,7 @@ class VideoClipper $generateCmd = $this->getCmd(); return shell_exec($generateCmd . ' 2>&1'); return $this->cmd_exec($generateCmd); } public function getCmd(): string Loading Loading @@ -205,7 +212,7 @@ class VideoClipper return false; } $episodeCover = imagecreatefromjpeg($this->episodeCoverPath); $episodeCover = $this->createCoverImage(); if (! $episodeCover) { return false; } Loading Loading @@ -340,6 +347,41 @@ class VideoClipper return true; } /** * @return int 0 (success), 1 - 2 - 254 - 255 (error) */ private function cmd_exec(string $cmd): int { $outFile = tempnam(WRITEPATH . 'logs', 'cmd-out-'); if (! $outFile) { return 254; } $descriptorSpec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['file', $outFile, 'w'], // FFmpeg outputs to stderr by default ]; $proc = proc_open($cmd, $descriptorSpec, $pipes); if (! is_resource($proc)) { return 255; } fclose($pipes[0]); //Don't really want to give any input $exit = proc_close($proc); $this->logs = (string) file_get_contents($outFile); // remove temporary files unlink($outFile); return $exit; } private function getFont(string $name): string { return config('MediaClipper')->fontsFolder . self::FONTS[$name]; Loading @@ -364,6 +406,15 @@ class VideoClipper return $background; } private function createCoverImage(): GdImage | false { return match ($this->episode->cover->file_mimetype) { 'image/jpeg' => imagecreatefromjpeg($this->episodeCoverPath), 'image/png' => imagecreatefrompng($this->episodeCoverPath), default => imagecreate(1400, 1400), }; } private function scaleImage(GdImage $image, int $width, int $height): GdImage | false { return imagescale($image, $width, $height); Loading