mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat: add media entity and link documents, images and audio files to it
This commit is contained in:
parent
1d1490b06a
commit
6ecf2866cf
@ -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,
|
||||
];
|
||||
|
83
app/Database/Migrations/2020-05-29-120000_add_media.php
Normal file
83
app/Database/Migrations/2020-05-29-120000_add_media.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?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,
|
||||
'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');
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
51
app/Entities/Audio.php
Normal file
51
app/Entities/Audio.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?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
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.');
|
||||
}
|
||||
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<string, array<string, int|string>> $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<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;
|
||||
}
|
||||
}
|
||||
|
123
app/Entities/ImageOLD.php
Normal file
123
app/Entities/ImageOLD.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?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}));
|
||||
}
|
||||
}
|
||||
}
|
95
app/Entities/Media.php
Normal file
95
app/Entities/Media.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
93
app/Entities/MediaOLD.php
Normal file
93
app/Entities/MediaOLD.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?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,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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()
|
@ -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',
|
||||
|
109
app/Models/MediaModel.php
Normal file
109
app/Models/MediaModel.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Entities\Audio;
|
||||
use App\Entities\Image;
|
||||
use App\Entities\Media;
|
||||
use CodeIgniter\Database\ConnectionInterface;
|
||||
use CodeIgniter\Model;
|
||||
use CodeIgniter\Validation\ValidationInterface;
|
||||
|
||||
class MediaModel extends Model
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'media';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $returnType = Media::class;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'file_path',
|
||||
'file_size',
|
||||
'file_content_type',
|
||||
'file_metadata',
|
||||
'type',
|
||||
'description',
|
||||
'language_code',
|
||||
'uploaded_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* Model constructor.
|
||||
*
|
||||
* @param ConnectionInterface|null $db DB Connection
|
||||
* @param ValidationInterface|null $validation Validation
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $fileType,
|
||||
ConnectionInterface &$db = null,
|
||||
ValidationInterface $validation = null
|
||||
) {
|
||||
switch ($fileType) {
|
||||
case 'audio':
|
||||
$this->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;
|
||||
}
|
||||
}
|
112
app/Models/MediaModelOLD.php
Normal file
112
app/Models/MediaModelOLD.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Entities\Audio;
|
||||
use App\Entities\Image;
|
||||
use App\Entities\Media;
|
||||
use CodeIgniter\Database\ConnectionInterface;
|
||||
use CodeIgniter\Model;
|
||||
use CodeIgniter\Validation\ValidationInterface;
|
||||
|
||||
class MediaModel extends Model
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'media';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $returnType = Media::class;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'file_path',
|
||||
'file_size',
|
||||
'file_content_type',
|
||||
'file_metadata',
|
||||
'type',
|
||||
'description',
|
||||
'language_code',
|
||||
'uploaded_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* Model constructor.
|
||||
*
|
||||
* @param ConnectionInterface|null $db DB Connection
|
||||
* @param ValidationInterface|null $validation Validation
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $fileType,
|
||||
ConnectionInterface &$db = null,
|
||||
ValidationInterface $validation = null
|
||||
) {
|
||||
switch ($fileType) {
|
||||
case 'audio':
|
||||
$this->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
|
||||
}
|
||||
}
|
@ -35,8 +35,7 @@ class PersonModel extends Model
|
||||
'full_name',
|
||||
'unique_name',
|
||||
'information_url',
|
||||
'avatar_path',
|
||||
'avatar_mimetype',
|
||||
'avatar_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ const drawEpisodesMap = async (mapDivId: string, dataUrl: string) => {
|
||||
data[i].longitude,
|
||||
]).bindPopup(
|
||||
'<div class="flex min-w-max w-full gap-x-2"><img src="' +
|
||||
data[i].cover_path +
|
||||
data[i].cover_url +
|
||||
'" alt="' +
|
||||
data[i].episode_title +
|
||||
'" class="rounded w-16 h-16" /><div class="flex flex-col flex-1"><h2 class="leading-tight text-sm w-56 line-clamp-2 font-bold"><a href="' +
|
||||
|
@ -14,13 +14,15 @@ use App\Entities\Episode;
|
||||
use App\Entities\EpisodeComment;
|
||||
use App\Entities\Image;
|
||||
use App\Entities\Location;
|
||||
use App\Entities\Media;
|
||||
use App\Entities\Podcast;
|
||||
use App\Entities\Post;
|
||||
use App\Models\ClipsModel;
|
||||
use App\Models\EpisodeCommentModel;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\MediaModel;
|
||||
use App\Models\PodcastModel;
|
||||
use App\Models\PostModel;
|
||||
use App\Models\SoundbiteModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\I18n\Time;
|
||||
@ -156,9 +158,30 @@ class EpisodeController extends BaseController
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$db = db_connect();
|
||||
$db->transStart();
|
||||
|
||||
$coverFile = $this->request->getFile('cover');
|
||||
if ($coverFile !== null && $coverFile->isValid()) {
|
||||
$newEpisode->cover = new Image($coverFile);
|
||||
$cover = new Image([
|
||||
'file_name' => $newEpisode->slug,
|
||||
'file_directory' => 'podcasts/' . $this->podcast->handle,
|
||||
'sizes' => config('Images')
|
||||
->podcastBannerSizes,
|
||||
'file' => $this->request->getFile('banner'),
|
||||
'uploaded_by' => user_id(),
|
||||
'updated_by' => user_id(),
|
||||
]);
|
||||
$mediaModel = new MediaModel('image');
|
||||
if (! ($newCoverId = $mediaModel->saveMedia($cover))) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $mediaModel->errors());
|
||||
}
|
||||
|
||||
$newEpisode->cover_id = $newCoverId;
|
||||
}
|
||||
|
||||
$transcriptChoice = $this->request->getPost('transcript-choice');
|
||||
@ -167,10 +190,26 @@ class EpisodeController extends BaseController
|
||||
&& ($transcriptFile = $this->request->getFile('transcript_file'))
|
||||
&& $transcriptFile->isValid()
|
||||
) {
|
||||
$newEpisode->transcript_file = $transcriptFile;
|
||||
$transcript = new Media([
|
||||
'file_name' => $newEpisode->slug . '-transcript',
|
||||
'file_directory' => 'podcasts/' . $this->podcast->handle,
|
||||
'file' => $transcriptFile,
|
||||
'uploaded_by' => user_id(),
|
||||
'updated_by' => user_id(),
|
||||
]);
|
||||
$mediaModel = new MediaModel('image');
|
||||
if (! ($newTranscriptId = $mediaModel->saveMedia($transcript))) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $mediaModel->errors());
|
||||
}
|
||||
|
||||
$newEpisode->transcript_id = $newTranscriptId;
|
||||
} elseif ($transcriptChoice === 'remote-url') {
|
||||
$newEpisode->transcript_file_remote_url = $this->request->getPost(
|
||||
'transcript_file_remote_url'
|
||||
$newEpisode->transcript_remote_url = $this->request->getPost(
|
||||
'transcript_remote_url'
|
||||
) === '' ? null : $this->request->getPost('transcript_file_remote_url');
|
||||
}
|
||||
|
||||
@ -813,11 +852,11 @@ class EpisodeController extends BaseController
|
||||
return redirect()->route('soundbites-edit', [$this->podcast->id, $this->episode->id]);
|
||||
}
|
||||
|
||||
public function soundbiteDelete(string $soundbiteId): RedirectResponse
|
||||
public function soundbiteDelete(string $clipId): RedirectResponse
|
||||
{
|
||||
(new SoundbiteModel())->deleteSoundbite($this->podcast->id, $this->episode->id, (int) $soundbiteId);
|
||||
(new ClipsModel())->deleteClip($this->podcast->id, $this->episode->id, (int) $clipId);
|
||||
|
||||
return redirect()->route('soundbites-edit', [$this->podcast->id, $this->episode->id]);
|
||||
return redirect()->route('clips-edit', [$this->podcast->id, $this->episode->id]);
|
||||
}
|
||||
|
||||
public function embed(): string
|
||||
|
@ -16,6 +16,7 @@ use App\Entities\Podcast;
|
||||
use App\Models\CategoryModel;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\LanguageModel;
|
||||
use App\Models\MediaModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
@ -192,11 +193,10 @@ class PodcastController extends BaseController
|
||||
$partnerImageUrl = null;
|
||||
}
|
||||
|
||||
$podcast = new Podcast([
|
||||
$newPodcast = new Podcast([
|
||||
'title' => $this->request->getPost('title'),
|
||||
'handle' => $this->request->getPost('handle'),
|
||||
'description_markdown' => $this->request->getPost('description'),
|
||||
'cover' => new Image($this->request->getFile('cover')),
|
||||
'language_code' => $this->request->getPost('language'),
|
||||
'category_id' => $this->request->getPost('category'),
|
||||
'parental_advisory' =>
|
||||
@ -225,17 +225,53 @@ class PodcastController extends BaseController
|
||||
'updated_by' => user_id(),
|
||||
]);
|
||||
|
||||
$db = db_connect();
|
||||
$db->transStart();
|
||||
|
||||
$cover = new Image([
|
||||
'file_name' => 'cover',
|
||||
'file_directory' => 'podcasts/' . $newPodcast->handle,
|
||||
'sizes' => config('Images')
|
||||
->podcastCoverSizes,
|
||||
'file' => $this->request->getFile('cover'),
|
||||
'uploaded_by' => user_id(),
|
||||
'updated_by' => user_id(),
|
||||
]);
|
||||
$mediaModel = new MediaModel('image');
|
||||
if (! ($newCoverId = $mediaModel->saveMedia($cover))) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $mediaModel->errors());
|
||||
}
|
||||
$newPodcast->cover_id = $newCoverId;
|
||||
|
||||
$bannerFile = $this->request->getFile('banner');
|
||||
if ($bannerFile !== null && $bannerFile->isValid()) {
|
||||
$podcast->banner = new Image($bannerFile);
|
||||
$banner = new Image([
|
||||
'file_name' => 'banner',
|
||||
'file_directory' => 'podcasts/' . $newPodcast->handle,
|
||||
'sizes' => config('Images')
|
||||
->podcastBannerSizes,
|
||||
'file' => $this->request->getFile('banner'),
|
||||
'uploaded_by' => user_id(),
|
||||
'updated_by' => user_id(),
|
||||
]);
|
||||
$mediaModel = new MediaModel('image');
|
||||
if (! ($newBannerId = $mediaModel->saveMedia($banner))) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $mediaModel->errors());
|
||||
}
|
||||
|
||||
$newPodcast->banner_id = $newBannerId;
|
||||
}
|
||||
|
||||
$podcastModel = new PodcastModel();
|
||||
$db = db_connect();
|
||||
|
||||
$db->transStart();
|
||||
|
||||
if (! ($newPodcastId = $podcastModel->insert($podcast, true))) {
|
||||
if (! ($newPodcastId = $podcastModel->insert($newPodcast, true))) {
|
||||
$db->transRollback();
|
||||
return redirect()
|
||||
->back()
|
||||
@ -311,7 +347,7 @@ class PodcastController extends BaseController
|
||||
|
||||
$coverFile = $this->request->getFile('cover');
|
||||
if ($coverFile !== null && $coverFile->isValid()) {
|
||||
$this->podcast->cover = new Image($coverFile);
|
||||
$this->podcast->cover->setFile($coverFile);
|
||||
}
|
||||
$bannerFile = $this->request->getFile('banner');
|
||||
if ($bannerFile !== null && $bannerFile->isValid()) {
|
||||
|
@ -36,7 +36,7 @@ class SchedulerController extends Controller
|
||||
// set activity post to delivered
|
||||
model('ActivityModel')
|
||||
->update($scheduledActivity->id, [
|
||||
'task_status' => 'delivered',
|
||||
'status' => 'delivered',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ class AddActivities extends Migration
|
||||
'payload' => [
|
||||
'type' => 'JSON',
|
||||
],
|
||||
'task_status' => [
|
||||
'status' => [
|
||||
'type' => 'ENUM',
|
||||
'constraint' => ['queued', 'delivered'],
|
||||
'null' => true,
|
||||
|
@ -23,7 +23,7 @@ use RuntimeException;
|
||||
* @property Post $post
|
||||
* @property string $type
|
||||
* @property object $payload
|
||||
* @property string|null $task_status
|
||||
* @property string|null $status
|
||||
* @property Time|null $scheduled_at
|
||||
* @property Time $created_at
|
||||
*/
|
||||
@ -55,7 +55,7 @@ class Activity extends UuidEntity
|
||||
'post_id' => '?string',
|
||||
'type' => 'string',
|
||||
'payload' => 'json',
|
||||
'task_status' => '?string',
|
||||
'status' => '?string',
|
||||
];
|
||||
|
||||
public function getActor(): Actor
|
||||
|
@ -42,7 +42,7 @@ class ActivityModel extends BaseUuidModel
|
||||
'post_id',
|
||||
'type',
|
||||
'payload',
|
||||
'task_status',
|
||||
'status',
|
||||
'scheduled_at',
|
||||
];
|
||||
|
||||
@ -100,7 +100,7 @@ class ActivityModel extends BaseUuidModel
|
||||
'type' => $type,
|
||||
'payload' => $payload,
|
||||
'scheduled_at' => $scheduledAt,
|
||||
'task_status' => $taskStatus,
|
||||
'status' => $taskStatus,
|
||||
],
|
||||
true,
|
||||
);
|
||||
@ -112,7 +112,7 @@ class ActivityModel extends BaseUuidModel
|
||||
public function getScheduledActivities(): array
|
||||
{
|
||||
return $this->where('`scheduled_at` <= NOW()', null, false)
|
||||
->where('task_status', 'queued')
|
||||
->where('status', 'queued')
|
||||
->orderBy('scheduled_at', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ Options All -Indexes
|
||||
Options +FollowSymlinks
|
||||
RewriteEngine On
|
||||
|
||||
# If you installed CodeIgniter in a subfolder, you will need to
|
||||
# If you installed Castopod Host in a subfolder, you will need to
|
||||
# change the following line to match the subfolder you need.
|
||||
# http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase
|
||||
# RewriteBase /
|
||||
|
@ -29,9 +29,9 @@
|
||||
'cell' => function ($episode, $podcast) {
|
||||
return '<div class="flex">' .
|
||||
'<div class="relative flex-shrink-0 mr-2">' .
|
||||
'<time class="absolute px-1 text-xs font-semibold text-white rounded bottom-2 right-2 bg-black/50" datetime="PT<?= $episode->audio_file_duration ?>S">' .
|
||||
'<time class="absolute px-1 text-xs font-semibold text-white rounded bottom-2 right-2 bg-black/50" datetime="PT<?= $episode->audio->duration ?>S">' .
|
||||
format_duration(
|
||||
$episode->audio_file_duration,
|
||||
$episode->audio->duration,
|
||||
) .
|
||||
'</time>' .
|
||||
'<img loading="lazy" src="' . $episode->cover->thumbnail_url . '" alt="' . $episode->title . '" class="object-cover w-20 rounded-lg shadow-inner aspect-square" />' .
|
||||
|
@ -54,12 +54,12 @@
|
||||
) ?>
|
||||
</div>
|
||||
<div class="text-xs text-skin-muted">
|
||||
<time datetime="PT<?= $episode->audio_file_duration ?>S">
|
||||
<?= format_duration($episode->audio_file_duration) ?>
|
||||
<time datetime="PT<?= $episode->audio->duration ?>S">
|
||||
<?= format_duration($episode->audio->duration) ?>
|
||||
</time>
|
||||
</div>
|
||||
</a>
|
||||
<?= audio_player($episode->audio_file_url, $episode->audio_file_mimetype, 'mt-auto') ?>
|
||||
<?= audio_player($episode->audio->file_url, $episode->audio->file_content_type, 'mt-auto') ?>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="flex justify-around px-6 py-3">
|
||||
|
@ -58,12 +58,12 @@
|
||||
<div class="text-xs text-skin-muted">
|
||||
<?= relative_time($episode->published_at) ?>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= $episode->audio_file_duration ?>S">
|
||||
<?= format_duration($episode->audio_file_duration) ?>
|
||||
<time datetime="PT<?= $episode->audio->duration ?>S">
|
||||
<?= format_duration($episode->audio->duration) ?>
|
||||
</time>
|
||||
</div>
|
||||
</a>
|
||||
<?= audio_player($episode->audio_file_url, $episode->audio_file_mimetype, 'mt-auto') ?>
|
||||
<?= audio_player($episode->audio->file_url, $episode->audio->file_content_type, 'mt-auto') ?>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="flex justify-around px-6 py-3">
|
||||
|
@ -35,8 +35,8 @@
|
||||
|
||||
foreach ($episode->soundbites as $soundbite) {
|
||||
$table->addRow(
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio_file_duration}' name='soundbites[{$soundbite->id}][start_time]' value='{$soundbite->start_time}' data-type='soundbite-field' data-field-type='start_time' required='true' />",
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio_file_duration}' name='soundbites[{$soundbite->id}][duration]' value='{$soundbite->duration}' data-type='soundbite-field' data-field-type='duration' required='true' />",
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio->duration}' name='soundbites[{$soundbite->id}][start_time]' value='{$soundbite->start_time}' data-type='soundbite-field' data-field-type='start_time' required='true' />",
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio->duration}' name='soundbites[{$soundbite->id}][duration]' value='{$soundbite->duration}' data-type='soundbite-field' data-field-type='duration' required='true' />",
|
||||
"<Forms.Input class='flex-1' name='soundbites[{$soundbite->id}][label]' value='{$soundbite->label}' />",
|
||||
"<IconButton variant='primary' glyph='play' data-type='play-soundbite' data-soundbite-id='{$soundbite->id}'>" . lang('Episode.soundbites_form.play') . '</IconButton>',
|
||||
'<IconButton uri=' . route_to(
|
||||
@ -49,8 +49,8 @@
|
||||
}
|
||||
|
||||
$table->addRow(
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio_file_duration}' name='soundbites[0][start_time]' data-type='soundbite-field' data-field-type='start_time' />",
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio_file_duration}' name='soundbites[0][duration]' data-type='soundbite-field' data-field-type='duration' />",
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio->duration}' name='soundbites[0][start_time]' data-type='soundbite-field' data-field-type='start_time' />",
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio->duration}' name='soundbites[0][duration]' data-type='soundbite-field' data-field-type='duration' />",
|
||||
"<Forms.Input class='flex-1' name='soundbites[0][label]' />",
|
||||
"<IconButton variant='primary' glyph='play' data-type='play-soundbite' data-soundbite-id='0'>" . lang('Episode.soundbites_form.play') . '</IconButton>',
|
||||
);
|
||||
@ -61,7 +61,7 @@
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<audio controls preload="auto" class="flex-1 w-full">
|
||||
<source src="<?= $episode->audio_file_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
||||
<source src="<?= $episode->audio->file_url ?>" type="<?= $episode->audio->file_content_type ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
<IconButton glyph="timer" variant="info" data-type="get-soundbite" data-start-time-field-name="soundbites[0][start_time]" data-duration-field-name="soundbites[0][duration]" ><?= lang('Episode.soundbites_form.bookmark') ?></IconButton>
|
||||
|
@ -28,7 +28,7 @@
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="mb-12">
|
||||
<?= audio_player($episode->audio_file_url, $episode->audio_file_mimetype) ?>
|
||||
<?= audio_player($episode->audio->file_url, $episode->audio->file_content_type) ?>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
|
@ -22,7 +22,7 @@
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="sticky z-40 flex flex-col w-full max-w-xs overflow-hidden shadow-sm bg-elevated border-3 border-subtle top-24 rounded-xl">
|
||||
<?php if ($podcast->banner_path !== null): ?>
|
||||
<?php if ($podcast->banner_id !== null): ?>
|
||||
<a href="<?= route_to('podcast-banner-delete', $podcast->id) ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast focus:ring-accent top-2 right-2" title="<?= lang('Podcast.form.banner_delete') ?>" data-tooltip="bottom"><?= icon('delete-bin') ?></a>
|
||||
<?php endif; ?>
|
||||
<img src="<?= $podcast->banner->small_url ?>" alt="" class="object-cover w-full aspect-[3/1] bg-header" />
|
||||
|
@ -41,12 +41,12 @@
|
||||
style="--vm-player-box-shadow:0; --vm-player-theme: hsl(var(--color-accent-base)); --vm-control-focus-color: hsl(var(--color-accent-contrast)); --vm-control-spacing: 4px; --vm-menu-item-focus-bg: hsl(var(--color-background-highlight)); --vm-control-icon-size: 24px; <?= str_ends_with($theme, 'transparent') ? '--vm-controls-bg: transparent;' : '' ?>"
|
||||
>
|
||||
<vm-audio preload="none">
|
||||
<?php $source = logged_in() ? $episode->audio_file_url : $episode->audio_file_analytics_url .
|
||||
<?php $source = logged_in() ? $episode->audio->file_url : $episode->audio_file_analytics_url .
|
||||
(isset($_SERVER['HTTP_REFERER'])
|
||||
? '?_from=' .
|
||||
parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)
|
||||
: '') ?>
|
||||
<source src="<?= $source ?>" type="<?= $episode->audio_file_mimetype ?>" />
|
||||
<source src="<?= $source ?>" type="<?= $episode->audio->file_content_type ?>" />
|
||||
</vm-audio>
|
||||
<vm-ui>
|
||||
<vm-icon-library name="castopod-icons"></vm-icon-library>
|
||||
|
@ -115,14 +115,14 @@
|
||||
title="<?= $episode->title ?>"
|
||||
podcast="<?= $episode->podcast->title ?>"
|
||||
src="<?= $episode->audio_file_web_url ?>"
|
||||
mediaType="<?= $episode->audio_file_mimetype ?>"
|
||||
mediaType="<?= $episode->audio->file_content_type ?>"
|
||||
playLabel="<?= lang('Common.play_episode_button.play') ?>"
|
||||
playingLabel="<?= lang('Common.play_episode_button.playing') ?>"></play-episode-button>
|
||||
<div class="text-xs">
|
||||
<?= relative_time($episode->published_at) ?>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= $episode->audio_file_duration ?>S">
|
||||
<?= format_duration_symbol($episode->audio_file_duration) ?>
|
||||
<time datetime="PT<?= $episode->audio->duration ?>S">
|
||||
<?= format_duration_symbol($episode->audio->duration) ?>
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<article class="flex w-full p-4 shadow bg-elevated rounded-conditional-2xl gap-x-2">
|
||||
<div class="relative">
|
||||
<time class="absolute px-1 text-xs font-semibold text-white rounded bottom-2 right-2 bg-black/75" datetime="PT<?= $episode->audio_file_duration ?>S">
|
||||
<?= format_duration($episode->audio_file_duration) ?>
|
||||
<time class="absolute px-1 text-xs font-semibold text-white rounded bottom-2 right-2 bg-black/75" datetime="PT<?= $episode->audio->duration ?>S">
|
||||
<?= format_duration($episode->audio->duration) ?>
|
||||
</time>
|
||||
<img loading="lazy" src="<?= $episode->cover
|
||||
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 rounded-lg shadow-inner aspect-square" />
|
||||
@ -20,7 +20,7 @@
|
||||
title="<?= $episode->title ?>"
|
||||
podcast="<?= $episode->podcast->title ?>"
|
||||
src="<?= $episode->audio_file_web_url ?>"
|
||||
mediaType="<?= $episode->audio_file_mimetype ?>"
|
||||
mediaType="<?= $episode->audio->file_content_type ?>"
|
||||
playLabel="<?= lang('Common.play_episode_button.play') ?>"
|
||||
playingLabel="<?= lang('Common.play_episode_button.playing') ?>"></play-episode-button>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="flex items-center border-y border-subtle">
|
||||
<div class="relative">
|
||||
<time class="absolute px-1 text-sm font-semibold text-white rounded bg-black/75 bottom-2 right-2" datetime="PT<?= $episode->audio_file_duration ?>S">
|
||||
<?= format_duration($episode->audio_file_duration) ?>
|
||||
<time class="absolute px-1 text-sm font-semibold text-white rounded bg-black/75 bottom-2 right-2" datetime="PT<?= $episode->audio->duration ?>S">
|
||||
<?= format_duration($episode->audio->duration) ?>
|
||||
</time>
|
||||
<img
|
||||
src="<?= $episode->cover->thumbnail_url ?>"
|
||||
@ -21,7 +21,7 @@
|
||||
title="<?= $episode->title ?>"
|
||||
podcast="<?= $episode->podcast->title ?>"
|
||||
src="<?= $episode->audio_file_web_url ?>"
|
||||
mediaType="<?= $episode->audio_file_mimetype ?>"
|
||||
mediaType="<?= $episode->audio->file_content_type ?>"
|
||||
playLabel="<?= lang('Common.play_episode_button.play') ?>"
|
||||
playingLabel="<?= lang('Common.play_episode_button.playing') ?>"></play-episode-button>
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user