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

feat: edit + delete podcast and episode

- refactor model / entity and controller logic for DRY code
- update episodes and podcasts
migrations
- define callbacks for podcast and episode models for enclosure update and cache
clearing
parent c815ecd6
......@@ -38,6 +38,12 @@ $routes->add('new-podcast', 'Podcast::create', ['as' => 'podcast_create']);
$routes->group('@(:podcastName)', function ($routes) {
$routes->add('/', 'Podcast::view/$1', ['as' => 'podcast_view']);
$routes->add('edit', 'Podcast::edit/$1', [
'as' => 'podcast_edit',
]);
$routes->add('delete', 'Podcast::delete/$1', [
'as' => 'podcast_delete',
]);
$routes->add('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
$routes->add('new-episode', 'Episode::create/$1', [
'as' => 'episode_create',
......@@ -45,6 +51,12 @@ $routes->group('@(:podcastName)', function ($routes) {
$routes->add('episodes/(:episodeSlug)', 'Episode::view/$1/$2', [
'as' => 'episode_view',
]);
$routes->add('episodes/(:episodeSlug)/edit', 'Episode::edit/$1/$2', [
'as' => 'episode_edit',
]);
$routes->add('episodes/(:episodeSlug)/delete', 'Episode::delete/$1/$2', [
'as' => 'episode_delete',
]);
});
// Route for podcast audio file analytics (/stats/podcast_id/episode_id/podcast_folder/filename.mp3)
......
......@@ -10,23 +10,44 @@ namespace App\Controllers;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
helper('podcast');
class Episode extends BaseController
{
public function create($podcast_name)
{
helper(['form', 'database', 'media', 'id3']);
protected \App\Entities\Podcast $podcast;
protected ?\App\Entities\Episode $episode;
$episode_model = new EpisodeModel();
public function _remap($method, ...$params)
{
$podcast_model = new PodcastModel();
$podcast = $podcast_model->where('name', $podcast_name)->first();
$this->podcast = $podcast_model->where('name', $params[0])->first();
if (count($params) > 1) {
$episode_model = new EpisodeModel();
if (
!($episode = $episode_model
->where([
'podcast_id' => $this->podcast->id,
'slug' => $params[1],
])
->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
}
return $this->$method();
}
public function create()
{
helper(['form']);
if (
!$this->validate([
'episode_file' =>
'uploaded[episode_file]|ext_in[episode_file,mp3,m4a]',
'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
'title' => 'required',
'slug' => 'required',
'description' => 'required',
......@@ -34,58 +55,19 @@ class Episode extends BaseController
])
) {
$data = [
'podcast' => $podcast,
'podcast' => $this->podcast,
];
echo view('episode/create', $data);
} else {
$episode_slug = $this->request->getVar('slug');
$episode_file = $this->request->getFile('episode_file');
$episode_file_metadata = get_file_tags($episode_file);
$image = $this->request->getFile('image');
// By default, the episode's image path is set to the podcast's
$image_path = $podcast->image_uri;
// check whether the user has inputted an image and store it
if ($image->isValid()) {
$image_path = save_podcast_media(
$image,
$podcast_name,
$episode_slug
);
} elseif ($APICdata = $episode_file_metadata['attached_picture']) {
// if the user didn't input an image,
// check if the uploaded audio file has an attached cover and store it
$cover_image = new \CodeIgniter\Files\File('episode_cover');
file_put_contents($cover_image, $APICdata);
$image_path = save_podcast_media(
$cover_image,
$podcast_name,
$episode_slug
);
}
$episode_path = save_podcast_media(
$episode_file,
$podcast_name,
$episode_slug
);
$episode = new \App\Entities\Episode([
'podcast_id' => $podcast->id,
$new_episode = new \App\Entities\Episode([
'podcast_id' => $this->podcast->id,
'title' => $this->request->getVar('title'),
'slug' => $episode_slug,
'enclosure_uri' => $episode_path,
'enclosure_length' => $episode_file->getSize(),
'enclosure_type' => $episode_file_metadata['mime_type'],
'slug' => $this->request->getVar('slug'),
'enclosure' => $this->request->getFile('enclosure'),
'pub_date' => $this->request->getVar('pub_date'),
'description' => $this->request->getVar('description'),
'duration' => $episode_file_metadata['playtime_seconds'],
'image_uri' => $image_path,
'image' => $this->request->getFile('image'),
'explicit' => $this->request->getVar('explicit') or false,
'number' => $this->request->getVar('episode_number'),
'season_number' => $this->request->getVar('season_number')
......@@ -97,30 +79,107 @@ class Episode extends BaseController
'block' => $this->request->getVar('block') or false,
]);
$episode_model->save($episode);
$episode_file = write_file_tags($podcast, $episode);
$episode_model = new EpisodeModel();
$episode_model->save($new_episode);
return redirect()->to(
base_url(route_to('episode_view', $podcast_name, $episode_slug))
base_url(
route_to(
'episode_view',
$this->podcast->name,
$new_episode->slug
)
)
);
}
}
public function view($podcast_name, $episode_slug)
public function edit()
{
$podcast_model = new PodcastModel();
$episode_model = new EpisodeModel();
helper(['form']);
if (
!$this->validate([
'enclosure' =>
'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
'title' => 'required',
'slug' => 'required',
'description' => 'required',
'type' => 'required',
])
) {
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
$podcast = $podcast_model->where('name', $podcast_name)->first();
$episode = $episode_model->where('slug', $episode_slug)->first();
echo view('episode/edit', $data);
} else {
$this->episode->title = $this->request->getVar('title');
$this->episode->slug = $this->request->getVar('slug');
$this->episode->pub_date = $this->request->getVar('pub_date');
$this->episode->description = $this->request->getVar('description');
$this->episode->explicit =
($this->request->getVar('explicit') or false);
$this->episode->number = $this->request->getVar('episode_number');
$this->episode->season_number = $this->request->getVar(
'season_number'
)
? $this->request->getVar('season_number')
: null;
$this->episode->type = $this->request->getVar('type');
$this->episode->author_name = $this->request->getVar('author_name');
$this->episode->author_email = $this->request->getVar(
'author_email'
);
$this->episode->block = ($this->request->getVar('block') or false);
$enclosure = $this->request->getFile('enclosure');
if ($enclosure->isValid()) {
$this->episode->enclosure = $this->request->getFile(
'enclosure'
);
}
$image = $this->request->getFile('image');
if ($image) {
$this->episode->image = $this->request->getFile('image');
}
$episode_model = new EpisodeModel();
$episode_model->save($this->episode);
return redirect()->to(
base_url(
route_to(
'episode_view',
$this->podcast->name,
$this->episode->slug
)
)
);
}
}
public function view()
{
self::triggerWebpageHit($this->podcast->id);
$data = [
'podcast' => $podcast,
'episode' => $episode,
'podcast' => $this->podcast,
'episode' => $this->episode,
];
self::triggerWebpageHit($data['podcast']->id);
return view('episode/view', $data);
}
public function delete()
{
$episode_model = new EpisodeModel();
$episode_model->delete($this->episode->id);
return view('episode/view.php', $data);
return redirect()->to(
base_url(route_to('podcast_view', $this->podcast->name))
);
}
}
......@@ -7,15 +7,26 @@
namespace App\Controllers;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\LanguageModel;
use App\Models\PodcastModel;
class Podcast extends BaseController
{
protected ?\App\Entities\Podcast $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
$podcast_model = new PodcastModel();
$this->podcast = $podcast_model->where('name', $params[0])->first();
}
return $this->$method();
}
public function create()
{
helper(['form', 'database', 'media', 'misc']);
helper(['form', 'misc']);
$podcast_model = new PodcastModel();
if (
......@@ -24,8 +35,8 @@ class Podcast extends BaseController
'name' => 'required|regex_match[^[a-z0-9\_]{1,191}$]',
'description' => 'required|max_length[4000]',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|max_dims[image,3000,3000]',
'owner_email' => 'required|valid_email|permit_empty',
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]',
'owner_email' => 'required|valid_email',
'type' => 'required',
])
) {
......@@ -41,18 +52,14 @@ class Podcast extends BaseController
echo view('podcast/create', $data);
} else {
$image = $this->request->getFile('image');
$podcast_name = $this->request->getVar('name');
$image_path = save_podcast_media($image, $podcast_name, 'cover');
$podcast = new \App\Entities\Podcast([
'title' => $this->request->getVar('title'),
'name' => $podcast_name,
'name' => $this->request->getVar('name'),
'description' => $this->request->getVar('description'),
'episode_description_footer' => $this->request->getVar(
'episode_description_footer'
),
'image_uri' => $image_path,
'image' => $this->request->getFile('image'),
'language' => $this->request->getVar('language'),
'category' => $this->request->getVar('category'),
'explicit' => $this->request->getVar('explicit') or false,
......@@ -77,20 +84,86 @@ class Podcast extends BaseController
}
}
public function view($podcast_name)
public function edit()
{
$podcast_model = new PodcastModel();
$episode_model = new EpisodeModel();
helper(['form', 'misc']);
if (
!$this->validate([
'title' => 'required',
'name' => 'required|regex_match[^[a-z0-9\_]{1,191}$]',
'description' => 'required|max_length[4000]',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
'owner_email' => 'required|valid_email',
'type' => 'required',
])
) {
$languageModel = new LanguageModel();
$categoryModel = new CategoryModel();
$data = [
'podcast' => $this->podcast,
'languages' => $languageModel->findAll(),
'categories' => $categoryModel->findAll(),
];
echo view('podcast/edit', $data);
} else {
$this->podcast->title = $this->request->getVar('title');
$this->podcast->name = $this->request->getVar('name');
$this->podcast->description = $this->request->getVar('description');
$this->podcast->episode_description_footer = $this->request->getVar(
'episode_description_footer'
);
$image = $this->request->getFile('image');
if ($image->isValid()) {
$this->podcast->image = $this->request->getFile('image');
}
$this->podcast->language = $this->request->getVar('language');
$this->podcast->category = $this->request->getVar('category');
$this->podcast->explicit =
($this->request->getVar('explicit') or false);
$this->podcast->author_name = $this->request->getVar('author_name');
$this->podcast->author_email = $this->request->getVar(
'author_email'
);
$this->podcast->owner_name = $this->request->getVar('owner_name');
$this->podcast->owner_email = $this->request->getVar('owner_email');
$this->podcast->type = $this->request->getVar('type');
$this->podcast->copyright = $this->request->getVar('copyright');
$this->podcast->block = ($this->request->getVar('block') or false);
$this->podcast->complete =
($this->request->getVar('complete') or false);
$this->podcast->custom_html_head = $this->request->getVar(
'custom_html_head'
);
$podcast_model = new PodcastModel();
$podcast_model->save($this->podcast);
return redirect()->to(
base_url(route_to('podcast_view', $this->podcast->name))
);
}
}
public function view()
{
self::triggerWebpageHit($this->podcast->id);
$podcast = $podcast_model->where('name', $podcast_name)->first();
$data = [
'podcast' => $podcast,
'episodes' => $episode_model
->where('podcast_id', $podcast->id)
->findAll(),
'podcast' => $this->podcast,
'episodes' => $this->podcast->episodes,
];
self::triggerWebpageHit($podcast->id);
return view('podcast/view', $data);
}
public function delete()
{
$podcast_model = new PodcastModel();
$podcast_model->delete($this->podcast->id);
return redirect()->to(base_url(route_to('home')));
}
}
......@@ -141,6 +141,10 @@ class AddPodcasts extends Migration
'updated_at' => [
'type' => 'TIMESTAMP',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('podcasts');
......
......@@ -47,25 +47,6 @@ class AddEpisodes extends Migration
'comment' =>
'The URI attribute points to your podcast media file. The file extension specified within the URI attribute determines whether or not content appears in the podcast directory. Supported file formats include M4A, MP3, MOV, MP4, M4V, and PDF.',
],
'enclosure_length' => [
'type' => 'INT',
'constraint' => 10,
'unsigned' => true,
'comment' =>
'The length attribute is the file size in bytes. You can find this information in the properties of your podcast file (on a Mac, choose File > Get Info and refer to the size field).',
],
'enclosure_type' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'comment' =>
'The type attribute provides the correct category for the type of file you are using. The type values for the supported file formats are: audio/x-m4a, audio/mpeg, video/quicktime, video/mp4, video/x-m4v, and application/pdf.',
],
'guid' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'comment' =>
'The episode’s globally unique identifier (GUID). It is very important that each episode have a unique GUID and that it never changes, even if an episode’s metadata, like title or enclosure URL, do change. Globally unique identifiers (GUID) are case-sensitive strings. If a GUID is not provided an episode’s enclosure URL will be used instead. If a GUID is not provided, make sure that an episode’s enclosure URL is unique and never changes. Failing to comply with these guidelines may result in duplicate episodes being shown to listeners, inaccurate data in Podcast Analytics, and can cause issues with your podcasts’s listing and chart placement in Apple Podcasts.',
],
'pub_date' => [
'type' => 'DATETIME',
'comment' =>
......@@ -87,6 +68,7 @@ class AddEpisodes extends Migration
'image_uri' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'null' => true,
'comment' =>
'The artwork for the show. Specify your show artwork by providing a URL linking to it. Depending on their device, subscribers see your podcast artwork in varying sizes. Therefore, make sure your design is effective at both its original size and at thumbnail size. You should include a show title, brand, or source name as part of your podcast artwork. Artwork must be a minimum size of 1400 x 1400 pixels and a maximum size of 3000 x 3000 pixels, in JPEG or PNG format, 72 dpi, with appropriate file extensions (.jpg, .png), and in the RGB colorspace.',
],
......@@ -146,6 +128,10 @@ class AddEpisodes extends Migration
'updated_at' => [
'type' => 'TIMESTAMP',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['podcast_id', 'slug']);
......
......@@ -12,24 +12,24 @@ use CodeIgniter\Entity;
class Episode extends Entity
{
protected $link;
protected $image_media_path;
protected $image_url;
protected $enclosure_media_path;
protected $enclosure_url;
protected $guid;
protected $podcast;
protected \App\Entities\Podcast $podcast;
protected string $GUID;
protected string $link;
protected \CodeIgniter\Files\File $image;
protected string $image_media_path;
protected string $image_url;
protected \CodeIgniter\Files\File $enclosure;
protected string $enclosure_media_path;
protected string $enclosure_url;
protected array $enclosure_metadata;
protected $casts = [
'slug' => 'string',
'title' => 'string',
'enclosure_uri' => 'string',
'enclosure_length' => 'integer',
'enclosure_type' => 'string',
'pub_date' => 'datetime',
'description' => 'string',
'duration' => 'integer',
'image_uri' => 'string',
'image_uri' => '?string',
'author_name' => '?string',
'author_email' => '?string',
'explicit' => 'boolean',
......@@ -39,6 +39,38 @@ class Episode extends Entity
'block' => 'boolean',
];
public function setImage(?\CodeIgniter\HTTP\Files\UploadedFile $image)
{
if ($image->isValid()) {
// check whether the user has inputted an image and store it
$this->attributes['image_uri'] = save_podcast_media(
$image,
$this->getPodcast()->name,
$this->attributes['slug']
);
} elseif (
$APICdata = $this->getEnclosureMetadata()['attached_picture']
) {
// if the user didn't input an image,
// check if the uploaded audio file has an attached cover and store it
$cover_image = new \CodeIgniter\Files\File('episode_cover');
file_put_contents($cover_image, $APICdata);
$this->attributes['image_uri'] = save_podcast_media(
$cover_image,
$this->getPodcast()->name,
$this->attributes['slug']
);
}
return $this;
}
public function getImage()
{
return new \CodeIgniter\Files\File($this->getImageMediaPath());
}
public function getImageMediaPath()
{
return media_path($this->attributes['image_uri']);
......@@ -46,11 +78,37 @@ class Episode extends Entity
public function getImageUrl()
{
return media_url($this->attributes['image_uri']);
if ($image_uri = $this->attributes['image_uri']) {
return media_url($image_uri);
}
return $this->getPodcast()->image_url;
}
public function setEnclosure(
\CodeIgniter\HTTP\Files\UploadedFile $enclosure = null
) {
if ($enclosure->isValid()) {
helper('media');
$this->attributes['enclosure_uri'] = save_podcast_media(
$enclosure,
$this->getPodcast()->name,
$this->attributes['slug']
);
return $this;
}
}
public function getEnclosure()
{
return new \CodeIgniter\Files\File($this->getEnclosureMediaPath());
}
public function getEnclosureMediaPath()
{
helper('media');
return media_path($this->attributes['enclosure_uri']);
}
......@@ -66,6 +124,13 @@ class Episode extends Entity
);
}
public function getEnclosureMetadata()
{
helper('id3');
return get_file_tags($this->getEnclosure());
}
public function getLink()
{
return base_url(
......@@ -77,7 +142,7 @@ class Episode extends Entity
);
}